diff --git a/composer.json b/composer.json
index ceb587a..cc9c08f 100644
--- a/composer.json
+++ b/composer.json
@@ -7,6 +7,7 @@
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
+ "ext-simplexml": "*",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6c4bfed..6915d12 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -32,7 +32,4 @@
-
-
-
diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php
index 048fe38..4ec9a8b 100644
--- a/src/BusProNet/ApiClient.php
+++ b/src/BusProNet/ApiClient.php
@@ -2,16 +2,9 @@
namespace App\BusProNet;
-use App\BusProNet\Model\CrmAttributeSelection;
-use App\BusProNet\Model\Profile;
-use App\BusProNet\Model\ErrorResponse;
-use App\BusProNet\Model\Result;
+use App\BusProNet\Model\BaseResponse;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\SerializerInterface;
-use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiClient
@@ -29,7 +22,7 @@ class ApiClient
/**
* @throws ApiClientException
*/
- public function getProfile(string $email, string $password): mixed
+ public function getProfile(string $email, string $password): BaseResponse
{
$data = [
'anfrage' => [
@@ -56,11 +49,7 @@ class ApiClient
$xml = $response->getContent();
- if (str_contains($xml, 'HINWEIS')) {
- return $this->serializer->deserialize($xml, ErrorResponse::class, 'xml');
- }
-
- return $this->serializer->deserialize($xml, Profile::class, 'xml');
+ return (new ResponseParser())->parseXmlString($xml);
} catch (\Throwable $e) {
}
@@ -73,7 +62,7 @@ class ApiClient
/**
* @throws ApiClientException
*/
- public function getCrmSelection(string $email, string $password): mixed
+ public function getCrmAttributes(string $email, string $password): BaseResponse
{
$data = [
'anfrage' => [
@@ -100,20 +89,13 @@ class ApiClient
$xml = $response->getContent();
- if (str_contains($xml, 'HINWEIS')) {
- return $this->serializer->deserialize($xml, ErrorResponse::class, 'xml');
- }
-
- return $this->serializer->deserialize($xml, CrmAttributeSelection::class, 'xml');
+ return (new ResponseParser())->parseXmlString($xml);
} catch (\Throwable $e) {
}
throw new ApiClientException($e->getMessage());
}
- /**
- * @Creates key for BusPro API access according to documentation
- */
private function createKey(string $username, string $password, string $type): string
{
$date = (new \DateTimeImmutable())->format('Ymd');
diff --git a/src/BusProNet/Model/Address.php b/src/BusProNet/Model/Address.php
index 6b1c5d6..d91fe00 100644
--- a/src/BusProNet/Model/Address.php
+++ b/src/BusProNet/Model/Address.php
@@ -2,20 +2,11 @@
namespace App\BusProNet\Model;
-use Symfony\Component\Serializer\Annotation\SerializedName;
-
class Address
{
- #[SerializedName('strasse')]
private ?string $street = null;
-
- #[SerializedName('plz')]
private ?string $postCode = null;
-
- #[SerializedName('ort')]
private ?string $city = null;
-
- #[SerializedName('land')]
private ?string $country = null;
public function getStreet(): ?string
diff --git a/src/BusProNet/Model/BaseResponse.php b/src/BusProNet/Model/BaseResponse.php
new file mode 100644
index 0000000..11fde4f
--- /dev/null
+++ b/src/BusProNet/Model/BaseResponse.php
@@ -0,0 +1,41 @@
+code = $code;
+ $this->message = $message;
+ }
+
+ public function getCode(): ?int
+ {
+ return $this->code;
+ }
+
+ public function getMessage(): ?string
+ {
+ return $this->message;
+ }
+
+ public function isSuccessful(): bool
+ {
+ // Successful responses don't carry codes and messages
+ if (null === $this->getCode() && null === $this->getMessage()) {
+ return true;
+ }
+
+ // These weird and contradictory looking assertions are required because
+ // of the stupid API implementation that doesn't distinguish between error
+ // and success responses.
+ $responseIsError = 100 <= $this->getCode() || $this->getMessage() === 'Daten konnten nicht gesendet werden.';
+ $responseIsSuccess = 650 === $this->getCode() && false === stripos($this->getMessage(), 'fehler');
+
+ return false === $responseIsError && true === $responseIsSuccess;
+ }
+}
\ No newline at end of file
diff --git a/src/BusProNet/Model/Communication.php b/src/BusProNet/Model/Communication.php
index 8193d30..511e032 100644
--- a/src/BusProNet/Model/Communication.php
+++ b/src/BusProNet/Model/Communication.php
@@ -6,13 +6,8 @@ use Symfony\Component\Serializer\Annotation\SerializedName;
class Communication
{
- #[SerializedName('telefonprivat')]
private ?string $phone = null;
-
- #[SerializedName('telefonmobil')]
private ?string $mobile = null;
-
- #[SerializedName('email')]
private ?string $email = null;
public function getPhone(): ?string
diff --git a/src/BusProNet/Model/CrmAttribute.php b/src/BusProNet/Model/CrmAttribute.php
index 1559c4b..22b6900 100644
--- a/src/BusProNet/Model/CrmAttribute.php
+++ b/src/BusProNet/Model/CrmAttribute.php
@@ -2,19 +2,11 @@
namespace App\BusProNet\Model;
-use Symfony\Component\Serializer\Annotation\Ignore;
-use Symfony\Component\Serializer\Annotation\SerializedName;
-
class CrmAttribute
{
- #[SerializedName('@id')]
private ?int $id = null;
-
- #[SerializedName('@bezeichnung')]
private ?string $label = null;
-
- #[SerializedName('@auswahl')]
- private ?string $selectedAsString = null;
+ private bool $selected = false;
public function getId(): ?int
{
@@ -40,26 +32,14 @@ class CrmAttribute
return $this;
}
- public function getSelectedAsString(): ?string
- {
- return $this->selectedAsString;
- }
-
- public function setSelectedAsString(?string $selectedAsString): static
- {
- $this->selectedAsString = $selectedAsString;
-
- return $this;
- }
-
public function isSelected(): bool
{
- return 'true' === strtolower($this->selectedAsString);
+ return $this->selected;
}
public function setSelected(bool $selected): static
{
- $this->selectedAsString = $selected ? 'True' : 'False';
+ $this->selected = $selected;
return $this;
}
diff --git a/src/BusProNet/Model/CrmAttributeSelectionGroup.php b/src/BusProNet/Model/CrmAttributeGroup.php
similarity index 64%
rename from src/BusProNet/Model/CrmAttributeSelectionGroup.php
rename to src/BusProNet/Model/CrmAttributeGroup.php
index 5f054bb..e5b67ec 100644
--- a/src/BusProNet/Model/CrmAttributeSelectionGroup.php
+++ b/src/BusProNet/Model/CrmAttributeGroup.php
@@ -2,14 +2,9 @@
namespace App\BusProNet\Model;
-use Symfony\Component\Serializer\Annotation\SerializedName;
-
-class CrmAttributeSelectionGroup
+class CrmAttributeGroup
{
- #[SerializedName('@bezeichnung')]
private ?string $label = null;
-
- #[SerializedName('selektion')]
private ?array $attributes = null;
public function getLabel(): ?string
@@ -24,18 +19,11 @@ class CrmAttributeSelectionGroup
return $this;
}
- /**
- * @return CrmAttribute[]|null
- */
public function getAttributes(): ?array
{
return $this->attributes;
}
- /**
- * @param CrmAttribute[]|null $attributes
- * @return $this
- */
public function setAttributes(?array $attributes): static
{
$this->attributes = $attributes;
diff --git a/src/BusProNet/Model/CrmAttributeSelection.php b/src/BusProNet/Model/CrmAttributeSelection.php
deleted file mode 100644
index 8fbd199..0000000
--- a/src/BusProNet/Model/CrmAttributeSelection.php
+++ /dev/null
@@ -1,30 +0,0 @@
-selectionGroups;
- }
-
- /**
- * @param CrmAttributeSelectionGroup[]|null $selectionGroups
- * @return $this
- */
- public function setSelectionGroups(?array $selectionGroups): static
- {
- $this->selectionGroups = $selectionGroups;
-
- return $this;
- }
-}
\ No newline at end of file
diff --git a/src/BusProNet/Model/CrmAttributesResponse.php b/src/BusProNet/Model/CrmAttributesResponse.php
new file mode 100644
index 0000000..73a7809
--- /dev/null
+++ b/src/BusProNet/Model/CrmAttributesResponse.php
@@ -0,0 +1,32 @@
+attributeGroups;
+ }
+
+ public function setAttributeGroups(?array $attributeGroups): static{
+ $this->attributeGroups = $attributeGroups;
+
+ return $this;
+ }
+
+ public function isTeamer(): bool
+ {
+ return $this->teamer;
+ }
+
+ public function setTeamer(bool $teamer): static
+ {
+ $this->teamer = $teamer;
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/src/BusProNet/Model/ErrorResponse.php b/src/BusProNet/Model/ErrorResponse.php
deleted file mode 100644
index 14c5f00..0000000
--- a/src/BusProNet/Model/ErrorResponse.php
+++ /dev/null
@@ -1,38 +0,0 @@
-code;
- }
-
- public function setCode(?string $code): static
- {
- $this->code = $code;
-
- return $this;
- }
-
- public function getType(): ?string
- {
- return $this->type;
- }
-
- public function setType(?string $type): static
- {
- $this->type = $type;
-
- return $this;
- }
-}
\ No newline at end of file
diff --git a/src/BusProNet/Model/Profile.php b/src/BusProNet/Model/ProfileResponse.php
similarity index 78%
rename from src/BusProNet/Model/Profile.php
rename to src/BusProNet/Model/ProfileResponse.php
index 0cb966e..93ab3f0 100644
--- a/src/BusProNet/Model/Profile.php
+++ b/src/BusProNet/Model/ProfileResponse.php
@@ -2,41 +2,17 @@
namespace App\BusProNet\Model;
-use Symfony\Component\Serializer\Annotation\Context;
-use Symfony\Component\Serializer\Annotation\SerializedPath;
-use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
-
-class Profile
+class ProfileResponse extends BaseResponse
{
- #[SerializedPath('[idadresse]')]
private ?int $addressId = null;
-
- #[SerializedPath('[idperson]')]
private ?int $personId = null;
-
- #[SerializedPath('[adressdaten][name]')]
private ?string $name = null;
-
- #[SerializedPath('[adressdaten][vorname]')]
private ?string $firstName = null;
-
- #[SerializedPath('[adressdaten][anrede]')]
private ?string $salutation = null;
-
- #[SerializedPath('[adressdaten][titel]')]
private ?string $title = null;
-
- #[SerializedPath('[adressdaten][geschlecht]')]
private ?string $gender = null;
-
- #[SerializedPath('[adressdaten][geburtsdatum]')]
- #[Context([DateTimeNormalizer::FORMAT_KEY => 'd.m.Y'])]
private ?\DateTimeImmutable $dateOfBirth = null;
-
- #[SerializedPath('[adressdaten][anschrift]')]
private ?Address $address = null;
-
- #[SerializedPath('[adressdaten][kommunikation]')]
private ?Communication $communication = null;
public function getAddressId(): ?int
diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php
new file mode 100644
index 0000000..304a1f8
--- /dev/null
+++ b/src/BusProNet/ResponseParser.php
@@ -0,0 +1,143 @@
+xpath('satz/@typ')[0];
+
+ switch ($type) {
+ case 'HINWEIS':
+ return $this->createBaseResponse($xml);
+ case 'KUNDENKONTO':
+ $subType = (string) $xml->xpath('art')[0];
+ switch ($subType) {
+ case 'Adressdaten':
+ return $this->createProfileResponse($xml);
+ case 'SelektionCRM':
+ return $this->createCrmAttributesResponse($xml);
+ }
+ }
+
+ throw new ResponseParserException('Unable to parse XML response');
+ }
+
+ public function createBaseResponse(\SimpleXMLElement $xml): BaseResponse
+ {
+ $code = (int) $xml->xpath('satz/nr')[0];
+ $message = (string) $xml->xpath('satz/text')[0];
+
+ return new BaseResponse($code, $message);
+ }
+
+ public function createProfileResponse(\SimpleXMLElement $xml): ProfileResponse
+ {
+ $addressId = (int) $xml->xpath('idadresse')[0];
+ $personId = (int) $xml->xpath('idperson')[0];
+
+ $lastName = (string) $xml->xpath('adressdaten/name')[0];
+ $firstName = (string) $xml->xpath('adressdaten/vorname')[0];
+ $salutation = (string) $xml->xpath('adressdaten/anrede')[0];
+ $title = (string) $xml->xpath('adressdaten/titel')[0];
+
+ $gender = (string) $xml->xpath('adressdaten/geschlecht')[0];
+ $gender = $gender === 'W' ? 'f' : strtolower($gender);
+
+ $date = $xml->xpath('adressdaten/geburtsdatum');
+ $dateOfBirth = $date ?\DateTimeImmutable::createFromFormat('d.m.Y', (string) $date[0]) : null;
+
+ $response = new ProfileResponse();
+ $response
+ ->setAddressId($addressId)
+ ->setPersonId($personId)
+ ->setFirstName($firstName)
+ ->setName($lastName)
+ ->setTitle($title)
+ ->setSalutation($salutation)
+ ->setGender($gender)
+ ->setDateOfBirth($dateOfBirth)
+ ;
+
+ $postalAddressStreet = $xml->xpath('adressdaten/anschrift/strasse');
+ $postalAddressPostcode = $xml->xpath('adressdaten/anschrift/plz');
+ $postalAddressCity = $xml->xpath('adressdaten/anschrift/ort');
+ $postalAddressCountry = $xml->xpath('adressdaten/anschrift/land');
+
+ $address = new Address();
+ $address
+ ->setStreet($postalAddressStreet ? (string) $postalAddressStreet[0] : null)
+ ->setPostCode($postalAddressPostcode ? (string) $postalAddressPostcode[0] : null)
+ ->setCity($postalAddressCity ? (string) $postalAddressCity[0] : null)
+ ->setCountry($postalAddressCountry ? (string) $postalAddressCountry[0] : null)
+ ;
+
+ $response->setAddress($address);
+
+ $phone = $xml->xpath('adressdaten/kommunikation/telefonprivat');
+ $mobile = $xml->xpath('adressdaten/kommunikation/telefonmobil');
+ $email = $xml->xpath('adressdaten/kommunikation/email');
+
+ $communication = new Communication();
+ $communication
+ ->setPhone($phone ? (string) $phone[0] : null)
+ ->setMobile($mobile ? (string) $mobile[0] : null)
+ ->setEmail($email ? (string) $email[0] : null)
+ ;
+
+ $response->setCommunication($communication);
+
+ return $response;
+ }
+
+ public function createCrmAttributesResponse(\SimpleXMLElement $xml): CrmAttributesResponse
+ {
+ $groups = [];
+ $isTeam = false;
+
+ foreach ($xml->xpath('selektionsmerkmale/selektionsgruppe') as $item) {
+ $group = new CrmAttributeGroup();
+ $group->setLabel($item->attributes()['bezeichnung']);
+ $attributes = [];
+
+ foreach ($item->xpath('selektion') as $subItem) {
+ $attribute = new CrmAttribute();
+ $attribute
+ ->setId((int) $subItem->attributes()['id'])
+ ->setLabel((string) $subItem->attributes()['bezeichnung'])
+ ->setSelected('True' === (string) $subItem->attributes()['auswahl'])
+ ;
+ $attributes[] = $attribute;
+
+ if (static::ATTR_ID_TEAMER === $attribute->getId() && true === $attribute->isSelected()) {
+ $isTeam = true;
+ }
+ }
+ $group->setAttributes($attributes);
+ $groups[] = $group;
+ }
+
+ $response = new CrmAttributesResponse();
+ $response
+ ->setAttributeGroups($groups)
+ ->setTeamer($isTeam)
+ ;
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/src/BusProNet/ResponseParserException.php b/src/BusProNet/ResponseParserException.php
new file mode 100644
index 0000000..5c22453
--- /dev/null
+++ b/src/BusProNet/ResponseParserException.php
@@ -0,0 +1,7 @@
+getRole()];
}
public function eraseCredentials(): void
diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php
index a437164..fab56da 100644
--- a/src/Security/BpnAuthenticator.php
+++ b/src/Security/BpnAuthenticator.php
@@ -4,8 +4,8 @@ namespace App\Security;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
-use App\BusProNet\Model\ErrorResponse;
-use App\BusProNet\Model\Profile;
+use App\BusProNet\Model\CrmAttributesResponse;
+use App\BusProNet\Model\ProfileResponse;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -53,7 +53,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
return null;
}
- if ($response instanceof ErrorResponse) {
+ if (false === $response instanceof ProfileResponse) {
return null;
}
@@ -81,7 +81,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
return new RedirectResponse($url);
}
- private function getOrCreateLocalUser(Profile $profile, string $email, string $password): User
+ private function getOrCreateLocalUser(ProfileResponse $profile, string $email, string $password): ?User
{
$repository = $this->entityManager->getRepository(User::class);
@@ -95,20 +95,25 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
}
try {
- $response = $this->apiClient->getCrmSelection($email, $password);
+ /** @var CrmAttributesResponse $crmAttributes */
+ $crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
} catch (ApiClientException $e) {
+ return null;
}
$user = new User();
$user
->setBusProAddressId($profile->getAddressId())
->setBusProPersonId($profile->getPersonId())
- ->setRole('ROLE_FOO')
->setEmail($profile->getCommunication()->getEmail())
->setFirstName($profile->getFirstName())
->setLastName($profile->getName())
;
+ if ($crmAttributes->isTeamer()) {
+ $user->setRole('ROLE_TEAMER');
+ }
+
$this->entityManager->persist($user);
$this->entityManager->flush();
diff --git a/tests/BusProNet/ResponseParserTest.php b/tests/BusProNet/ResponseParserTest.php
new file mode 100644
index 0000000..08166a0
--- /dev/null
+++ b/tests/BusProNet/ResponseParserTest.php
@@ -0,0 +1,51 @@
+853ID, EMail oder Passwort falsch';
+
+ $parser = new ResponseParser();
+ $response = $parser->parseXmlString($content);
+
+ $this->assertEquals(853, $response->getCode());
+ $this->assertEquals('ID, EMail oder Passwort falsch', $response->getMessage());
+ $this->assertFalse($response->isSuccessful());
+ }
+
+ public function testParseSuccessfulProfileResponse(): void
+ {
+ $content = 'Adressdaten141747224526FrommeBjörnHerrM16.04.1972D119447Emilienstraße 5742853RemscheidDmail@fromme.orgFalse02191-4615837';
+
+ $parser = new ResponseParser();
+ $response = $parser->parseXmlString($content);
+
+ $this->assertInstanceOf(ProfileResponse::class, $response);
+ $this->assertNull($response->getCode());
+ $this->assertNull($response->getMessage());
+ $this->assertTrue($response->isSuccessful());
+ }
+
+ public function testParseSuccessfulCrmAttributesResponse(): void
+ {
+ $content = 'SelektionCRM141747224526';
+
+ $parser = new ResponseParser();
+ $response = $parser->parseXmlString($content);
+
+ $this->assertInstanceOf(CrmAttributesResponse::class, $response);
+ $this->assertNull($response->getCode());
+ $this->assertNull($response->getMessage());
+ $this->assertTrue($response->isSuccessful());
+ $this->assertCount(3, $response->getAttributeGroups());
+ $this->assertTrue($response->isTeamer());
+ }
+}
\ No newline at end of file