Skip to content

Commit 2e8287d

Browse files
committedOct 11, 2024
fix(laravel): allow serializer attributes through ApiProperty
1 parent 4ad7a50 commit 2e8287d

File tree

19 files changed

+469
-26
lines changed

19 files changed

+469
-26
lines changed
 

‎src/Laravel/ApiPlatformProvider.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
use ApiPlatform\Serializer\ItemNormalizer;
166166
use ApiPlatform\Serializer\JsonEncoder;
167167
use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory;
168+
use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader;
168169
use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider;
169170
use ApiPlatform\Serializer\SerializerContextBuilder;
170171
use ApiPlatform\State\CallableProcessor;
@@ -206,6 +207,7 @@
206207
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
207208
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
208209
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
210+
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
209211
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
210212
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
211213
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
@@ -244,8 +246,15 @@ public function register(): void
244246

245247
$this->app->bind(LoaderInterface::class, AttributeLoader::class);
246248
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
247-
$this->app->singleton(ClassMetadataFactory::class, function () {
248-
return new ClassMetadataFactory(new AttributeLoader());
249+
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
250+
return new ClassMetadataFactory(
251+
new LoaderChain([
252+
new PropertyMetadataLoader(
253+
$app->make(PropertyNameCollectionFactoryInterface::class),
254+
),
255+
new AttributeLoader(),
256+
])
257+
);
249258
});
250259

251260
$this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) {

‎src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@ public function create(string $resourceClass, array $options = []): PropertyName
3939
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
4040
}
4141

42-
$refl = new \ReflectionClass($resourceClass);
4342
try {
43+
$refl = new \ReflectionClass($resourceClass);
44+
if ($refl->isAbstract()) {
45+
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
46+
}
47+
4448
$model = $refl->newInstanceWithoutConstructor();
4549
} catch (\ReflectionException) {
4650
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();

‎src/Laravel/Tests/JsonApiTest.php

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Workbench\App\Models\Book;
2424
use Workbench\Database\Factories\AuthorFactory;
2525
use Workbench\Database\Factories\BookFactory;
26+
use Workbench\Database\Factories\WithAccessorFactory;
2627

2728
class JsonApiTest extends TestCase
2829
{
@@ -197,4 +198,15 @@ public function testDeleteBook(): void
197198
$response->assertStatus(204);
198199
$this->assertNull(Book::find($book->id));
199200
}
201+
202+
public function testRelationWithGroups(): void
203+
{
204+
WithAccessorFactory::new()->create();
205+
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']);
206+
$content = $response->json();
207+
$this->assertArrayHasKey('data', $content);
208+
$this->assertArrayHasKey('relationships', $content['data']);
209+
$this->assertArrayHasKey('relation', $content['data']['relationships']);
210+
$this->assertArrayHasKey('data', $content['data']['relationships']['relation']);
211+
}
200212
}

‎src/Laravel/Tests/JsonLdTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Workbench\Database\Factories\CommentFactory;
2727
use Workbench\Database\Factories\PostFactory;
2828
use Workbench\Database\Factories\SluggableFactory;
29+
use Workbench\Database\Factories\WithAccessorFactory;
2930

3031
class JsonLdTest extends TestCase
3132
{
@@ -327,4 +328,13 @@ public function testError(): void
327328
$content = $response->json();
328329
$this->assertArrayHasKey('trace', $content);
329330
}
331+
332+
public function testRelationWithGroups(): void
333+
{
334+
WithAccessorFactory::new()->create();
335+
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']);
336+
$content = $response->json();
337+
$this->assertArrayHasKey('relation', $content);
338+
$this->assertArrayHasKey('name', $content['relation']);
339+
}
330340
}

‎src/Laravel/workbench/app/Models/WithAccessor.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@
1313

1414
namespace Workbench\App\Models;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use ApiPlatform\Metadata\ApiResource;
1718
use Illuminate\Database\Eloquent\Casts\Attribute;
1819
use Illuminate\Database\Eloquent\Factories\HasFactory;
1920
use Illuminate\Database\Eloquent\Model;
21+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
22+
use Symfony\Component\Serializer\Attribute\Groups;
2023

