From f5e923ca39c11f9819a8b587c3c7038c3be11a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 9 Jan 2026 15:58:40 +0100 Subject: [PATCH] Refactor address and postal code serialization: enhance normalization logic, add `docgen:read` groups and tests, and improve null handling. --- .../Helper/NormalizeNullValueHelper.php | 8 +- .../Test/DocGenNormalizerTestAbstract.php | 3 +- .../ChillMainBundle/Doctrine/Model/Point.php | 9 +- src/Bundle/ChillMainBundle/Entity/Country.php | 3 +- .../ChillMainBundle/Entity/PostalCode.php | 10 +- .../Normalizer/AddressNormalizer.php | 36 ++-- .../AddressDocGenNormalizerTest.php | 168 ++++++++++++++++++ .../Normalizer/AddressNormalizerTest.php | 118 ++++++++++++ .../PostalCodeDocGenNormalizerTest.php | 41 +++++ 9 files changed, 369 insertions(+), 27 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressDocGenNormalizerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressNormalizerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/PostalCodeDocGenNormalizerTest.php diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php index 0ac47be61..4b59d119b 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php @@ -14,9 +14,13 @@ namespace Chill\DocGeneratorBundle\Serializer\Helper; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -class NormalizeNullValueHelper +final readonly class NormalizeNullValueHelper { - public function __construct(private readonly NormalizerInterface $normalizer, private readonly ?string $discriminatorType = null, private readonly ?string $discriminatorValue = null) {} + public function __construct( + private NormalizerInterface $normalizer, + private ?string $discriminatorType = null, + private ?string $discriminatorValue = null, + ) {} public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ?ClassMetadataInterface $classMetadata = null) { diff --git a/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php b/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php index 48d214d0e..2740374ae 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php +++ b/src/Bundle/ChillDocGeneratorBundle/Test/DocGenNormalizerTestAbstract.php @@ -42,10 +42,11 @@ abstract class DocGenNormalizerTestAbstract extends KernelTestCase self::assertIsArray($normalizedObject); self::assertIsArray($nullNormalizedObject); - self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject)); + self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject), "Compare not-null-objects's keys (expected) with null-object keys (actual)"); self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key'); self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects'); self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects'); + self::assertGreaterThan(1, count(array_keys($normalizedObject)), 'Test that there are more than one key (if fails, maybe there is only the isNull key)'); foreach ($normalizedObject as $key => $value) { if (in_array($key, ['isNull', 'type'])) { diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php index de6af9625..5a135a01b 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/Point.php @@ -11,11 +11,18 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\Model; +use Symfony\Component\Serializer\Attribute\Groups; + class Point implements \JsonSerializable { public static string $SRID = '4326'; - private function __construct(private readonly ?float $lon, private readonly ?float $lat) {} + private function __construct( + #[Groups(['read', 'docgen:read'])] + private readonly ?float $lon, + #[Groups(['read', 'docgen:read'])] + private readonly ?float $lat, + ) {} public static function fromArrayGeoJson(array $array): self { diff --git a/src/Bundle/ChillMainBundle/Entity/Country.php b/src/Bundle/ChillMainBundle/Entity/Country.php index 946733ad3..61360e72f 100644 --- a/src/Bundle/ChillMainBundle/Entity/Country.php +++ b/src/Bundle/ChillMainBundle/Entity/Country.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Serializer\Attribute\SerializedName; /** * Country. @@ -25,8 +26,8 @@ use Symfony\Component\Serializer\Attribute\Groups; class Country { #[Groups(['read', 'docgen:read'])] + #[SerializedName('code')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 3)] - #[Context(['is-translatable' => true], groups: ['docgen:read'])] private string $countryCode = ''; #[Groups(['read', 'docgen:read'])] diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index 7ebe2c02e..68d737d86 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -45,28 +45,28 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])] private string $canonical = ''; - #[Groups(['read'])] + #[Groups(['read', 'docgen:read'])] #[ORM\Column(type: 'point', nullable: true)] private ?Point $center = null; - #[Groups(['write', 'read'])] + #[Groups(['write', 'read', 'docgen:read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 100)] private ?string $code = null; - #[Groups(['write', 'read'])] + #[Groups(['write', 'read', 'docgen:read'])] #[ORM\ManyToOne(targetEntity: Country::class)] private ?Country $country = null; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])] private ?\DateTimeImmutable $deletedAt = null; - #[Groups(['write', 'read'])] + #[Groups(['write', 'read', 'docgen:read'])] #[ORM\Id] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\GeneratedValue(strategy: 'AUTO')] private ?int $id = null; - #[Groups(['write', 'read'])] + #[Groups(['write', 'read', 'docgen:read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, name: 'label')] private ?string $name = null; diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php index 2c40e17af..d8b56dc19 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/AddressNormalizer.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Serializer\Normalizer; use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\Country; +use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Templating\Entity\AddressRender; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -60,17 +62,6 @@ class AddressNormalizer implements \Symfony\Component\Serializer\Normalizer\Norm 'text' => $this->addressRender->renderString($address, []), 'street' => $address->getStreet(), 'streetNumber' => $address->getStreetNumber(), - 'postcode' => [ - 'id' => $address->getPostCode()->getId(), - 'name' => $address->getPostCode()->getName(), - 'code' => $address->getPostCode()->getCode(), - 'center' => $address->getPostcode()->getCenter(), - ], - 'country' => [ - 'id' => $address->getPostCode()->getCountry()->getId(), - 'name' => $address->getPostCode()->getCountry()->getName(), - 'code' => $address->getPostCode()->getCountry()->getCountryCode(), - ], 'floor' => $address->getFloor(), 'corridor' => $address->getCorridor(), 'steps' => $address->getSteps(), @@ -80,6 +71,7 @@ class AddressNormalizer implements \Symfony\Component\Serializer\Normalizer\Norm 'extra' => $address->getExtra(), 'confidential' => $address->getConfidential(), 'lines' => $this->addressRender->renderLines($address), + 'isNoAddress' => $address->isNoAddress(), ]; if ('json' === $format) { @@ -88,33 +80,42 @@ class AddressNormalizer implements \Symfony\Component\Serializer\Normalizer\Norm $format, [AbstractNormalizer::GROUPS => ['read']] ); - $data['validFrom'] = $address->getValidFrom(); - $data['validTo'] = $address->getValidTo(); + $data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $context); + $data['validTo'] = $this->normalizer->normalize($address->getValidTo(), $format, $context); $data['refStatus'] = $address->getRefStatus(); $data['point'] = $this->normalizer->normalize( $address->getPoint(), $format, [AbstractNormalizer::GROUPS => ['read']] ); - $data['isNoAddress'] = $address->isNoAddress(); + $data['postcode'] = $this->normalizer->normalize($address->getPostcode(), $format, [AbstractNormalizer::GROUPS => ['read']]); + $data['country'] = $this->normalizer->normalize($address->getPostcode()?->getCountry(), $format, [AbstractNormalizer::GROUPS => ['read']]); } elseif ('docgen' === $format) { $dateContext = array_merge($context, ['docgen:expects' => \DateTimeInterface::class]); $data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $dateContext); $data['validTo'] = $this->normalizer->normalize($address->getValidTo(), $format, $dateContext); + $data['postcode'] = $this->normalizer->normalize($address->getPostcode(), $format, [...$context, 'docgen:expects' => PostalCode::class]); + $data['country'] = $this->normalizer->normalize($address->getPostcode()?->getCountry(), $format, [...$context, 'docgen:expects' => Country::class]); + $data['isNull'] = false; } return $data; } if (null === $address) { + if ('json' === $format) { + return null; + } + $helper = new NormalizeNullValueHelper($this->normalizer); return array_merge( $helper->normalize(self::NULL_VALUE, $format, $context), [ - 'postcode' => $helper->normalize(self::NULL_POSTCODE_COUNTRY, $format, $context), - 'country' => $helper->normalize(self::NULL_POSTCODE_COUNTRY, $format, $context), + 'postcode' => $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => PostalCode::class]), + 'country' => $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => Country::class]), 'lines' => [], + 'isNoAddress' => true, ] ); } @@ -147,7 +148,8 @@ class AddressNormalizer implements \Symfony\Component\Serializer\Normalizer\Norm if ('docgen' === $format) { return [ - Address::class => false, + Address::class => true, + '*' => false, ]; } diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressDocGenNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressDocGenNormalizerTest.php new file mode 100644 index 000000000..73b831983 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressDocGenNormalizerTest.php @@ -0,0 +1,168 @@ +setStreet('Rue de Rosée') + ->setStreetNumber('15') + ->setFloor('2') + ->setCorridor('A') + ->setSteps('3') + ->setFlat('B') + ->setBuildingName('Le Palais') + ->setDistribution('Boite 1') + ->setExtra('Près de la fontaine') + ->setConfidential(true) + ->setValidFrom(new \DateTime('2024-01-01')) + ->setValidTo(new \DateTime('2024-12-31')) + ->setPostcode( + new PostalCode() + ->setCode('4020') + ->setName('Liège') + ->setCountry( + new Country() + ->setName(['fr' => 'Belgique']) + ->setCountryCode('BE') + ) + ); + } + + public function provideDocGenExpectClass(): string + { + return Address::class; + } + + public function testNormalizeDocGen(): void + { + $address = new Address(); + $address->setStreet('Rue de la Loi'); + $address->setStreetNumber('16'); + $address->setFloor('5'); + $address->setCorridor('B'); + $address->setSteps('12'); + $address->setFlat('A'); + $address->setBuildingName('Berlaymont'); + $address->setDistribution('Boite 5'); + $address->setExtra('Extra info'); + $address->setConfidential(false); + $address->setValidFrom(new \DateTime('2023-01-01')); + $address->setValidTo(new \DateTime('2023-12-31')); + $address->setIsNoAddress(false); + + $postalCode = new PostalCode(); + $postalCode->setCode('1000'); + $postalCode->setName('Bruxelles'); + $country = new Country(); + $country->setCountryCode('BE'); + $country->setName(['fr' => 'Belgique']); + $postalCode->setCountry($country); + + $postalCode->setCenter(Point::fromLonLat(5.0, 50.0)); + + $address->setPostcode($postalCode); + + $address->setPoint(Point::fromLonLat(5.0, 50.0)); + + $result = $this->normalizer->normalize($address, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read']]); + $this->assertIsArray($result); + + // Check main keys + $this->assertArrayHasKey('address_id', $result); + $this->assertArrayHasKey('text', $result); + $this->assertArrayHasKey('street', $result); + $this->assertArrayHasKey('streetNumber', $result); + $this->assertArrayHasKey('postcode', $result); + $this->assertArrayHasKey('country', $result); + $this->assertArrayHasKey('floor', $result); + $this->assertArrayHasKey('corridor', $result); + $this->assertArrayHasKey('steps', $result); + $this->assertArrayHasKey('flat', $result); + $this->assertArrayHasKey('buildingName', $result); + $this->assertArrayHasKey('distribution', $result); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('confidential', $result); + $this->assertArrayHasKey('lines', $result); + $this->assertArrayHasKey('validFrom', $result); + $this->assertArrayHasKey('validTo', $result); + $this->assertArrayHasKey('isNoAddress', $result); + $this->assertArrayNotHasKey('addressReference', $result); + + // Check postcode subkeys. This is because until the time of writing, those keys are in used + $this->assertIsArray($result['postcode']); + $this->assertArrayHasKey('id', $result['postcode']); + $this->assertArrayHasKey('name', $result['postcode']); + $this->assertArrayHasKey('code', $result['postcode']); + $this->assertArrayHasKey('center', $result['postcode']); + + // Check postcode subkeys. This is because until the time of writing, those keys are in used + $this->assertIsArray($result['country']); + $this->assertArrayHasKey('id', $result['country']); + $this->assertArrayHasKey('name', $result['country']); + $this->assertArrayHasKey('code', $result['country']); + } + + public function testNormalizeDocGenNullValue(): void + { + $result = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Address::class]); + $this->assertIsArray($result); + + // Check main keys + $this->assertArrayHasKey('address_id', $result); + $this->assertArrayHasKey('text', $result); + $this->assertArrayHasKey('street', $result); + $this->assertArrayHasKey('streetNumber', $result); + $this->assertArrayHasKey('postcode', $result); + $this->assertArrayHasKey('country', $result); + $this->assertArrayHasKey('floor', $result); + $this->assertArrayHasKey('corridor', $result); + $this->assertArrayHasKey('steps', $result); + $this->assertArrayHasKey('flat', $result); + $this->assertArrayHasKey('buildingName', $result); + $this->assertArrayHasKey('distribution', $result); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('confidential', $result); + $this->assertArrayHasKey('lines', $result); + $this->assertArrayHasKey('validFrom', $result); + $this->assertArrayHasKey('validTo', $result); + $this->assertArrayHasKey('isNoAddress', $result); + $this->assertArrayNotHasKey('addressReference', $result); + + // Check postcode subkeys. This is because until the time of writing, those keys are in used + $this->assertIsArray($result['postcode']); + $this->assertArrayHasKey('id', $result['postcode']); + $this->assertArrayHasKey('name', $result['postcode']); + $this->assertArrayHasKey('code', $result['postcode']); + $this->assertArrayHasKey('center', $result['postcode']); + + // Check postcode subkeys. This is because until the time of writing, those keys are in used + $this->assertIsArray($result['country']); + $this->assertArrayHasKey('id', $result['country']); + $this->assertArrayHasKey('name', $result['country']); + $this->assertArrayHasKey('code', $result['country']); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressNormalizerTest.php new file mode 100644 index 000000000..1f7317a66 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/AddressNormalizerTest.php @@ -0,0 +1,118 @@ +normalizer = self::getContainer()->get(NormalizerInterface::class); + } + + public function testNormalizeJson(): void + { + $address = new Address(); + $address->setStreet('Rue de la Loi'); + $address->setStreetNumber('16'); + $address->setFloor('5'); + $address->setCorridor('B'); + $address->setSteps('12'); + $address->setFlat('A'); + $address->setBuildingName('Berlaymont'); + $address->setDistribution('Boite 5'); + $address->setExtra('Extra info'); + $address->setConfidential(false); + $address->setValidFrom(new \DateTime('2023-01-01')); + $address->setValidTo(new \DateTime('2023-12-31')); + $address->setIsNoAddress(false); + + $postalCode = new PostalCode(); + $postalCode->setCode('1000'); + $postalCode->setName('Bruxelles'); + $country = new Country(); + $country->setCountryCode('BE'); + $country->setName(['fr' => 'Belgique']); + $postalCode->setCountry($country); + + $point = Point::fromLonLat(5.0, 50.0); + $postalCode->setCenter($point); + + $address->setPostcode($postalCode); + + $address->setPoint($point); + + $address->setAddressReference(new AddressReference()); + + $result = $this->normalizer->normalize($address, 'json', ['groups' => ['read']]); + + $this->assertIsArray($result); + + // Check main keys + $this->assertArrayHasKey('address_id', $result); + $this->assertArrayHasKey('text', $result); + $this->assertArrayHasKey('street', $result); + $this->assertArrayHasKey('streetNumber', $result); + $this->assertArrayHasKey('postcode', $result); + $this->assertArrayHasKey('country', $result); + $this->assertArrayHasKey('floor', $result); + $this->assertArrayHasKey('corridor', $result); + $this->assertArrayHasKey('steps', $result); + $this->assertArrayHasKey('flat', $result); + $this->assertArrayHasKey('buildingName', $result); + $this->assertArrayHasKey('distribution', $result); + $this->assertArrayHasKey('extra', $result); + $this->assertArrayHasKey('confidential', $result); + $this->assertArrayHasKey('lines', $result); + $this->assertArrayHasKey('addressReference', $result); + $this->assertArrayHasKey('validFrom', $result); + $this->assertArrayHasKey('validTo', $result); + $this->assertArrayHasKey('refStatus', $result); + $this->assertArrayHasKey('point', $result); + $this->assertArrayHasKey('isNoAddress', $result); + + // Check postcode subkeys + $this->assertIsArray($result['postcode']); + $this->assertArrayHasKey('id', $result['postcode']); + $this->assertArrayHasKey('name', $result['postcode']); + $this->assertArrayHasKey('code', $result['postcode']); + $this->assertArrayHasKey('center', $result['postcode']); + + // Check country subkeys + $this->assertIsArray($result['country']); + $this->assertArrayHasKey('id', $result['country']); + $this->assertArrayHasKey('name', $result['country']); + $this->assertArrayHasKey('code', $result['country']); + } + + public function testNormalizeNullJson(): void + { + $result = $this->normalizer->normalize(null, 'json', ['groups' => ['read']]); + + self::assertNull($result); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/PostalCodeDocGenNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/PostalCodeDocGenNormalizerTest.php new file mode 100644 index 000000000..5fc734353 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/PostalCodeDocGenNormalizerTest.php @@ -0,0 +1,41 @@ +setCode('4020') + ->setName('Liège') + ->setCountry( + new Country() + ->setName(['fr' => 'Belgique']) + ->setCountryCode('BE') + ); + } + + public function provideDocGenExpectClass(): string + { + return PostalCode::class; + } +}