321 lines
12 KiB
PHP
321 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\BusProNet;
|
|
|
|
use App\BusProNet\Model\Address;
|
|
use App\BusProNet\Model\Communication;
|
|
use App\BusProNet\Model\CrmAttribute;
|
|
use App\BusProNet\Model\CrmAttributeGroup;
|
|
use App\BusProNet\Model\CrmAttributesResponse;
|
|
use App\BusProNet\Model\NotificationResponse;
|
|
use App\BusProNet\Model\ProfileResponse;
|
|
use App\BusProNet\Model\ProfileUpdateResponse;
|
|
use App\BusProNet\Model\RegistrationResponse;
|
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
|
|
|
class ResponseParser
|
|
{
|
|
private array $config;
|
|
|
|
public function __construct(array $options)
|
|
{
|
|
$this->config = $this->resolveOptions($options);
|
|
}
|
|
|
|
/**
|
|
* @throws ResponseParserException
|
|
*/
|
|
public function parseXmlString(string $type, string $content): mixed
|
|
{
|
|
$xml = $this->loadXml($content);
|
|
|
|
// Override type when present in XML to catch error responses
|
|
$responseType = $type;
|
|
$responseTypeXml = $xml->xpath('satz/@typ') ?: [];
|
|
if (0 < count($responseTypeXml)) {
|
|
$responseType = (string) $responseTypeXml[0];
|
|
}
|
|
|
|
switch ($responseType) {
|
|
case ApiClient::TYPE_NOTIFICATION:
|
|
return $this->createNotificationResponse($xml);
|
|
case ApiClient::TYPE_CUSTOMER_DATA:
|
|
$subType = (string) $xml->art;
|
|
switch ($subType) {
|
|
case 'Adressdaten':
|
|
return $this->createProfileResponse($xml);
|
|
case 'Adressdaten_Ändern':
|
|
return $this->createProfileUpdateResponse($xml);
|
|
case 'SelektionCRM':
|
|
return $this->createCrmAttributesResponse($xml);
|
|
case 'SelektionCRM_Ändern':
|
|
// Not a CRM attribute tree: the write answers idadresse + idperson +
|
|
// änderung, exactly the shape createProfileUpdateResponse() reads. It used
|
|
// to be routed to createCrmAttributesResponse(), which walks a
|
|
// "selektionsmerkmale" node that is absent here and would have returned an
|
|
// empty response with every role flag false - dead code until something
|
|
// sent the art.
|
|
return $this->createProfileUpdateResponse($xml);
|
|
case 'Adresse_Neu':
|
|
return $this->createRegistrationResponse($xml);
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw new ResponseParserException(sprintf('Unrecognised BusProNet response type "%s"', $responseType));
|
|
}
|
|
|
|
public function createNotificationResponse(\SimpleXMLElement $xml): NotificationResponse
|
|
{
|
|
$recordXml = $xml->satz;
|
|
|
|
$code = (int) $recordXml->nr;
|
|
$message = (string) $recordXml->text;
|
|
|
|
return new NotificationResponse($code, $message);
|
|
}
|
|
|
|
public function createProfileResponse(\SimpleXMLElement $xml): ProfileResponse
|
|
{
|
|
$addressId = (int) $xml->idadresse;
|
|
$personId = (int) $xml->idperson;
|
|
|
|
$addressXml = $xml->adressdaten;
|
|
|
|
$lastName = (string) $addressXml->name;
|
|
$firstName = (string) $addressXml->vorname;
|
|
$salutation = (string) $addressXml->anrede;
|
|
$title = (string) $addressXml->titel;
|
|
|
|
$gender = (string) $addressXml->geschlecht;
|
|
$gender = 'W' === strtoupper($gender) ? 'F' : strtoupper($gender);
|
|
|
|
$dateString = (string) $addressXml->geburtsdatum;
|
|
$dateOfBirth = null;
|
|
|
|
if ('' !== $dateString) {
|
|
$parsedDate = \DateTimeImmutable::createFromFormat('d.m.Y', $dateString);
|
|
$dateOfBirth = false === $parsedDate ? null : $parsedDate;
|
|
}
|
|
|
|
$response = new ProfileResponse();
|
|
$response
|
|
->setAddressId($addressId)
|
|
->setPersonId($personId)
|
|
->setFirstName($firstName)
|
|
->setName($lastName)
|
|
->setTitle($title)
|
|
->setSalutation($salutation)
|
|
->setGender($gender)
|
|
->setDateOfBirth($dateOfBirth)
|
|
;
|
|
|
|
$postalXml = $addressXml->anschrift;
|
|
|
|
$postalAddressStreet = (string) $postalXml->strasse;
|
|
$postalAddressPostcode = (string) $postalXml->plz;
|
|
$postalAddressCity = (string) $postalXml->ort;
|
|
$postalAddressCountry = (string) $postalXml->land;
|
|
|
|
$address = new Address();
|
|
$address
|
|
->setStreet($postalAddressStreet)
|
|
->setPostCode($postalAddressPostcode)
|
|
->setCity($postalAddressCity)
|
|
->setCountry($postalAddressCountry)
|
|
;
|
|
|
|
$response->setAddress($address);
|
|
|
|
$contactXml = $addressXml->kommunikation;
|
|
|
|
$phone = (string) $contactXml->telefonprivat;
|
|
$mobile = (string) $contactXml->telefonmobil;
|
|
$email = (string) $contactXml->email;
|
|
|
|
$communication = new Communication();
|
|
$communication
|
|
->setPhone($phone)
|
|
->setMobile($mobile)
|
|
->setEmail($email)
|
|
;
|
|
|
|
$response->setCommunication($communication);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function createProfileUpdateResponse(\SimpleXMLElement $xml): ProfileUpdateResponse
|
|
{
|
|
$addressId = (int) $xml->idadresse;
|
|
$personId = (int) $xml->idperson;
|
|
|
|
$updated = false;
|
|
$updatedXml = $xml->xpath('änderung|aenderung') ?: [];
|
|
if (0 < count($updatedXml)) {
|
|
$updated = 'true' === strtolower((string) $updatedXml[0]);
|
|
}
|
|
|
|
$response = new ProfileUpdateResponse();
|
|
$response
|
|
->setAddressId($addressId)
|
|
->setPersonId($personId)
|
|
->setUpdated($updated)
|
|
;
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* @throws ResponseParserException
|
|
*/
|
|
public function createRegistrationResponse(\SimpleXMLElement $xml): RegistrationResponse
|
|
{
|
|
$addressIdXml = $xml->xpath('idadresse') ?: [];
|
|
$personIdXml = $xml->xpath('idperson') ?: [];
|
|
|
|
// Without both ids there is nothing to carry on with: the CRM selection is set by id, and
|
|
// a local account without them cannot be matched again on the next login. Better a parse
|
|
// error naming the response than a half-provisioned applicant.
|
|
if (0 === count($addressIdXml) || 0 === count($personIdXml)) {
|
|
throw new ResponseParserException('Missing idadresse or idperson in the Adresse_Neu response');
|
|
}
|
|
|
|
$isNewRecord = false;
|
|
$isNewRecordXml = $xml->xpath('neuanlage') ?: [];
|
|
if (0 < count($isNewRecordXml)) {
|
|
$isNewRecord = 'true' === strtolower((string) $isNewRecordXml[0]);
|
|
}
|
|
|
|
return new RegistrationResponse(
|
|
(int) $addressIdXml[0],
|
|
(int) $personIdXml[0],
|
|
$isNewRecord,
|
|
);
|
|
}
|
|
|
|
public function createCrmAttributesResponse(\SimpleXMLElement $xml): CrmAttributesResponse
|
|
{
|
|
$groups = [];
|
|
$isAdmin = $isManager = $isHouseManager = $isTeamer = false;
|
|
$hotelCodes = [];
|
|
|
|
foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) {
|
|
$group = new CrmAttributeGroup();
|
|
$group->setLabel($item->attributes()['bezeichnung']);
|
|
$attributes = [];
|
|
|
|
foreach ($item->selektion as $subItem) {
|
|
$subItemAttributes = $subItem->attributes();
|
|
$attributeId = (int) $subItemAttributes['id'];
|
|
$attributeLabel = (string) $subItemAttributes['bezeichnung'];
|
|
$attributeSelected = 'True' === (string) $subItemAttributes['auswahl'];
|
|
$attribute = new CrmAttribute();
|
|
$attribute
|
|
->setId($attributeId)
|
|
->setLabel($attributeLabel)
|
|
->setSelected($attributeSelected)
|
|
;
|
|
$attributes[] = $attribute;
|
|
|
|
// Matched by id, like every other role: the "Hausleitung XXX" label is BusPro
|
|
// wording and must not decide who gets access to which house.
|
|
$houseManagerCode = $this->config['bpn_crm_house_manager_ids'][$attribute->getId()] ?? null;
|
|
if (null !== $houseManagerCode && true === $attribute->isSelected()) {
|
|
$isHouseManager = true;
|
|
$hotelCodes[] = $houseManagerCode;
|
|
}
|
|
if ($this->config['bpn_crm_id_team_admin'] === $attribute->getId() && true === $attribute->isSelected()) {
|
|
$isAdmin = true;
|
|
}
|
|
if ($this->config['bpn_crm_id_manager'] === $attribute->getId() && true === $attribute->isSelected()) {
|
|
$isManager = true;
|
|
}
|
|
if ($this->config['bpn_crm_id_teamer'] === $attribute->getId() && true === $attribute->isSelected()) {
|
|
$isTeamer = true;
|
|
}
|
|
}
|
|
$group->setAttributes($attributes);
|
|
$groups[] = $group;
|
|
}
|
|
|
|
// Apply additional role and hotel code for testing purposes when provided
|
|
if ($isAdmin && null !== $this->config['bpn_default_hotel_code']) {
|
|
$isHouseManager = true;
|
|
$hotelCodes[] = $this->config['bpn_default_hotel_code'];
|
|
}
|
|
|
|
$response = new CrmAttributesResponse();
|
|
$response
|
|
->setAttributeGroups($groups)
|
|
->setAdmin($isAdmin)
|
|
->setManager($isManager)
|
|
->setTeamer($isTeamer)
|
|
->setHouseManager($isHouseManager)
|
|
->setHotelCodes($hotelCodes)
|
|
;
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* The BusProNet endpoint can answer with an empty or truncated body. Keep the libxml
|
|
* reason instead of collapsing every shape of broken response into one message.
|
|
*
|
|
* @throws ResponseParserException
|
|
*/
|
|
private function loadXml(string $content): \SimpleXMLElement
|
|
{
|
|
if ('' === trim($content)) {
|
|
throw new ResponseParserException(sprintf('Unable to parse XML response (%d bytes): empty response', strlen($content)));
|
|
}
|
|
|
|
$previousUseErrors = libxml_use_internal_errors(true);
|
|
libxml_clear_errors();
|
|
|
|
try {
|
|
$xml = simplexml_load_string($content);
|
|
|
|
if (false === $xml) {
|
|
throw new ResponseParserException(sprintf('Unable to parse XML response (%d bytes): %s', strlen($content), $this->describeLibxmlErrors()));
|
|
}
|
|
|
|
return $xml;
|
|
} finally {
|
|
libxml_clear_errors();
|
|
libxml_use_internal_errors($previousUseErrors);
|
|
}
|
|
}
|
|
|
|
private function describeLibxmlErrors(): string
|
|
{
|
|
$messages = [];
|
|
|
|
foreach (libxml_get_errors() as $error) {
|
|
$messages[] = sprintf('%s (line %d, column %d)', trim($error->message), $error->line, $error->column);
|
|
}
|
|
|
|
if ([] === $messages) {
|
|
return 'unknown XML error';
|
|
}
|
|
|
|
return implode('; ', array_unique($messages));
|
|
}
|
|
|
|
private function resolveOptions(array $options): array
|
|
{
|
|
$optionsResolver = new OptionsResolver();
|
|
$optionsResolver->setRequired(['bpn_crm_id_team_admin', 'bpn_crm_id_manager', 'bpn_crm_id_teamer', 'bpn_crm_house_manager_ids']);
|
|
$optionsResolver->setDefaults([
|
|
'bpn_default_hotel_code' => null,
|
|
]);
|
|
$optionsResolver->setAllowedTypes('bpn_crm_id_team_admin', 'int');
|
|
$optionsResolver->setAllowedTypes('bpn_crm_id_manager', 'int');
|
|
$optionsResolver->setAllowedTypes('bpn_crm_id_teamer', 'int');
|
|
$optionsResolver->setAllowedTypes('bpn_crm_house_manager_ids', 'array');
|
|
$optionsResolver->setAllowedTypes('bpn_default_hotel_code', ['string', 'null']);
|
|
|
|
return $optionsResolver->resolve($options);
|
|
}
|
|
}
|