diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php index cab134684..cbce1a422 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php +++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php @@ -96,7 +96,10 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte return 'docgen' === $format && (is_object($data) || null === $data); } - private function getExpectedType(AttributeMetadata $attribute, ReflectionClass $reflection): string + /** + * @return string|array + */ + private function getExpectedType(AttributeMetadata $attribute, ReflectionClass $reflection): string|array { $type = null; @@ -147,14 +150,31 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte } } while (null === $type && $reflection instanceof ReflectionClass); - if (null === $type ?? null) { + if ($type instanceof \ReflectionNamedType) { + return $type->getName(); + } + if ($type instanceof \ReflectionIntersectionType) { + foreach (array_map(fn (\ReflectionNamedType $t) => $t->getName(), $type->getTypes()) as $classString) { + if (ReadableCollection::class === $classString) { + return ReadableCollection::class; + } + + $class = new ReflectionClass($classString); + + if ($class->implementsInterface(ReadableCollection::class)) { + return ReadableCollection::class; + } + } + + throw new \LogicException(sprintf("The automatic normalization of intersection types is not supported, unless a %s is contained in the intersected types", ReadableCollection::class)); + } elseif (null === $type ?? null) { 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(); + throw new \LogicException(sprintf("The automatic normalization of %s is not supported", $type::class)); } /** diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php index 4ea289a8c..444254a1f 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php @@ -13,6 +13,10 @@ namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ReadableCollection; +use Doctrine\Common\Collections\Selectable; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -28,7 +32,6 @@ final class DocGenObjectNormalizerTest extends KernelTestCase protected function setUp(): void { - parent::setUp(); self::bootKernel(); $this->normalizer = self::$container->get(NormalizerInterface::class); @@ -171,6 +174,64 @@ final class DocGenObjectNormalizerTest extends KernelTestCase $this->assertEquals($expected, $normalized, 'test normalization fo an user with null center'); } + + public function testIntersectionTypeForReadableCollection(): void + { + $value = new TestableWithIntersectionReadableCollection(); + + $normalized = $this->normalizer->normalize($value, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableWithIntersectionReadableCollection::class]); + + self::assertIsArray($normalized); + self::assertIsArray($normalized['collection']); + self::assertCount(2, $normalized['collection']); + + self::assertEquals( + ['baz' => 'bloup', 'isNull' => false], + $normalized['collection'][0] + ); + self::assertEquals( + ['baz' => 'bloup', 'isNull' => false], + $normalized['collection'][1] + ); + } + + public function testIntersectionTypeForReadableCollectionWithNullValue(): void + { + $normalized = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableWithIntersectionReadableCollection::class]); + + self::assertIsArray($normalized); + self::assertIsArray($normalized['collection']); + self::assertCount(0, $normalized['collection']); + } + + public function testIntersectionTypeForCollection(): void + { + $value = new TestableWithCollection(); + + $normalized = $this->normalizer->normalize($value, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableWithCollection::class]); + + self::assertIsArray($normalized); + self::assertIsArray($normalized['collection']); + self::assertCount(2, $normalized['collection']); + + self::assertEquals( + ['baz' => 'bloup', 'isNull' => false], + $normalized['collection'][0] + ); + self::assertEquals( + ['baz' => 'bloup', 'isNull' => false], + $normalized['collection'][1] + ); + } + + public function testIntersectionTypeForCollectionWithNullValue(): void + { + $normalized = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableWithCollection::class]); + + self::assertIsArray($normalized); + self::assertIsArray($normalized['collection']); + self::assertCount(0, $normalized['collection']); + } } class TestableParentClass @@ -215,3 +276,29 @@ class TestableClassWithBool return true; } } + +class TestableWithIntersectionReadableCollection +{ + /** + * @Serializer\Groups("docgen:read") + */ + public ReadableCollection&Selectable $collection; + + public function __construct() + { + $this->collection = new ArrayCollection([new TestableChildClass(), new TestableChildClass()]); + } +} + +class TestableWithCollection +{ + /** + * @Serializer\Groups("docgen:read") + */ + public Collection&Selectable $collection; + + public function __construct() + { + $this->collection = new ArrayCollection([new TestableChildClass(), new TestableChildClass()]); + } +}