21-
#[ApiResource]
24+
#[ApiResource(normalizationContext: ['groups' => ['read']])]
2225
class WithAccessor extends Model
2326
{
2427
use HasFactory;
2528

2629
protected $hidden = ['created_at', 'updated_at', 'id'];
2730

31+
#[ApiProperty(serialize: [new Groups(['read'])])]
32+
public function relation(): BelongsTo
33+
{
34+
return $this->belongsTo(WithAccessorRelation::class);
35+
}
36+
37+
#[ApiProperty(serialize: [new Groups(['read'])])]
2838
protected function name(): Attribute
2939
{
3040
return Attribute::make(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Models;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use Illuminate\Database\Eloquent\Factories\HasFactory;
18+
use Illuminate\Database\Eloquent\Model;
19+
use Symfony\Component\Serializer\Attribute\Groups;
20+
21+
#[Groups(['read'])]
22+
#[ApiResource(operations: [])]
23+
class WithAccessorRelation extends Model
24+
{
25+
use HasFactory;
26+
}

‎src/Laravel/workbench/database/factories/WithAccessorFactory.php

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function definition(): array
3939
{
4040
return [
4141
'name' => strtolower(fake()->name()),
42+
'relation_id' => WithAccessorRelationFactory::new(),
4243
];
4344
}
4445
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\Database\Factories;
15+
16+
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Workbench\App\Models\WithAccessorRelation;
18+
19+
/**
20+
* @template TModel of \Workbench\App\Models\WithAccessorRelation
21+
*
22+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
23+
*/
24+
class WithAccessorRelationFactory extends Factory
25+
{
26+
/**
27+
* The name of the factory's corresponding model.
28+
*
29+
* @var class-string<TModel>
30+
*/
31+
protected $model = WithAccessorRelation::class;
32+
33+
/**
34+
* Define the model's default state.
35+
*
36+
* @return array<string, mixed>
37+
*/
38+
public function definition(): array
39+
{
40+
return [
41+
'name' => strtolower(fake()->name()),
42+
];
43+
}
44+
}

‎src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php

+9
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,17 @@
2121
*/
2222
public function up(): void
2323
{
24+
Schema::create('with_accessor_relations', function (Blueprint $table): void {
25+
$table->id();
26+
$table->string('name');
27+
$table->timestamps();
28+
});
29+
2430
Schema::create('with_accessors', function (Blueprint $table): void {
2531
$table->id();
2632
$table->string('name');
33+
$table->integer('relation_id')->unsigned();
34+
$table->foreign('relation_id')->references('id')->on('with_accessor_relations');
2735
$table->timestamps();
2836
});
2937
}
@@ -34,5 +42,6 @@ public function up(): void
3442
public function down(): void
3543
{
3644
Schema::dropIfExists('with_accessors');
45+
Schema::dropIfExists('with_accessors_relation');
3746
}
3847
};

‎src/Metadata/ApiProperty.php

+47-21
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
namespace ApiPlatform\Metadata;
1515

1616
use Symfony\Component\PropertyInfo\Type;
17+
use Symfony\Component\Serializer\Attribute\Context;
18+
use Symfony\Component\Serializer\Attribute\Groups;
19+
use Symfony\Component\Serializer\Attribute\Ignore;
20+
use Symfony\Component\Serializer\Attribute\MaxDepth;
21+
use Symfony\Component\Serializer\Attribute\SerializedName;
22+
use Symfony\Component\Serializer\Attribute\SerializedPath;
1723

1824
/**
1925
* ApiProperty annotation.
@@ -23,24 +29,28 @@
2329
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)]
2430
final class ApiProperty
2531
{
32+
private ?array $types;
33+
private ?array $serialize;
34+
2635
/**
27-
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
28-
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
29-
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
30-
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
31-
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
32-
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
33-
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
34-
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
35-
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
36-
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
37-
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
38-
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
39-
* @param string[] $types the RDF types of this property
40-
* @param string[] $iris
41-
* @param Type[] $builtinTypes
42-
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
43-
* @param string|null $property The property name
36+
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
37+
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
38+
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
39+
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
40+
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
41+
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
42+
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
43+
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
44+
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
45+
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
46+
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
47+
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
48+
* @param string[] $types the RDF types of this property
49+
* @param string[] $iris
50+
* @param Type[] $builtinTypes
51+
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
52+
* @param string|null $property The property name
53+
* @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array<array-key, Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth> $serialize Serializer attributes
4454
*/
4555
public function __construct(
4656
private ?string $description = null,
@@ -193,7 +203,7 @@ public function __construct(
193203
* </div>
194204
*/
195205
private string|\Stringable|null $securityPostDenormalize = null,
196-
private array|string|null $types = null,
206+
array|string|null $types = null,
197207
/*
198208
* The related php types.
199209
*/
@@ -205,11 +215,11 @@ public function __construct(
205215
private ?string $uriTemplate = null,
206216
private ?string $property = null,
207217
private ?string $policy = null,
218+
array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null,
208219
private array $extraProperties = [],
209220
) {
210-
if (\is_string($types)) {
211-
$this->types = (array) $types;
212-
}
221+
$this->types = \is_string($types) ? (array) $types : $types;
222+
$this->serialize = \is_array($serialize) ? $serialize : (array) $serialize;
213223
}
214224

215225
public function getProperty(): ?string
@@ -600,4 +610,20 @@ public function withPolicy(?string $policy): static
600610

601611
return $self;
602612
}
613+
614+
public function getSerialize(): ?array
615+
{
616+
return $this->serialize;
617+
}
618+
619+
/**
620+
* @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array<array-key, Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth> $serialize
621+
*/
622+
public function withSerialize(array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth $serialize): static
623+
{
624+
$self = clone $this;
625+
$self->serialize = (array) $serialize;
626+
627+
return $self;
628+
}
603629
}

‎src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface
4747
'property',
4848
];
4949

50-
private const EXCLUDE = ['policy'];
50+
// TODO: add serialize support for XML (policy is Laravel-only)
51+
private const EXCLUDE = ['policy', 'serialize'];
5152

5253
/**
5354
* {@inheritdoc}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Mapping\Loader;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
18+
use Illuminate\Database\Eloquent\Model;
19+
use Symfony\Component\Serializer\Attribute\Context;
20+
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
21+
use Symfony\Component\Serializer\Attribute\Groups;
22+
use Symfony\Component\Serializer\Attribute\Ignore;
23+
use Symfony\Component\Serializer\Attribute\MaxDepth;
24+
use Symfony\Component\Serializer\Attribute\SerializedName;
25+
use Symfony\Component\Serializer\Attribute\SerializedPath;
26+
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
27+
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
28+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
29+
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
30+
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
31+
32+
/**
33+
* Loader for PHP attributes using ApiProperty.
34+
*/
35+
final class PropertyMetadataLoader implements LoaderInterface
36+
{
37+
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory)
38+
{
39+
}
40+
41+
public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
42+
{
43+
$attributesMetadata = $classMetadata->getAttributesMetadata();
44+
// It's very weird to grab Eloquent's properties in that case as they're never serialized
45+
// the Serializer makes a call on the abstract class, let's save some unneeded work with a condition
46+
if (Model::class === $classMetadata->getName()) {
47+
return false;
48+
}
49+
50+
$refl = $classMetadata->getReflectionClass();
51+
$attributes = [];
52+
$classGroups = [];
53+
$classContextAnnotation = null;
54+
55+
foreach ($refl->getAttributes(ApiProperty::class) as $clAttr) {
56+
$this->addAttributeMetadata($clAttr->newInstance(), $attributes);
57+
}
58+
59+
$attributesMetadata = $classMetadata->getAttributesMetadata();
60+
61+
foreach ($refl->getAttributes() as $a) {
62+
$attribute = $a->newInstance();
63+
if ($attribute instanceof DiscriminatorMap) {
64+
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
65+
$attribute->getTypeProperty(),
66+
$attribute->getMapping()
67+
));
68+
continue;
69+
}
70+
71+
if ($attribute instanceof Groups) {
72+
$classGroups = $attribute->getGroups();
73+
74+
continue;
75+
}
76+
77+
if ($attribute instanceof Context) {
78+
$classContextAnnotation = $attribute;
79+
}
80+
}
81+
82+
foreach ($refl->getProperties() as $reflProperty) {
83+
foreach ($reflProperty->getAttributes(ApiProperty::class) as $propAttr) {
84+
$this->addAttributeMetadata($propAttr->newInstance()->withProperty($reflProperty->name), $attributes);
85+
}
86+
}
87+
88+
foreach ($refl->getMethods() as $reflMethod) {
89+
foreach ($reflMethod->getAttributes(ApiProperty::class) as $methodAttr) {
90+
$this->addAttributeMetadata($methodAttr->newInstance()->withProperty($reflMethod->getName()), $attributes);
91+
}
92+
}
93+
94+
foreach ($this->propertyNameCollectionFactory->create($classMetadata->getName()) as $propertyName) {
95+
if (!isset($attributesMetadata[$propertyName])) {
96+
$attributesMetadata[$propertyName] = new AttributeMetadata($propertyName);
97+
$classMetadata->addAttributeMetadata($attributesMetadata[$propertyName]);
98+
}
99+
100+
foreach ($classGroups as $group) {
101+
$attributesMetadata[$propertyName]->addGroup($group);
102+
}
103+
104+
if ($classContextAnnotation) {
105+
$this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$propertyName]);
106+
}
107+
108+
if (!isset($attributes[$propertyName])) {
109+
continue;
110+
}
111+
112+
$attributeMetadata = $attributesMetadata[$propertyName];
113+
114+
// This code is adapted from Symfony\Component\Serializer\Mapping\Loader\AttributeLoader
115+
foreach ($attributes[$propertyName] as $attr) {
116+
if ($attr instanceof Groups) {
117+
foreach ($attr->getGroups() as $group) {
118+
$attributeMetadata->addGroup($group);
119+
}
120+
continue;
121+
}
122+
123+
match (true) {
124+
$attr instanceof MaxDepth => $attributeMetadata->setMaxDepth($attr->getMaxDepth()),
125+
$attr instanceof SerializedName => $attributeMetadata->setSerializedName($attr->getSerializedName()),
126+
$attr instanceof SerializedPath => $attributeMetadata->setSerializedPath($attr->getSerializedPath()),
127+
$attr instanceof Ignore => $attributeMetadata->setIgnore(true),
128+
$attr instanceof Context => $this->setAttributeContextsForGroups($attr, $attributeMetadata),
129+
default => null,
130+
};
131+
}
132+
}
133+
134+
return true;
135+
}
136+
137+
/**
138+
* @param ApiProperty[] $attributes
139+
*/
140+
private function addAttributeMetadata(ApiProperty $attribute, array &$attributes): void
141+
{
142+
if (($prop = $attribute->getProperty()) && ($value = $attribute->getSerialize())) {
143+
$attributes[$prop] = $value;
144+
}
145+
}
146+
147+
private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void
148+
{
149+
$context = $annotation->getContext();
150+
$groups = $annotation->getGroups();
151+
$normalizationContext = $annotation->getNormalizationContext();
152+
$denormalizationContext = $annotation->getDenormalizationContext();
153+
if ($normalizationContext || $context) {
154+
$attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups);
155+
}
156+
157+
if ($denormalizationContext || $context) {
158+
$attributeMetadata->setDenormalizationContextForGroups($denormalizationContext ?: $context, $groups);
159+
}
160+
}
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Tests\Fixtures\Model;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use Symfony\Component\Serializer\Attribute\Groups;
18+
19+
class HasRelation
20+
{
21+
#[ApiProperty(serialize: [new Groups(['read'])])]
22+
public function relation(): Relation
23+
{
24+
return new Relation();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Tests\Fixtures\Model;
15+
16+
use Symfony\Component\Serializer\Attribute\Groups;
17+
18+
#[Groups(['read'])]
19+
class Relation
20+
{
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Serializer\Tests\Mapping\Loader;
15+
16+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
17+
use ApiPlatform\Metadata\Property\PropertyNameCollection;
18+
use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader;
19+
use ApiPlatform\Serializer\Tests\Fixtures\Model\HasRelation;
20+
use ApiPlatform\Serializer\Tests\Fixtures\Model\Relation;
21+
use PHPUnit\Framework\TestCase;
22+
use Symfony\Component\Serializer\Mapping\ClassMetadata;
23+
24+
final class PropertyMetadataLoaderTest extends TestCase
25+
{
26+
public function testCreateMappingForASetOfProperties(): void
27+
{
28+
$coll = $this->createStub(PropertyNameCollectionFactoryInterface::class);
29+
$coll->method('create')->willReturn(new PropertyNameCollection(['relation']));
30+
$loader = new PropertyMetadataLoader($coll);
31+
$classMetadata = new ClassMetadata(HasRelation::class);
32+
$loader->loadClassMetadata($classMetadata);
33+
$this->assertArrayHasKey('relation', $classMetadata->attributesMetadata);
34+
$this->assertEquals(['read'], $classMetadata->attributesMetadata['relation']->getGroups());
35+
}
36+
37+
public function testCreateMappingForAClass(): void
38+
{
39+
$coll = $this->createStub(PropertyNameCollectionFactoryInterface::class);
40+
$coll->method('create')->willReturn(new PropertyNameCollection(['name']));
41+
$loader = new PropertyMetadataLoader($coll);
42+
$classMetadata = new ClassMetadata(Relation::class);
43+
$loader->loadClassMetadata($classMetadata);
44+
$this->assertArrayHasKey('name', $classMetadata->attributesMetadata);
45+
$this->assertEquals(['read'], $classMetadata->attributesMetadata['name']->getGroups());
46+
}
47+
}

‎src/Symfony/Bundle/ApiPlatformBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass;
2424
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
2525
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
26+
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass;
2627
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass;
2728
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass;
2829
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -58,5 +59,6 @@ public function build(ContainerBuilder $container): void
5859
$container->addCompilerPass(new TestClientPass());
5960
$container->addCompilerPass(new TestMercureHubPass());
6061
$container->addCompilerPass(new AuthenticatorManagerPass());
62+
$container->addCompilerPass(new SerializerMappingLoaderPass());
6163
}
6264
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler;
15+
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
19+
final class SerializerMappingLoaderPass implements CompilerPassInterface
20+
{
21+
public function process(ContainerBuilder $container): void
22+
{
23+
$chainLoader = $container->getDefinition('serializer.mapping.chain_loader');
24+
$loaders = $chainLoader->getArgument(0);
25+
$loaders[] = $container->getDefinition('api_platform.serializer.property_metadata_loader');
26+
$container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $loaders);
27+
}
28+
}

‎src/Symfony/Bundle/Resources/config/api.xml

+4
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,9 @@
175175

176176
<tag name="serializer.normalizer" priority="-780" />
177177
</service>
178+
179+
<service id="api_platform.serializer.property_metadata_loader" class="ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader" public="false">
180+
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
181+
</service>
178182
</services>
179183
</container>

‎tests/Symfony/Bundle/ApiPlatformBundleTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass;
2525
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
2626
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
27+
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass;
2728
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass;
2829
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass;
2930
use PHPUnit\Framework\TestCase;
@@ -54,6 +55,7 @@ public function testBuild(): void
5455
$containerProphecy->addCompilerPass(Argument::type(TestClientPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled();
5556
$containerProphecy->addCompilerPass(Argument::type(TestMercureHubPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled();
5657
$containerProphecy->addCompilerPass(Argument::type(AuthenticatorManagerPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled();
58+
$containerProphecy->addCompilerPass(Argument::type(SerializerMappingLoaderPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled();
5759

5860
$bundle = new ApiPlatformBundle();
5961
$bundle->build($containerProphecy->reveal());

0 commit comments

Comments
 (0)
Please sign in to comment.