diff --git a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php index 03c806e908..b5a46d8848 100644 --- a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php +++ b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php @@ -70,15 +70,15 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if ($attribute instanceof DiscriminatorMap) { $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( - method_exists($attribute, 'getTypeProperty') ? $attribute->getTypeProperty() : $attribute->typeProperty, - method_exists($attribute, 'getMapping') ? $attribute->getMapping() : $attribute->mapping, - method_exists($attribute, 'getDefaultType') ? $attribute->getDefaultType() : ($attribute->defaultType ?? null), + $attribute->typeProperty, + $attribute->mapping, + $attribute->defaultType, )); continue; } if ($attribute instanceof Groups) { - $classGroups = method_exists($attribute, 'getGroups') ? $attribute->getGroups() : $attribute->groups; + $classGroups = $attribute->groups; continue; } @@ -123,7 +123,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool // This code is adapted from Symfony\Component\Serializer\Mapping\Loader\AttributeLoader foreach ($attributes[$propertyName] as $attr) { if ($attr instanceof Groups) { - $groups = method_exists($attr, 'getGroups') ? $attr->getGroups() : $attr->groups; + $groups = $attr->groups; foreach ($groups as $group) { $attributeMetadata->addGroup($group); } @@ -131,9 +131,9 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } match (true) { - $attr instanceof MaxDepth => $attributeMetadata->setMaxDepth(method_exists($attr, 'getMaxDepth') ? $attr->getMaxDepth() : $attr->maxDepth), - $attr instanceof SerializedName => $attributeMetadata->setSerializedName(method_exists($attr, 'getSerializedName') ? $attr->getSerializedName() : $attr->serializedName), - $attr instanceof SerializedPath => $attributeMetadata->setSerializedPath(method_exists($attr, 'getSerializedPath') ? $attr->getSerializedPath() : $attr->serializedPath), + $attr instanceof MaxDepth => $attributeMetadata->setMaxDepth($attr->maxDepth), + $attr instanceof SerializedName => $attributeMetadata->setSerializedName($attr->serializedName), + $attr instanceof SerializedPath => $attributeMetadata->setSerializedPath($attr->serializedPath), $attr instanceof Ignore => $attributeMetadata->setIgnore(true), $attr instanceof Context => $this->setAttributeContextsForGroups($attr, $attributeMetadata), default => null, @@ -156,10 +156,10 @@ private function addAttributeMetadata(ApiProperty $attribute, array &$attributes private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void { - $context = method_exists($annotation, 'getContext') ? $annotation->getContext() : $annotation->context; - $groups = method_exists($annotation, 'getGroups') ? $annotation->getGroups() : $annotation->groups; - $normalizationContext = method_exists($annotation, 'getNormalizationContext') ? $annotation->getNormalizationContext() : $annotation->normalizationContext; - $denormalizationContext = method_exists($annotation, 'getDenormalizationContext') ? $annotation->getDenormalizationContext() : $annotation->denormalizationContext; + $context = $annotation->context; + $groups = $annotation->groups; + $normalizationContext = $annotation->normalizationContext; + $denormalizationContext = $annotation->denormalizationContext; if ($normalizationContext || $context) { $attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups); diff --git a/src/Serializer/Tests/Fixtures/Model/AbstractWithOtherDiscriminator.php b/src/Serializer/Tests/Fixtures/Model/AbstractWithOtherDiscriminator.php new file mode 100644 index 0000000000..12f0931479 --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/AbstractWithOtherDiscriminator.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + +#[DiscriminatorMap(typeProperty: 'kind', mapping: ['other' => ConcreteWithDiscriminator::class], defaultType: 'other')] +abstract class AbstractWithOtherDiscriminator +{ +} diff --git a/src/Serializer/Tests/Fixtures/Model/HasClassAttributes.php b/src/Serializer/Tests/Fixtures/Model/HasClassAttributes.php new file mode 100644 index 0000000000..f099c9e50f --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/HasClassAttributes.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +use Symfony\Component\Serializer\Attribute\Context; +use Symfony\Component\Serializer\Attribute\Groups; + +#[Groups(['classA', 'classB'])] +#[Context(normalizationContext: ['classNorm' => 'cn'], denormalizationContext: ['classDenorm' => 'cd'], groups: ['classGroup'])] +class HasClassAttributes +{ + public ?string $any = null; + + public ?string $other = null; +} diff --git a/src/Serializer/Tests/Fixtures/Model/HasSerializerAttributes.php b/src/Serializer/Tests/Fixtures/Model/HasSerializerAttributes.php new file mode 100644 index 0000000000..36efe42b37 --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/HasSerializerAttributes.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +use ApiPlatform\Metadata\ApiProperty; +use Symfony\Component\Serializer\Attribute\Context; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Attribute\MaxDepth; +use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; + +class HasSerializerAttributes +{ + #[ApiProperty(serialize: [new MaxDepth(2)])] + public ?self $shallow = null; + + #[ApiProperty(serialize: [new MaxDepth(7)])] + public ?self $deep = null; + + #[ApiProperty(serialize: [new SerializedName('renamed')])] + public ?string $first = null; + + #[ApiProperty(serialize: [new SerializedName('otherName')])] + public ?string $second = null; + + #[ApiProperty(serialize: [new SerializedPath('[nested][path]')])] + public ?string $nested = null; + + #[ApiProperty(serialize: [new SerializedPath('[other][spot]')])] + public ?string $elsewhere = null; + + #[ApiProperty(serialize: [new Groups(['read'])])] + public ?string $readable = null; + + #[ApiProperty(serialize: [new Groups(['write', 'admin'])])] + public ?string $writable = null; + + #[ApiProperty(serialize: [new Ignore()])] + public ?string $hidden = null; + + public ?string $plain = null; + + #[ApiProperty(serialize: [new Context(normalizationContext: ['norm' => 'n'], denormalizationContext: ['denorm' => 'd'], groups: ['split'])])] + public ?string $splitContext = null; + + #[ApiProperty(serialize: [new Context(context: ['shared' => 's'], groups: ['both'])])] + public ?string $sharedContext = null; +} diff --git a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php index 8352fdb7e5..6c0a7f2e5b 100644 --- a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php +++ b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php @@ -17,26 +17,22 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; use ApiPlatform\Serializer\Tests\Fixtures\Model\AbstractWithDiscriminator; +use ApiPlatform\Serializer\Tests\Fixtures\Model\AbstractWithOtherDiscriminator; +use ApiPlatform\Serializer\Tests\Fixtures\Model\ConcreteWithDiscriminator; +use ApiPlatform\Serializer\Tests\Fixtures\Model\HasClassAttributes; use ApiPlatform\Serializer\Tests\Fixtures\Model\HasRelation; +use ApiPlatform\Serializer\Tests\Fixtures\Model\HasSerializerAttributes; use ApiPlatform\Serializer\Tests\Fixtures\Model\Relation; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassMetadata; final class PropertyMetadataLoaderTest extends TestCase { public function testCreateMappingForASetOfProperties(): void { - $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); - $coll->method('create')->willReturn(new PropertyNameCollection(['relation'])); - $loader = new PropertyMetadataLoader($coll); - $classMetadata = new ClassMetadata(HasRelation::class); - $loader->loadClassMetadata($classMetadata); - if (method_exists($classMetadata, 'getAttributesMetadata')) { // @phpstan-ignore-line - $attributesMetadata = $classMetadata->getAttributesMetadata(); - } else { - $attributesMetadata = $classMetadata->attributesMetadata; // @phpstan-ignore-line - } + $attributesMetadata = $this->loadAttributesMetadata(['relation'], HasRelation::class); $this->assertArrayHasKey('relation', $attributesMetadata); $this->assertEquals(['read'], $attributesMetadata['relation']->getGroups()); @@ -44,34 +40,129 @@ public function testCreateMappingForASetOfProperties(): void public function testCreateMappingForAClass(): void { - $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); - $coll->method('create')->willReturn(new PropertyNameCollection(['name'])); - $loader = new PropertyMetadataLoader($coll); - $classMetadata = new ClassMetadata(Relation::class); - $loader->loadClassMetadata($classMetadata); - if (method_exists($classMetadata, 'getAttributesMetadata')) { // @phpstan-ignore-line - $attributesMetadata = $classMetadata->getAttributesMetadata(); - } else { - $attributesMetadata = $classMetadata->attributesMetadata; // @phpstan-ignore-line - } + $attributesMetadata = $this->loadAttributesMetadata(['name'], Relation::class); + $this->assertArrayHasKey('name', $attributesMetadata); $this->assertEquals(['read'], $attributesMetadata['name']->getGroups()); } - public function testForwardsDiscriminatorDefaultType(): void + public function testForwardsMaxDepth(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['shallow', 'deep']); + + $this->assertSame(2, $attributesMetadata['shallow']->getMaxDepth()); + $this->assertSame(7, $attributesMetadata['deep']->getMaxDepth()); + } + + public function testForwardsSerializedName(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['first', 'second']); + + $this->assertSame('renamed', $attributesMetadata['first']->getSerializedName()); + $this->assertSame('otherName', $attributesMetadata['second']->getSerializedName()); + } + + public function testForwardsSerializedPath(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['nested', 'elsewhere']); + + $this->assertSame('[nested][path]', (string) $attributesMetadata['nested']->getSerializedPath()); + $this->assertSame('[other][spot]', (string) $attributesMetadata['elsewhere']->getSerializedPath()); + } + + public function testForwardsPropertyGroups(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['readable', 'writable']); + + $this->assertSame(['read'], $attributesMetadata['readable']->getGroups()); + $this->assertSame(['write', 'admin'], $attributesMetadata['writable']->getGroups()); + } + + public function testForwardsIgnore(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['hidden', 'plain']); + + $this->assertTrue($attributesMetadata['hidden']->isIgnored()); + $this->assertFalse($attributesMetadata['plain']->isIgnored()); + } + + public function testForwardsSplitContextForGroups(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['splitContext']); + + $this->assertSame(['norm' => 'n'], $attributesMetadata['splitContext']->getNormalizationContextForGroups(['split'])); + $this->assertSame(['denorm' => 'd'], $attributesMetadata['splitContext']->getDenormalizationContextForGroups(['split'])); + } + + public function testForwardsSharedContextForGroups(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['sharedContext']); + + $this->assertSame(['shared' => 's'], $attributesMetadata['sharedContext']->getNormalizationContextForGroups(['both'])); + $this->assertSame(['shared' => 's'], $attributesMetadata['sharedContext']->getDenormalizationContextForGroups(['both'])); + } + + public function testForwardsClassGroupsToEveryProperty(): void + { + $attributesMetadata = $this->loadAttributesMetadata(['any', 'other'], HasClassAttributes::class); + + $this->assertSame(['classA', 'classB'], $attributesMetadata['any']->getGroups()); + $this->assertSame(['classA', 'classB'], $attributesMetadata['other']->getGroups()); + } + + public function testForwardsClassContextToEveryProperty(): void { - if (!method_exists(ClassDiscriminatorMapping::class, 'getDefaultType')) { // @phpstan-ignore-line - $this->markTestSkipped('ClassDiscriminatorMapping::getDefaultType() requires symfony/serializer 7.1+.'); + $attributesMetadata = $this->loadAttributesMetadata(['any', 'other'], HasClassAttributes::class); + + foreach (['any', 'other'] as $property) { + $this->assertSame(['classNorm' => 'cn'], $attributesMetadata[$property]->getNormalizationContextForGroups(['classGroup'])); + $this->assertSame(['classDenorm' => 'cd'], $attributesMetadata[$property]->getDenormalizationContextForGroups(['classGroup'])); } + } + /** + * @param class-string $class + * @param array $expectedMapping + */ + #[DataProvider('provideDiscriminatorCases')] + public function testForwardsDiscriminatorMapping(string $class, string $expectedTypeProperty, array $expectedMapping, string $expectedDefaultType): void + { $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); $coll->method('create')->willReturn(new PropertyNameCollection([])); $loader = new PropertyMetadataLoader($coll); - $classMetadata = new ClassMetadata(AbstractWithDiscriminator::class); + $classMetadata = new ClassMetadata($class); $loader->loadClassMetadata($classMetadata); $mapping = $classMetadata->getClassDiscriminatorMapping(); $this->assertNotNull($mapping); - $this->assertSame('concrete', $mapping->getDefaultType()); + $this->assertSame($expectedTypeProperty, $mapping->getTypeProperty()); + $this->assertSame($expectedMapping, $mapping->getTypesMapping()); + $this->assertSame($expectedDefaultType, $mapping->getDefaultType()); + } + + /** + * @return iterable, string}> + */ + public static function provideDiscriminatorCases(): iterable + { + yield 'discr' => [AbstractWithDiscriminator::class, 'discr', ['concrete' => ConcreteWithDiscriminator::class], 'concrete']; + yield 'kind' => [AbstractWithOtherDiscriminator::class, 'kind', ['other' => ConcreteWithDiscriminator::class], 'other']; + } + + /** + * @param list $properties + * @param class-string|null $class + * + * @return array + */ + private function loadAttributesMetadata(array $properties, ?string $class = null): array + { + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection($properties)); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata($class ?? HasSerializerAttributes::class); + $loader->loadClassMetadata($classMetadata); + + return $classMetadata->getAttributesMetadata(); } }