Skip to content

Commit ec91360

Browse files
committed
fix(SerializerPropertyMetadataFactory): add support for normalization/denormalization with attributes
1 parent 36c1db8 commit ec91360

File tree

2 files changed

+124
-18
lines changed

2 files changed

+124
-18
lines changed

src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,23 @@ public function create(string $resourceClass, string $property, array $options =
5757
$denormalizationGroups = [$denormalizationGroups];
5858
}
5959

60+
[$normalizationAttributes, $denormalizationAttributes] = $this->getEffectiveSerializerAttributes($options);
61+
62+
if ($normalizationAttributes && !\is_array($normalizationAttributes)) {
63+
$normalizationAttributes = [$normalizationAttributes];
64+
}
65+
66+
if ($denormalizationAttributes && !\is_array($denormalizationAttributes)) {
67+
$denormalizationAttributes = [$denormalizationAttributes];
68+
}
69+
6070
$ignoredAttributes = $options['ignored_attributes'] ?? [];
6171
} catch (ResourceClassNotFoundException) {
6272
// TODO: for input/output classes, the serializer groups must be read from the actual resource class
6373
return $propertyMetadata;
6474
}
6575

66-
$propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups, $ignoredAttributes);
76+
$propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups, $normalizationAttributes, $denormalizationAttributes, $ignoredAttributes);
6777

6878
// TODO: remove in 5.x
6979
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
@@ -77,26 +87,26 @@ public function create(string $resourceClass, string $property, array $options =
7787
}
7888
}
7989

80-
return $this->transformLinkStatusLegacy($propertyMetadata, $normalizationGroups, $denormalizationGroups, $types);
90+
return $this->transformLinkStatusLegacy($propertyMetadata, $property, $normalizationGroups, $denormalizationGroups, $normalizationAttributes, $denormalizationAttributes, $types);
8191
}
8292
$type = $propertyMetadata->getNativeType();
8393
if (null !== $type && !$this->isResourceClass($resourceClass) && $type->isSatisfiedBy(static fn (Type $t): bool => $t instanceof CollectionType)) {
8494
return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
8595
}
8696

87-
return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups, $type);
97+
return $this->transformLinkStatus($propertyMetadata, $property, $normalizationGroups, $denormalizationGroups, $normalizationAttributes, $denormalizationAttributes, $type);
8898
}
8999

90100
/**
91-
* Sets readable/writable based on matching normalization/denormalization groups and property's ignorance.
101+
* Sets readable/writable based on matching normalization/denormalization groups/attributes and property's ignorance.
92102
*
93103
* A false value is never reset as it could be unreadable/unwritable for other reasons.
94104
* If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable.
95105
*
96106
* @param string[]|null $normalizationGroups
97107
* @param string[]|null $denormalizationGroups
98108
*/
99-
private function transformReadWrite(ApiProperty $propertyMetadata, string $resourceClass, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, array $ignoredAttributes = []): ApiProperty
109+
private function transformReadWrite(ApiProperty $propertyMetadata, string $resourceClass, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $normalizationAttributes = null, ?array $denormalizationAttributes = null, array $ignoredAttributes = []): ApiProperty
100110
{
101111
if (\in_array($propertyName, $ignoredAttributes, true)) {
102112
return $propertyMetadata->withWritable(false)->withReadable(false);
@@ -107,26 +117,26 @@ private function transformReadWrite(ApiProperty $propertyMetadata, string $resou
107117
$ignored = $serializerAttributeMetadata?->isIgnored() ?? false;
108118

109119
if (false !== $propertyMetadata->isReadable()) {
110-
$propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)));
120+
$propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)) && (null === $normalizationAttributes || $this->isPropertyInAttributes($propertyName, $normalizationAttributes)));
111121
}
112122

113123
if (false !== $propertyMetadata->isWritable()) {
114-
$propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)));
124+
$propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)) && (null === $denormalizationAttributes || $this->isPropertyInAttributes($propertyName, $denormalizationAttributes)));
115125
}
116126

117127
return $propertyMetadata;
118128
}
119129

