Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/Serializer/Mapping/Loader/PropertyMetadataLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -123,17 +123,17 @@ 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);
}
continue;
}

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,
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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
{
}
26 changes: 26 additions & 0 deletions src/Serializer/Tests/Fixtures/Model/HasClassAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
}
60 changes: 60 additions & 0 deletions src/Serializer/Tests/Fixtures/Model/HasSerializerAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
}
143 changes: 117 additions & 26 deletions src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,61 +17,152 @@
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());
}

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<string, mixed> $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, array{class-string, string, array<string, mixed>, 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<string> $properties
* @param class-string|null $class
*
* @return array<string, AttributeMetadataInterface>
*/
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();
}
}
Loading