@@ -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}
0 commit comments