1818use ApiPlatform \Metadata \Exception \ItemNotFoundException ;
1919use ApiPlatform \Metadata \HttpOperation ;
2020use ApiPlatform \Metadata \IriConverterInterface ;
21+ use ApiPlatform \Metadata \Operation \Factory \OperationMetadataFactoryInterface ;
2122use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
2223use ApiPlatform \Metadata \Property \Factory \PropertyNameCollectionFactoryInterface ;
2324use ApiPlatform \Metadata \Resource \Factory \ResourceMetadataCollectionFactoryInterface ;
2728use ApiPlatform \Metadata \Util \ClassInfoTrait ;
2829use ApiPlatform \Serializer \AbstractItemNormalizer ;
2930use ApiPlatform \Serializer \ContextTrait ;
31+ use ApiPlatform \Serializer \OperationResourceClassResolverInterface ;
3032use ApiPlatform \Serializer \TagCollectorInterface ;
3133use Symfony \Component \PropertyAccess \PropertyAccessorInterface ;
3234use Symfony \Component \Serializer \Exception \LogicException ;
@@ -70,9 +72,9 @@ final class ItemNormalizer extends AbstractItemNormalizer
7072 '@vocab ' ,
7173 ];
7274
73- public function __construct (ResourceMetadataCollectionFactoryInterface $ resourceMetadataCollectionFactory , PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory , IriConverterInterface $ iriConverter , ResourceClassResolverInterface $ resourceClassResolver , private readonly ContextBuilderInterface $ contextBuilder , ?PropertyAccessorInterface $ propertyAccessor = null , ?NameConverterInterface $ nameConverter = null , ?ClassMetadataFactoryInterface $ classMetadataFactory = null , array $ defaultContext = [], ?ResourceAccessCheckerInterface $ resourceAccessChecker = null , protected ?TagCollectorInterface $ tagCollector = null )
75+ public function __construct (ResourceMetadataCollectionFactoryInterface $ resourceMetadataCollectionFactory , PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory , IriConverterInterface $ iriConverter , ResourceClassResolverInterface $ resourceClassResolver , private readonly ContextBuilderInterface $ contextBuilder , ?PropertyAccessorInterface $ propertyAccessor = null , ?NameConverterInterface $ nameConverter = null , ?ClassMetadataFactoryInterface $ classMetadataFactory = null , array $ defaultContext = [], ?ResourceAccessCheckerInterface $ resourceAccessChecker = null , protected ?TagCollectorInterface $ tagCollector = null , private ? OperationMetadataFactoryInterface $ operationMetadataFactory = null , ? OperationResourceClassResolverInterface $ operationResourceResolver = null )
7476 {
75- parent ::__construct ($ propertyNameCollectionFactory , $ propertyMetadataFactory , $ iriConverter , $ resourceClassResolver , $ propertyAccessor , $ nameConverter , $ classMetadataFactory , $ defaultContext , $ resourceMetadataCollectionFactory , $ resourceAccessChecker , $ tagCollector );
77+ parent ::__construct ($ propertyNameCollectionFactory , $ propertyMetadataFactory , $ iriConverter , $ resourceClassResolver , $ propertyAccessor , $ nameConverter , $ classMetadataFactory , $ defaultContext , $ resourceMetadataCollectionFactory , $ resourceAccessChecker , $ tagCollector, $ operationResourceResolver );
7678 }
7779
7880 /**
@@ -84,9 +86,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array
8486 }
8587
8688 /**
87- * @param string|null $format
89+ * {@inheritdoc}
8890 */
89- public function getSupportedTypes ($ format ): array
91+ public function getSupportedTypes (? string $ format ): array
9092 {
9193 return self ::FORMAT === $ format ? parent ::getSupportedTypes ($ format ) : [];
9294 }
@@ -96,20 +98,39 @@ public function getSupportedTypes($format): array
9698 *
9799 * @throws LogicException
98100 */
99- public function normalize (mixed $ object , ?string $ format = null , array $ context = []): array |string |int |float |bool |\ArrayObject |null
101+ public function normalize (mixed $ data , ?string $ format = null , array $ context = []): array |string |int |float |bool |\ArrayObject |null
100102 {
101- $ resourceClass = $ this ->getObjectClass ($ object );
103+ $ resourceClass = $ this ->getObjectClass ($ data );
102104 $ outputClass = $ this ->getOutputClass ($ context );
103105
104- if ($ outputClass && !($ context ['item_uri_template ' ] ?? null )) {
105- return parent ::normalize ($ object , $ format , $ context );
106+ if ($ outputClass ) {
107+ if ($ context ['item_uri_template ' ] ?? null ) {
108+ // When both output and item_uri_template are present, temporarily remove
109+ // item_uri_template so the output re-dispatch produces the correct @type
110+ // from the output class (not from the item_uri_template operation).
111+ $ itemUriTemplate = $ context ['item_uri_template ' ];
112+ unset($ context ['item_uri_template ' ]);
113+ $ originalData = $ data ;
114+ $ data = parent ::normalize ($ data , $ format , $ context );
115+ if (\is_array ($ data )) {
116+ try {
117+ $ context ['item_uri_template ' ] = $ itemUriTemplate ;
118+ $ data ['@id ' ] = $ this ->iriConverter ->getIriFromResource ($ originalData , UrlGeneratorInterface::ABS_PATH , null , $ context );
119+ } catch (\Exception ) {
120+ }
121+ }
122+
123+ return $ data ;
124+ }
125+
126+ return parent ::normalize ($ data , $ format , $ context );
106127 }
107128
108129 // TODO: we should not remove the resource_class in the normalizeRawCollection as we would find out anyway that it's not the same as the requested one
109130 $ previousResourceClass = $ context ['resource_class ' ] ?? null ;
110131 $ metadata = [];
111132 if ($ isResourceClass = $ this ->resourceClassResolver ->isResourceClass ($ resourceClass ) && (null === $ previousResourceClass || $ this ->resourceClassResolver ->isResourceClass ($ previousResourceClass ))) {
112- $ resourceClass = $ this ->resourceClassResolver ->getResourceClass ($ object , $ previousResourceClass );
133+ $ resourceClass = $ this ->resourceClassResolver ->getResourceClass ($ data , $ previousResourceClass );
113134 $ context = $ this ->initContext ($ resourceClass , $ context );
114135 $ metadata = $ this ->addJsonLdContext ($ this ->contextBuilder , $ resourceClass , $ context );
115136 } elseif ($ this ->contextBuilder instanceof AnonymousContextBuilderInterface) {
@@ -119,45 +140,68 @@ public function normalize(mixed $object, ?string $format = null, array $context
119140 $ context ['output ' ]['iri ' ] = null ;
120141 }
121142
122- if ($ this ->resourceClassResolver ->isResourceClass ($ resourceClass )) {
143+ if (isset ($ context ['item_uri_template ' ]) && $ this ->operationMetadataFactory ) {
144+ $ itemOp = $ this ->operationMetadataFactory ->create ($ context ['item_uri_template ' ]);
145+ // Use resource-level shortName for @type, not operation-specific shortName
146+ try {
147+ $ itemResourceShortName = $ this ->resourceMetadataCollectionFactory ->create ($ itemOp ->getClass ())[0 ]->getShortName ();
148+ $ context ['output ' ]['operation ' ] = $ itemOp ->withShortName ($ itemResourceShortName );
149+ } catch (\Exception ) {
150+ $ context ['output ' ]['operation ' ] = $ itemOp ;
151+ }
152+ } elseif ($ this ->resourceClassResolver ->isResourceClass ($ resourceClass )) {
123153 $ context ['output ' ]['operation ' ] = $ this ->resourceMetadataCollectionFactory ->create ($ resourceClass )->getOperation ();
124154 }
125155
126156 // We should improve what's behind the context creation, its probably more complicated then it should
127- $ metadata = $ this ->createJsonLdContext ($ this ->contextBuilder , $ object , $ context );
157+ $ metadata = $ this ->createJsonLdContext ($ this ->contextBuilder , $ data , $ context );
128158 }
129159
130160 // Special case: non-resource got serialized and contains a resource therefore we need to reset part of the context
131161 if ($ previousResourceClass !== $ resourceClass && $ resourceClass !== $ outputClass ) {
132162 unset($ context ['operation ' ], $ context ['operation_name ' ], $ context ['output ' ]);
133163 }
134164
135- if (true === ($ context ['output ' ]['gen_id ' ] ?? true ) && true === ($ context ['force_iri_generation ' ] ?? true ) && $ iri = $ this ->iriConverter ->getIriFromResource ($ object , UrlGeneratorInterface::ABS_PATH , $ context ['operation ' ] ?? null , $ context )) {
165+ if (true === ($ context ['output ' ]['gen_id ' ] ?? true ) && true === ($ context ['force_iri_generation ' ] ?? true ) && $ iri = $ this ->iriConverter ->getIriFromResource ($ data , UrlGeneratorInterface::ABS_PATH , $ context ['operation ' ] ?? null , $ context )) {
136166 $ context ['iri ' ] = $ iri ;
137167 $ metadata ['@id ' ] = $ iri ;
138168 }
139169
140170 $ context ['api_normalize ' ] = true ;
141171
142- $ data = parent ::normalize ($ object , $ format , $ context );
143- if (!\is_array ($ data )) {
144- return $ data ;
172+ $ normalizedData = parent ::normalize ($ data , $ format , $ context );
173+ if (!\is_array ($ normalizedData )) {
174+ return $ normalizedData ;
145175 }
146176
147177 $ operation = $ context ['operation ' ] ?? null ;
178+
179+ if ($ this ->operationMetadataFactory && isset ($ context ['item_uri_template ' ]) && !$ operation ) {
180+ $ operation = $ this ->operationMetadataFactory ->create ($ context ['item_uri_template ' ]);
181+ }
182+
148183 if ($ isResourceClass && !$ operation ) {
149184 $ operation = $ this ->resourceMetadataCollectionFactory ->create ($ resourceClass )->getOperation ();
150185 }
151186
152187 if (!isset ($ metadata ['@type ' ]) && $ operation ) {
153188 $ types = $ operation instanceof HttpOperation ? $ operation ->getTypes () : null ;
154189 if (null === $ types ) {
155- $ types = [$ operation ->getShortName ()];
190+ // TODO: 5.x break on this as this looks wrong, CollectionReferencingItem returns an IRI that point through
191+ // ItemReferencedInCollection but it returns a CollectionReferencingItem therefore we should use the current
192+ // object's class Type and not rely on operation ?
193+ // Use resource-level shortName to avoid operation-specific overrides
194+ $ typeClass = $ isResourceClass ? $ resourceClass : ($ operation ->getClass () ?? $ resourceClass );
195+ try {
196+ $ types = [$ this ->resourceMetadataCollectionFactory ->create ($ typeClass )[0 ]->getShortName ()];
197+ } catch (\Exception ) {
198+ $ types = [$ operation ->getShortName ()];
199+ }
156200 }
157201 $ metadata ['@type ' ] = 1 === \count ($ types ) ? $ types [0 ] : $ types ;
158202 }
159203
160- return $ metadata + $ data ;
204+ return $ metadata + $ normalizedData ;
161205 }
162206
163207 /**
@@ -173,7 +217,7 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form
173217 *
174218 * @throws NotNormalizableValueException
175219 */
176- public function denormalize (mixed $ data , string $ class , ?string $ format = null , array $ context = []): mixed
220+ public function denormalize (mixed $ data , string $ type , ?string $ format = null , array $ context = []): mixed
177221 {
178222 // Avoid issues with proxies if we populated the object
179223 if (isset ($ data ['@id ' ]) && !isset ($ context [self ::OBJECT_TO_POPULATE ])) {
@@ -192,7 +236,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null,
192236 }
193237 }
194238
195- return parent ::denormalize ($ data , $ class , $ format , $ context );
239+ return parent ::denormalize ($ data , $ type , $ format , $ context );
196240 }
197241
198242 protected function getAllowedAttributes (string |object $ classOrObject , array $ context , bool $ attributesAsString = false ): array |bool
0 commit comments