diff --git a/CHANGELOG.md b/CHANGELOG.md index 947649b59..a308e03cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to * ajout d'un bouton "recherche avancée" sur la page d'accueil * [person] create an accompanying course: add client-side validation if no origin (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/210) * [person] fix bounds for computing current person address: the new address appears immediatly +* [docgen] create a normalizer and serializer for normalization on doc format ## Test releases diff --git a/composer.json b/composer.json index 7b356f698..18722a420 100644 --- a/composer.json +++ b/composer.json @@ -91,7 +91,8 @@ }, "autoload-dev": { "psr-4": { - "App\\": "tests/app/src/" + "App\\": "tests/app/src/", + "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests" } }, "minimum-stability": "dev", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 46b6698ec..7943ab3aa 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,6 +4,7 @@ parameters: - src/ excludePaths: - src/Bundle/*/Tests/* + - src/Bundle/*/tests/* - src/Bundle/*/Test/* - src/Bundle/*/config/* - src/Bundle/*/migrations/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3faa756b3..80367c691 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -37,6 +37,9 @@ src/Bundle/ChillCalendarBundle/Tests/ + + src/Bundle/ChillDocGeneratorBundle/tests/ + diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Encoder/DocGenEncoder.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Encoder/DocGenEncoder.php new file mode 100644 index 000000000..da98504ae --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Encoder/DocGenEncoder.php @@ -0,0 +1,69 @@ +isAssociative($data)) { + throw new UnexpectedValueException("Only associative arrays are allowed; lists are not allowed"); + } + + $result = []; + $this->recusiveEncoding($data, $result, ''); + + return $result; + } + + private function recusiveEncoding(array $data, array &$result, $path) + { + if ($this->isAssociative($data)) { + foreach ($data as $key => $value) { + if (\is_array($value)) { + $this->recusiveEncoding($value, $result, $this->canonicalizeKey($path, $key)); + } else { + $result[$this->canonicalizeKey($path, $key)] = $value; + } + } + } else { + foreach ($data as $elem) { + + if (!$this->isAssociative($elem)) { + throw new UnexpectedValueException(sprintf("Embedded loops are not allowed. See data under %s path", $path)); + } + + $sub = []; + $this->recusiveEncoding($elem, $sub, ''); + $result[$path][] = $sub; + } + } + } + + private function canonicalizeKey(string $path, string $key): string + { + return $path === '' ? $key : $path.'_'.$key; + } + + private function isAssociative(array $data) + { + $keys = \array_keys($data); + + return $keys !== \array_keys($keys); + } + + + /** + * @inheritDoc + */ + public function supportsEncoding(string $format) + { + return $format === 'docgen'; + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php new file mode 100644 index 000000000..be5e971d7 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Helper/NormalizeNullValueHelper.php @@ -0,0 +1,47 @@ +normalizer = $normalizer; + } + + public function normalize(array $attributes, string $format = 'docgen', ?array $context = []) + { + $data = []; + + foreach ($attributes as $key => $class) { + if (is_numeric($key)) { + $data[$class] = ''; + } else { + switch ($class) { + case 'array': + case 'bool': + case 'double': + case 'float': + case 'int': + case 'resource': + case 'string': + case 'null': + $data[$key] = ''; + break; + default: + $data[$key] = $this->normalizer->normalize(null, $format, \array_merge( + $context, + ['docgen:expects' => $class] + )); + break; + } + } + } + + return $data; + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php new file mode 100644 index 000000000..08e2c63d9 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php @@ -0,0 +1,177 @@ +classMetadataFactory = $classMetadataFactory; + $this->propertyAccess = PropertyAccess::createPropertyAccessor(); + } + + /** + * @inheritDoc + */ + public function normalize($object, string $format = null, array $context = []) + { + $classMetadataKey = $object ?? $context['docgen:expects']; + + if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) { + throw new LogicException(sprintf("This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s", is_object($object) ? get_class($object) : $context['docgen:expects'], $format, \implode(', ', $context['groups']))); + } + + $metadata = $this->classMetadataFactory->getMetadataFor($classMetadataKey); + $expectedGroups = \array_key_exists(AbstractNormalizer::GROUPS, $context) ? + \is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]] + : []; + $attributes = \array_filter( + $metadata->getAttributesMetadata(), + function (AttributeMetadata $a) use ($expectedGroups) { + foreach ($a->getGroups() as $g) { + if (\in_array($g, $expectedGroups, true)) { + return true; + } + } + + return false; + }); + + if (null === $object) { + return $this->normalizeNullData($format, $context, $metadata, $attributes); + } + + return $this->normalizeObject($object, $format, $context, $expectedGroups, $metadata, $attributes); + } + + /** + * @param string $format + * @param array $context + * @param array $expectedGroups + * @param ClassMetadata $metadata + * @param array|AttributeMetadata[] $attributes + */ + private function normalizeNullData(string $format, array $context, ClassMetadata $metadata, array $attributes): array + { + $keys = []; + + foreach ($attributes as $attribute) { + $key = $attribute->getSerializedName() ?? $attribute->getName(); + $keys[$key] = $this->getExpectedType($attribute, $metadata->getReflectionClass()); + } + + $normalizer = new NormalizeNullValueHelper($this->normalizer); + + return $normalizer->normalize($keys, $format, $context); + } + + /** + * @param $object + * @param $format + * @param array $context + * @param array $expectedGroups + * @param ClassMetadata $metadata + * @param array|AttributeMetadata[] $attributes + * @return array + * @throws ExceptionInterface + */ + private function normalizeObject($object, $format, array $context, array $expectedGroups, ClassMetadata $metadata, array $attributes) + { + $data = []; + $reflection = $metadata->getReflectionClass(); + + foreach ($attributes as $attribute) { + /** @var AttributeMetadata $attribute */ + $value = $this->propertyAccess->getValue($object, $attribute->getName()); + $key = $attribute->getSerializedName() ?? $attribute->getName(); + + if (is_object($value)) { + $data[$key] = + $this->normalizer->normalize($value, $format, \array_merge( + $context, $attribute->getNormalizationContextForGroups($expectedGroups) + )); + } elseif (null === $value) { + $data[$key] = $this->normalizeNullOutputValue($format, $context, $attribute, $reflection); + } else { + $data[$key] = (string) $value; + } + } + + return $data; + } + + private function getExpectedType(AttributeMetadata $attribute, \ReflectionClass $reflection): string + { + // we have to get the expected content + if ($reflection->hasProperty($attribute->getName())) { + $type = $reflection->getProperty($attribute->getName())->getType(); + } elseif ($reflection->hasMethod($attribute->getName())) { + $type = $reflection->getMethod($attribute->getName())->getReturnType(); + } else { + throw new \LogicException(sprintf( + "Could not determine how the content is determined for the attribute %s. Add attribute property only on property or method", $attribute->getName() + )); + } + + if (null === $type) { + throw new \LogicException(sprintf( + "Could not determine the type for this attribute: %s. Add a return type to the method or property declaration", $attribute->getName() + )); + } + + return $type->getName(); + } + + /** + */ + private function normalizeNullOutputValue($format, array $context, AttributeMetadata $attribute, \ReflectionClass $reflection) + { + $type = $this->getExpectedType($attribute, $reflection); + + switch ($type) { + case 'array': + case 'bool': + case 'double': + case 'float': + case 'int': + case 'resource': + case 'string': + return ''; + default: + return $this->normalizer->normalize( + null, + $format, + \array_merge( + $context, + ['docgen:expects' => $type] + ) + ); + } + } + + /** + * @inheritDoc + */ + public function supportsNormalization($data, string $format = null): bool + { + return $format === 'docgen' && (is_object($data) || null === $data); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml index fb8ec28a1..6d87dbd25 100644 --- a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml +++ b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml @@ -8,3 +8,10 @@ services: autowire: true autoconfigure: true resource: '../Repository/' + + Chill\DocGeneratorBundle\Serializer\Normalizer\: + autowire: true + autoconfigure: true + resource: '../Serializer/Normalizer/' + tags: + - { name: 'serializer.normalizer', priority: -152 } diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Encoder/DocGenEncoderTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Encoder/DocGenEncoderTest.php new file mode 100644 index 000000000..a6267e489 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Encoder/DocGenEncoderTest.php @@ -0,0 +1,113 @@ +encoder = new DocGenEncoder(); + } + + /** + * @dataProvider generateEncodeData + */ + public function testEncode($expected, $data, string $msg) + { + $generated = $this->encoder->encode($data, 'docgen'); + $this->assertEquals($expected, $generated, $msg); + } + + public function testEmbeddedLoopsThrowsException() + { + $this->expectException(UnexpectedValueException::class); + + $data = [ + 'data' => [ + ['item' => 'one'], + [ + 'embedded' => [ + [ + ['subitem' => 'two'], + ['subitem' => 'three'] + ] + ] + ], + ] + ]; + + $this->encoder->encode($data, 'docgen'); + } + + public function generateEncodeData() + { + yield [ ['tests' => 'ok'], ['tests' => 'ok'], "A simple test with a simple array"]; + + yield [ + // expected: + ['item_subitem' => 'value'], + // data: + ['item' => ['subitem' => 'value']], + "A test with multidimensional array" + ]; + + yield [ + // expected: + [ 'data' => [['item' => 'one'], ['item' => 'two']] ], + // data: + [ 'data' => [['item' => 'one'], ['item' => 'two']] ], + "a list of items" + ]; + + yield [ + // expected: + [ 'data' => [['item_subitem' => 'alpha'], ['item' => 'two']] ], + // data: + [ 'data' => [['item' => ['subitem' => 'alpha']], ['item' => 'two'] ] ], + "a list of items with multidimensional array inside item" + ]; + + yield [ + // expected: + [ + 'persons' => [ + [ + 'firstname' => 'Jonathan', + 'lastname' => 'Dupont', + 'dateOfBirth_long' => '16 juin 1981', + 'dateOfBirth_short' => '16/06/1981', + 'father_firstname' => 'Marcel', + 'father_lastname' => 'Dupont', + 'father_dateOfBirth_long' => '10 novembre 1953', + 'father_dateOfBirth_short' => '10/11/1953' + ], + ] + ], + // data: + [ + 'persons' => [ + [ + 'firstname' => 'Jonathan', + 'lastname' => 'Dupont', + 'dateOfBirth' => [ 'long' => '16 juin 1981', 'short' => '16/06/1981'], + 'father' => [ + 'firstname' => 'Marcel', + 'lastname' => 'Dupont', + 'dateOfBirth' => ['long' => '10 novembre 1953', 'short' => '10/11/1953'] + ] + ], + ] + ], + "a longer list, with near real data inside and embedded associative arrays" + ]; + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php new file mode 100644 index 000000000..4d0031f55 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php @@ -0,0 +1,80 @@ +normalizer = self::$container->get(NormalizerInterface::class); + } + + public function testNormalizationBasic() + { + $user = new User(); + $user->setUsername('User Test'); + $user->setMainCenter($center = new Center()); + $center->setName('test'); + + $normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]); + $expected = [ + 'label' => 'User Test', + 'email' => '', + 'mainCenter' => [ + 'name' => 'test' + ] + ]; + + $this->assertEquals($expected, $normalized, "test normalization fo an user"); + } + + public function testNormalizeWithNullValueEmbedded() + { + $user = new User(); + $user->setUsername('User Test'); + + $normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]); + $expected = [ + 'label' => 'User Test', + 'email' => '', + 'mainCenter' => [ + 'name' => '' + ] + ]; + + $this->assertEquals($expected, $normalized, "test normalization fo an user with null center"); + } + + public function testNormalizeNullObjectWithObjectEmbedded() + { + $normalized = $this->normalizer->normalize(null, 'docgen', [ + AbstractNormalizer::GROUPS => ['docgen:read'], + 'docgen:expects' => User::class, + ]); + + $expected = [ + 'label' => '', + 'email' => '', + 'mainCenter' => [ + 'name' => '' + ] + ]; + + $this->assertEquals($expected, $normalized, "test normalization for a null user"); + + } + + +} diff --git a/src/Bundle/ChillMainBundle/Entity/Center.php b/src/Bundle/ChillMainBundle/Entity/Center.php index 2d87a20ab..a0c540a06 100644 --- a/src/Bundle/ChillMainBundle/Entity/Center.php +++ b/src/Bundle/ChillMainBundle/Entity/Center.php @@ -1,19 +1,19 @@ - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -23,12 +23,11 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Entity * @ORM\Table(name="centers") - * - * @author Julien Fastré */ class Center implements HasCenterInterface { @@ -46,9 +45,10 @@ class Center implements HasCenterInterface * @var string * * @ORM\Column(type="string", length=255) + * @Serializer\Groups({"docgen:read"}) */ - private $name; - + private string $name = ''; + /** * @var Collection * @@ -58,8 +58,8 @@ class Center implements HasCenterInterface * ) */ private $groupCenters; - - + + /** * Center constructor. */ @@ -67,7 +67,7 @@ class Center implements HasCenterInterface { $this->groupCenters = new \Doctrine\Common\Collections\ArrayCollection(); } - + /** * @return string */ @@ -75,7 +75,7 @@ class Center implements HasCenterInterface { return $this->name; } - + /** * @param $name * @return $this @@ -85,7 +85,7 @@ class Center implements HasCenterInterface $this->name = $name; return $this; } - + /** * @return int */ @@ -93,7 +93,7 @@ class Center implements HasCenterInterface { return $this->id; } - + /** * @return ArrayCollection|Collection */ @@ -101,7 +101,7 @@ class Center implements HasCenterInterface { return $this->groupCenters; } - + /** * @param GroupCenter $groupCenter * @return $this @@ -111,7 +111,7 @@ class Center implements HasCenterInterface $this->groupCenters->add($groupCenter); return $this; } - + /** * @return string */ @@ -119,7 +119,7 @@ class Center implements HasCenterInterface { return $this->getName(); } - + /** * @return $this|Center */ diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 2048667e1..2d8636e5f 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -8,7 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Chill\MainBundle\Entity\UserJob; use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Serializer\Annotation\DiscriminatorMap; +use Symfony\Component\Serializer\Annotation as Serializer; /** * User @@ -16,7 +16,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap; * @ORM\Entity * @ORM\Table(name="users") * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region") - * @DiscriminatorMap(typeProperty="type", mapping={ + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ * "user"=User::class * }) */ @@ -51,6 +51,7 @@ class User implements AdvancedUserInterface { /** * @ORM\Column(type="string", length=200) + * @Serializer\Groups({"docgen:read"}) */ private string $label = ''; @@ -58,8 +59,9 @@ class User implements AdvancedUserInterface { * @var string * * @ORM\Column(type="string", length=150, nullable=true) + * @Serializer\Groups({"docgen:read"}) */ - private $email; + private ?string $email = null; /** * @var string @@ -123,6 +125,7 @@ class User implements AdvancedUserInterface { /** * @var Center|null * @ORM\ManyToOne(targetEntity=Center::class) + * @Serializer\Groups({"docgen:read"}) */ private ?Center $mainCenter = null; diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php index f1681c60a..aeb437336 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php @@ -1,18 +1,18 @@ - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -27,7 +27,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** - * + * * */ class CenterNormalizer implements NormalizerInterface, DenormalizerInterface @@ -52,7 +52,7 @@ class CenterNormalizer implements NormalizerInterface, DenormalizerInterface public function supportsNormalization($data, string $format = null): bool { - return $data instanceof Center; + return $data instanceof Center && $format === 'json'; } public function denormalize($data, string $type, string $format = null, array $context = []) diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php index 8e333a2a4..288a6865d 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DateNormalizer.php @@ -19,23 +19,78 @@ namespace Chill\MainBundle\Serializer\Normalizer; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -class DateNormalizer implements NormalizerInterface, DenormalizerInterface +class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface { + private RequestStack $requestStack; + private ParameterBagInterface $parameterBag; + + public function __construct(RequestStack $requestStack, ParameterBagInterface $parameterBag) + { + $this->requestStack = $requestStack; + $this->parameterBag = $parameterBag; + } + public function normalize($date, string $format = null, array $context = array()) { /** @var \DateTimeInterface $date */ - return [ - 'datetime' => $date->format(\DateTimeInterface::ISO8601) - ]; + switch($format) { + case 'json': + return [ + 'datetime' => $date->format(\DateTimeInterface::ISO8601) + ]; + case 'docgen': + + if (null === $date) { + return [ + 'long' => '', 'short' => '' + ]; + } + + $hasTime = $date->format('His') !== "000000"; + $request = $this->requestStack->getCurrentRequest(); + $locale = null !== $request ? $request->getLocale() : $this->parameterBag->get('kernel.default_locale'); + $formatterLong = \IntlDateFormatter::create( + $locale, + \IntlDateFormatter::LONG, + $hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE + ); + $formatterShort = \IntlDateFormatter::create( + $locale, + \IntlDateFormatter::SHORT, + $hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE + ); + + return [ + 'short' => $formatterShort->format($date), + 'long' => $formatterLong->format($date) + ]; + } } - public function supportsNormalization($data, string $format = null): bool + public function supportsNormalization($data, string $format = null, array $context = []): bool { - return $data instanceof \DateTimeInterface; + if ($format === 'json') { + return $data instanceof \DateTimeInterface; + } elseif ($format === 'docgen') { + return $data instanceof \DateTimeInterface || ( + $data === null + && \array_key_exists('docgen:expects', $context) + && ( + $context['docgen:expects'] === \DateTimeInterface::class + || $context['docgen:expects'] === \DateTime::class + || $context['docgen:expects'] === \DateTimeImmutable::class + ) + ); + } + + return false; } public function denormalize($data, string $type, string $format = null, array $context = []) diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php index af29ad4f4..13315bc9c 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php @@ -52,6 +52,6 @@ class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface public function supportsNormalization($data, string $format = null): bool { - return $data instanceof User; + return $format === 'json' && $data instanceof User; } } diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DateNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DateNormalizerTest.php new file mode 100644 index 000000000..03e3015e8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/DateNormalizerTest.php @@ -0,0 +1,88 @@ +prophet = new Prophet(); + } + + public function testSupports() + { + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'json')); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'json')); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'docgen')); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'docgen')); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeImmutable::class])); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeInterface::class])); + $this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTime::class])); + $this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \stdClass(), 'docgen')); + $this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'xml')); + } + + /** + * @dataProvider generateDataNormalize + */ + public function testNormalize($expected, $date, $format, $locale, $msg) + { + $this->assertEquals($expected, $this->buildDateNormalizer($locale)->normalize($date, $format, []), $msg); + } + + private function buildDateNormalizer(string $locale = null): DateNormalizer + { + $requestStack = $this->prophet->prophesize(RequestStack::class); + $parameterBag = new ParameterBag(); + $parameterBag->set('kernel.default_locale', 'fr'); + + if ($locale === null) { + $requestStack->getCurrentRequest()->willReturn(null); + } else { + $request = $this->prophet->prophesize(Request::class); + $request->getLocale()->willReturn($locale); + $requestStack->getCurrentRequest()->willReturn($request->reveal()); + } + + return new DateNormalizer($requestStack->reveal(), $parameterBag); + } + + public function generateDataNormalize() + { + $datetime = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 15:05:01+02:00'); + $date = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 00:00:00+02:00'); + yield [ + ['datetime' => '2021-06-05T15:05:01+0200'], + $datetime, 'json', null, 'simple normalization to json' + ]; + + yield [ + ['long' => '5 juin 2021', 'short' => '05/06/2021'], + $date, 'docgen', 'fr', 'normalization to docgen for a date, with current request' + ]; + + yield [ + ['long' => '5 juin 2021', 'short' => '05/06/2021'], + $date, 'docgen', null, 'normalization to docgen for a date, without current request' + ]; + + yield [ + ['long' => '5 juin 2021 à 15:05', 'short' => '05/06/2021 15:05'], + $datetime, 'docgen', null, 'normalization to docgen for a datetime, without current request' + ]; + + yield [ + ['long' => '', 'short' => ''], + null, 'docgen', null, 'normalization to docgen for a null datetime' + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 15f33abe7..74c7be856 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -1139,11 +1139,11 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface public function getGroupSequence() { - if ($this->getStep() == self::STEP_DRAFT) { + if ($this->getStep() == self::STEP_DRAFT) + { return [[self::STEP_DRAFT]]; - } - - if ($this->getStep() == self::STEP_CONFIRMED) { + } elseif ($this->getStep() == self::STEP_CONFIRMED) + { return [[self::STEP_DRAFT, self::STEP_CONFIRMED]]; } diff --git a/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php b/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php index 67185c36a..afca96370 100644 --- a/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php +++ b/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php @@ -37,20 +37,20 @@ class MaritalStatus * @ORM\Id() * @ORM\Column(type="string", length=7) */ - private $id; + private ?string $id; /** * @var string array * @ORM\Column(type="json") */ - private $name; + private array $name; /** * Get id * * @return string */ - public function getId() + public function getId(): string { return $this->id; } @@ -61,7 +61,7 @@ class MaritalStatus * @param string $id * @return MaritalStatus */ - public function setId($id) + public function setId(string $id): self { $this->id = $id; return $this; @@ -73,7 +73,7 @@ class MaritalStatus * @param string array $name * @return MaritalStatus */ - public function setName($name) + public function setName(array $name): self { $this->name = $name; @@ -85,7 +85,7 @@ class MaritalStatus * * @return string array */ - public function getName() + public function getName(): array { return $this->name; } diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index dcb70510b..a7e8fec68 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -219,7 +219,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * groups={"general", "creation"} * ) */ - private ?\DateTime $maritalStatusDate; + private ?\DateTime $maritalStatusDate = null; /** * Comment on marital status @@ -252,7 +252,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * The person's phonenumber * @var string * - * @ORM\Column(type="text", length=40, nullable=true) + * @ORM\Column(type="text") * @Assert\Regex( * pattern="/^([\+{1}])([0-9\s*]{4,20})$/", * groups={"general", "creation"} @@ -262,13 +262,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * groups={"general", "creation"} * ) */ - private $phonenumber = ''; + private string $phonenumber = ''; /** * The person's mobile phone number * @var string * - * @ORM\Column(type="text", length=40, nullable=true) + * @ORM\Column(type="text") * @Assert\Regex( * pattern="/^([\+{1}])([0-9\s*]{4,20})$/", * groups={"general", "creation"} @@ -278,7 +278,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * groups={"general", "creation"} * ) */ - private $mobilenumber = ''; + private string $mobilenumber = ''; /** * @var Collection @@ -1094,9 +1094,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI /** * Get nationality * - * @return Chill\MainBundle\Entity\Country + * @return Country */ - public function getNationality() + public function getNationality(): ?Country { return $this->nationality; } @@ -1176,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * * @return string */ - public function getPhonenumber() + public function getPhonenumber(): string { return $this->phonenumber; } @@ -1199,7 +1199,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI * * @return string */ - public function getMobilenumber() + public function getMobilenumber(): string { return $this->mobilenumber; } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php new file mode 100644 index 000000000..769e1c907 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonDocGenNormalizer.php @@ -0,0 +1,116 @@ +personRender = $personRender; + $this->translator = $translator; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function normalize($person, string $format = null, array $context = []) + { + /** @var Person $person */ + + $dateContext = $context; + $dateContext['docgen:expects'] = \DateTimeInterface::class; + + if (null === $person) { + return $this->normalizeNullValue($format, $context); + } + + return [ + 'firstname' => $person->getFirstName(), + 'lastname' => $person->getLastName(), + 'altNames' => \implode( + ', ', + \array_map( + function (PersonAltName $altName) { + return $altName->getLabel(); + }, + $person->getAltNames()->toArray() + ) + ), + 'text' => $this->personRender->renderString($person, []), + 'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext), + 'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext), + 'gender' => $this->translator->trans($person->getGender()), + 'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '', + 'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext), + 'email' => $person->getEmail(), + 'firstPhoneNumber' => $person->getPhonenumber() ?? $person->getMobilenumber(), + 'fixPhoneNumber' => $person->getPhonenumber(), + 'mobilePhoneNumber' => $person->getMobilenumber(), + 'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '', + 'placeOfBirth' => $person->getPlaceOfBirth(), + 'memo' => $person->getMemo(), + 'numberOfChildren' => (string) $person->getNumberOfChildren(), + ]; + } + + private function normalizeNullValue(string $format, array $context) + { + $normalizer = new NormalizeNullValueHelper($this->normalizer); + + $attributes = [ + 'firstname', 'lastname', 'altNames', 'text', + 'birthdate' => \DateTimeInterface::class, + 'deathdate' => \DateTimeInterface::class, + 'gender', 'maritalStatus', + 'maritalStatusDate' => \DateTimeInterface::class, + 'email', 'firstPhoneNumber', 'fixPhoneNumber', 'mobilePhoneNumber', 'nationality', + 'placeOfBirth', 'memo', 'numberOfChildren' + ]; + + return $normalizer->normalize($attributes, $format, $context); + } + + public function supportsNormalization($data, string $format = null, array $context = []) + { + if ($format !== 'docgen') { + return false; + } + + return + $data instanceof Person + || ( + \array_key_exists('docgen:expects', $context) + && $context['docgen:expects'] === Person::class + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php similarity index 72% rename from src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php rename to src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php index b7e0069ee..3b42e86db 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php @@ -39,7 +39,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; * Serialize a Person entity * */ -class PersonNormalizer implements +class PersonJsonNormalizer implements NormalizerInterface, NormalizerAwareInterface, DenormalizerInterface, @@ -105,7 +105,7 @@ class PersonNormalizer implements public function supportsNormalization($data, string $format = null): bool { - return $data instanceof Person; + return $data instanceof Person && $format === 'json'; } public function denormalize($data, string $type, string $format = null, array $context = []) @@ -128,45 +128,48 @@ class PersonNormalizer implements $person = new Person(); } - $properties = ['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender']; + foreach (['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender', + 'birthdate', 'deathdate', 'center'] + as $item) { - $properties = array_filter( - $properties, - static fn (string $property): bool => array_key_exists($property, $data) - ); - - foreach ($properties as $item) { - $callable = [$person, sprintf('set%s', ucfirst($item))]; - - if (is_callable($callable)) { - $closure = \Closure::fromCallable($callable); - - $closure($data[$item]); + if (!\array_key_exists($item, $data)) { + continue; } - } - $propertyToClassMapping = [ - 'birthdate' => \DateTime::class, - 'deathdate' => \DateTime::class, - 'center' => Center::class, - ]; - - $propertyToClassMapping = array_filter( - $propertyToClassMapping, - static fn (string $item): bool => array_key_exists($item, $data) - ); - - foreach ($propertyToClassMapping as $item => $class) { - $object = $this->denormalizer->denormalize($data[$item], $class, $format, $context); - - if ($object instanceof $class) { - $callable = [$object, sprintf('set%s', ucfirst($item))]; - - if (is_callable($callable)) { - $closure = \Closure::fromCallable($callable); - - $closure($object); - } + switch ($item) { + case 'firstName': + $person->setFirstName($data[$item]); + break; + case 'lastName': + $person->setLastName($data[$item]); + break; + case 'phonenumber': + $person->setPhonenumber($data[$item]); + break; + case 'mobilenumber': + $person->setMobilenumber($data[$item]); + break; + case 'gender': + $person->setGender($data[$item]); + break; + case 'birthdate': + $object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context); + if ($object instanceof \DateTime) { + $person->setBirthdate($object); + } + break; + case 'deathdate': + $object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context); + if ($object instanceof \DateTime) { + $person->setDeathdate($object); + } + break; + case 'center': + $object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context); + $person->setCenter($object); + break; + default: + throw new \LogicException("item not defined: $item"); } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php new file mode 100644 index 000000000..96a744913 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonDocGenNormalizerTest.php @@ -0,0 +1,71 @@ +normalizer = self::$container->get(NormalizerInterface::class); + } + + /** + * @dataProvider generateData + */ + public function testNormalize(?Person $person, $expected, $msg) + { + $normalized = $this->normalizer->normalize($person, 'docgen', ['docgen:expects' => Person::class]); + + $this->assertEquals($expected, $normalized, $msg); + } + + public function generateData() + { + $person = new Person(); + $person + ->setFirstName('Renaud') + ->setLastName('Mégane') + ; + + $expected = \array_merge( + self::BLANK, ['firstname' => 'Renaud', 'lastname' => 'Mégane', + 'text' => 'Renaud Mégane'] + ); + + yield [$person, $expected, 'partial normalization for a person']; + + yield [null, self::BLANK, 'normalization for a null person']; + } + + + private const BLANK = [ + 'firstname' => '', + 'lastname' => '', + 'altNames' => '', + 'text' => '', + 'birthdate' => ['short' => '', 'long' => ''], + 'deathdate' => ['short' => '', 'long' => ''], + 'gender' => '', + 'maritalStatus' => '', + 'maritalStatusDate' => ['short' => '', 'long' => ''], + 'email' => '', + 'firstPhoneNumber' => '', + 'fixPhoneNumber' => '', + 'mobilePhoneNumber' => '', + 'nationality' => '', + 'placeOfBirth' => '', + 'memo' => '', + 'numberOfChildren' => '' + ]; + +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php new file mode 100644 index 000000000..4839d84f5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonNormalizerTest.php @@ -0,0 +1,28 @@ +normalizer = self::$container->get(NormalizerInterface::class); + } + + public function testNormalization() + { + $person = new Person(); + $result = $this->normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => [ 'read' ]]); + + $this->assertIsArray($result); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/serializer.yaml b/src/Bundle/ChillPersonBundle/config/services/serializer.yaml index f64b3121b..5a1e54400 100644 --- a/src/Bundle/ChillPersonBundle/config/services/serializer.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/serializer.yaml @@ -4,6 +4,7 @@ services: Chill\PersonBundle\Serializer\Normalizer\: autowire: true + autoconfigure: true resource: '../../Serializer/Normalizer' tags: - { name: 'serializer.normalizer', priority: 64 } diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211112170027.php b/src/Bundle/ChillPersonBundle/migrations/Version20211112170027.php new file mode 100644 index 000000000..65434bb33 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20211112170027.php @@ -0,0 +1,37 @@ +addSql('UPDATE chill_person_person SET mobilenumber = \'\' WHERE mobilenumber IS NULL'); + $this->addSql('UPDATE chill_person_person SET phonenumber = \'\' WHERE phonenumber IS NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET NOT NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET NOT NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT \'\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER phonenumber DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber SET DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_person_person ALTER phonenumber SET DEFAULT NULL'); + } +}