120130
/**
121-
* Sets readableLink/writableLink based on matching normalization/denormalization groups.
131+
* Sets readableLink/writableLink based on matching normalization/denormalization groups/attributes.
122132
*
123-
* If normalization/denormalization groups are not specified,
133+
* If normalization/denormalization groups/attributes are not specified,
124134
* set link status to false since embedding of resource must be explicitly enabled
125135
*
126136
* @param string[]|null $normalizationGroups
127137
* @param string[]|null $denormalizationGroups
128138
*/
129-
private function transformLinkStatusLegacy(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $types = null): ApiProperty
139+
private function transformLinkStatusLegacy(ApiProperty $propertyMetadata, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $normalizationAttributes = null, ?array $denormalizationAttributes = null, ?array $types = null): ApiProperty
130140
{
131141
// No need to check link status if property is not readable and not writable
132142
if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
@@ -157,11 +167,11 @@ private function transformLinkStatusLegacy(ApiProperty $propertyMetadata, ?array
157167
$relatedGroups = $this->getClassSerializerGroups($relatedClass);
158168

159169
if (null === $propertyMetadata->isReadableLink()) {
160-
$propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
170+
$propertyMetadata = $propertyMetadata->withReadableLink((null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups))) || (null !== $normalizationAttributes && $this->isPropertyInAttributes($propertyName, $normalizationAttributes)));
161171
}
162172

163173
if (null === $propertyMetadata->isWritableLink()) {
164-
$propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
174+
$propertyMetadata = $propertyMetadata->withWritableLink((null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups))) || (null !== $denormalizationAttributes && $this->isPropertyInAttributes($propertyName, $denormalizationAttributes)));
165175
}
166176

167177
return $propertyMetadata;
@@ -171,15 +181,15 @@ private function transformLinkStatusLegacy(ApiProperty $propertyMetadata, ?array
171181
}
172182

173183
/**
174-
* Sets readableLink/writableLink based on matching normalization/denormalization groups.
184+
* Sets readableLink/writableLink based on matching normalization/denormalization groups/attributes.
175185
*
176-
* If normalization/denormalization groups are not specified,
186+
* If normalization/denormalization groups/attributes are not specified,
177187
* set link status to false since embedding of resource must be explicitly enabled
178188
*
179189
* @param string[]|null $normalizationGroups
180190
* @param string[]|null $denormalizationGroups
181191
*/
182-
private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?Type $type = null): ApiProperty
192+
private function transformLinkStatus(ApiProperty $propertyMetadata, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $normalizationAttributes = null, ?array $denormalizationAttributes = null, ?Type $type = null): ApiProperty
183193
{
184194
// No need to check link status if property is not readable and not writable
185195
if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
@@ -207,11 +217,11 @@ private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $norm
207217
$relatedGroups = $this->getClassSerializerGroups($className);
208218

209219
if (null === $propertyMetadata->isReadableLink()) {
210-
$propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
220+
$propertyMetadata = $propertyMetadata->withReadableLink((null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups))) || (null !== $normalizationAttributes && $this->isPropertyInAttributes($propertyName, $normalizationAttributes)));
211221
}
212222

213223
if (null === $propertyMetadata->isWritableLink()) {
214-
$propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
224+
$propertyMetadata = $propertyMetadata->withWritableLink((null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups))) || (null !== $denormalizationAttributes && $this->isPropertyInAttributes($propertyName, $denormalizationAttributes)));
215225
}
216226

217227
return $propertyMetadata;
@@ -243,6 +253,32 @@ private function getEffectiveSerializerGroups(array $options): array
243253
return [null, null];
244254
}
245255

256+
/**
257+
* Gets the effective serializer attributes used in normalization/denormalization.
258+
*
259+
* Attributes are extracted in the following order:
260+
*
261+
* - From the "serializer_attributes" key of the $options array.
262+
* - From metadata of the given operation ("operation_name" key).
263+
* - From metadata of the current resource.
264+
*
265+
* @return (array|null)[]
266+
*/
267+
private function getEffectiveSerializerAttributes(array $options): array
268+
{
269+
if (isset($options['serializer_attributes'])) {
270+
$attributes = (array) $options['serializer_attributes'];
271+
272+
return [$attributes, $attributes];
273+
}
274+
275+
if (\array_key_exists('normalization_attributes', $options) && \array_key_exists('denormalization_attributes', $options)) {
276+
return [$options['normalization_attributes'] ?? null, $options['denormalization_attributes'] ?? null];
277+
}
278+
279+
return [null, null];
280+
}
281+
246282
private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface
247283
{
248284
$serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
@@ -272,4 +308,9 @@ private function getClassSerializerGroups(string $class): array
272308

273309
return array_unique(array_merge(...$groups));
274310
}
311+
312+
private function isPropertyInAttributes(string $propertyName, array $attributes): bool
313+
{
314+
return \in_array($propertyName, $attributes, true) || \array_key_exists($propertyName, $attributes);
315+
}
275316
}

src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static function groupsProvider(): array
4141
}
4242

4343
#[\PHPUnit\Framework\Attributes\DataProvider('groupsProvider')]
44-
public function testCreate($readGroups, $writeGroups): void
44+
public function testCreateWithGroups($readGroups, $writeGroups): void
4545
{
4646
$serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class);
4747
$dummySerializerClassMetadata = new SerializerClassMetadata(Dummy::class);
@@ -110,6 +110,71 @@ public function testCreate($readGroups, $writeGroups): void
110110
$this->assertFalse($actual[2]->isWritable());
111111
}
112112

113+
public static function attributesProvider(): array
114+
{
115+
return [
116+
[['foo', 'relatedDummy'], ['foo']],
117+
[['foo', 'relatedDummy' => ['name']], ['foo' => []]],
118+
];
119+
}
120+
121+
#[\PHPUnit\Framework\Attributes\DataProvider('attributesProvider')]
122+
public function testCreateWithAttributes($readAttributes, $writeAttributes): void
123+
{
124+
$serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class);
125+
$dummySerializerClassMetadata = new SerializerClassMetadata(Dummy::class);
126+
$dummySerializerClassMetadata->addAttributeMetadata(new SerializerAttributeMetadata('foo'));
127+
$dummySerializerClassMetadata->addAttributeMetadata(new SerializerAttributeMetadata('relatedDummy'));
128+
$dummySerializerClassMetadata->addAttributeMetadata(new SerializerAttributeMetadata('notIncluded'));
129+
$serializerClassMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummySerializerClassMetadata);
130+
$relatedDummySerializerClassMetadata = new SerializerClassMetadata(RelatedDummy::class);
131+
$serializerClassMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedDummySerializerClassMetadata);
132+
133+
$context = ['normalization_attributes' => $readAttributes, 'denormalization_attributes' => $writeAttributes];
134+
135+
$decoratedProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
136+
$fooPropertyMetadata = (new ApiProperty())
137+
->withNativeType(Type::nullable(Type::array()))
138+
->withReadable(true)
139+
->withWritable(true);
140+
$decoratedProphecy->create(Dummy::class, 'foo', $context)->willReturn($fooPropertyMetadata);
141+
$relatedDummyPropertyMetadata = (new ApiProperty())
142+
->withNativeType(Type::nullable(Type::object(RelatedDummy::class)))
143+
->withReadable(true)
144+
->withWritable(true);
145+
$decoratedProphecy->create(Dummy::class, 'relatedDummy', $context)->willReturn($relatedDummyPropertyMetadata);
146+
$notIncludedPropertyMetadata = (new ApiProperty())
147+
->withNativeType(Type::nullable(Type::string()))
148+
->withReadable(true)
149+
->withWritable(true);
150+
$decoratedProphecy->create(Dummy::class, 'notIncluded', $context)->willReturn($notIncludedPropertyMetadata);
151+
152+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
153+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
154+
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);
155+
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
156+
157+
$serializerPropertyMetadataFactory = new SerializerPropertyMetadataFactory($serializerClassMetadataFactoryProphecy->reveal(), $decoratedProphecy->reveal(), $resourceClassResolverProphecy->reveal());
158+
159+
$actual = [];
160+
$actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'foo', $context);
161+
$actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'relatedDummy', $context);
162+
$actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'notIncluded', $context);
163+
164+
$this->assertInstanceOf(ApiProperty::class, $actual[0]);
165+
$this->assertTrue($actual[0]->isReadable());
166+
$this->assertTrue($actual[0]->isWritable());
167+
168+
$this->assertInstanceOf(ApiProperty::class, $actual[1]);
169+
$this->assertTrue($actual[1]->isReadable());
170+
$this->assertFalse($actual[1]->isWritable());
171+
$this->assertTrue($actual[1]->isReadableLink());
172+
173+
$this->assertInstanceOf(ApiProperty::class, $actual[2]);
174+
$this->assertFalse($actual[2]->isReadable());
175+
$this->assertFalse($actual[2]->isWritable());
176+
}
177+
113178
public function testCreateWithIgnoredProperty(): void
114179
{
115180
$ignoredSerializerAttributeMetadata = new SerializerAttributeMetadata('ignored');

0 commit comments

Comments
 (0)