1313
1414namespace ApiPlatform \JsonApi \Serializer ;
1515
16+ use ApiPlatform \JsonApi \Util \ResourceLinkageResolver ;
1617use ApiPlatform \Metadata \ApiProperty ;
1718use ApiPlatform \Metadata \Exception \ItemNotFoundException ;
1819use ApiPlatform \Metadata \HttpOperation ;
2627use ApiPlatform \Metadata \UrlGeneratorInterface ;
2728use ApiPlatform \Metadata \Util \ClassInfoTrait ;
2829use ApiPlatform \Metadata \Util \CompositeIdentifierParser ;
29- use ApiPlatform \Metadata \Util \TypeHelper ;
3030use ApiPlatform \Serializer \AbstractItemNormalizer ;
3131use ApiPlatform \Serializer \ContextTrait ;
3232use ApiPlatform \Serializer \OperationResourceClassResolverInterface ;
3333use ApiPlatform \Serializer \TagCollectorInterface ;
3434use Symfony \Component \ErrorHandler \Exception \FlattenException ;
3535use Symfony \Component \PropertyAccess \PropertyAccessorInterface ;
36- use Symfony \Component \PropertyInfo \PropertyInfoExtractor ;
3736use Symfony \Component \Serializer \Exception \LogicException ;
3837use Symfony \Component \Serializer \Exception \NotNormalizableValueException ;
3938use Symfony \Component \Serializer \Exception \RuntimeException ;
4039use Symfony \Component \Serializer \Exception \UnexpectedValueException ;
4140use Symfony \Component \Serializer \Mapping \Factory \ClassMetadataFactoryInterface ;
4241use Symfony \Component \Serializer \NameConverter \NameConverterInterface ;
4342use Symfony \Component \Serializer \Normalizer \NormalizerInterface ;
44- use Symfony \Component \TypeInfo \Type ;
45- use Symfony \Component \TypeInfo \Type \CompositeTypeInterface ;
46- use Symfony \Component \TypeInfo \Type \ObjectType ;
4743
4844/**
4945 * Converts between objects and array.
@@ -69,6 +65,7 @@ final class ItemNormalizer extends AbstractItemNormalizer
6965
7066 private array $ componentsCache = [];
7167 private bool $ useIriAsId ;
68+ private readonly ResourceLinkageResolver $ resourceLinkageResolver ;
7269
7370 public function __construct (
7471 PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory ,
@@ -85,9 +82,11 @@ public function __construct(
8582 ?OperationResourceClassResolverInterface $ operationResourceResolver = null ,
8683 private readonly ?IdentifiersExtractorInterface $ identifiersExtractor = null ,
8784 bool $ useIriAsId = true ,
85+ ?ResourceLinkageResolver $ resourceLinkageResolver = null ,
8886 ) {
8987 parent ::__construct ($ propertyNameCollectionFactory , $ propertyMetadataFactory , $ iriConverter , $ resourceClassResolver , $ propertyAccessor , $ nameConverter , $ classMetadataFactory , $ defaultContext , $ resourceMetadataCollectionFactory , $ resourceAccessChecker , $ tagCollector , $ operationResourceResolver );
9088 $ this ->useIriAsId = $ useIriAsId ;
89+ $ this ->resourceLinkageResolver = $ resourceLinkageResolver ?? new ResourceLinkageResolver ($ resourceClassResolver );
9190 }
9291
9392 /**
@@ -409,105 +408,36 @@ private function getComponents(object $object, ?string $format, array $context):
409408 ->propertyMetadataFactory
410409 ->create ($ context ['resource_class ' ], $ attribute , $ options );
411410
412- // prevent declaring $attribute as attribute if it's already declared as relationship
413- $ isRelationship = false ;
411+ // Shared with the JSON Schema SchemaFactory so the documented split cannot drift from this output.
412+ $ relationships = $ this -> resourceLinkageResolver -> getRelationships ( $ propertyMetadata ) ;
414413
415- if (!method_exists (PropertyInfoExtractor::class, 'getType ' )) {
416- $ types = $ propertyMetadata ->getBuiltinTypes () ?? [];
417-
418- foreach ($ types as $ type ) {
419- $ isOne = $ isMany = false ;
420-
421- if ($ type ->isCollection ()) {
422- $ collectionValueType = $ type ->getCollectionValueTypes ()[0 ] ?? null ;
423- $ isMany = $ collectionValueType && ($ className = $ collectionValueType ->getClassName ()) && $ this ->resourceClassResolver ->isResourceClass ($ className );
424- } else {
425- $ isOne = ($ className = $ type ->getClassName ()) && $ this ->resourceClassResolver ->isResourceClass ($ className );
426- }
427-
428- if (!isset ($ className ) || !$ isOne && !$ isMany ) {
429- // don't declare it as an attribute too quick: maybe the next type is a valid resource
430- continue ;
431- }
414+ foreach ($ relationships as [$ className , $ isCollection ]) {
415+ $ relation = [
416+ 'name ' => $ attribute ,
417+ 'type ' => $ this ->getResourceShortName ($ className ),
418+ 'cardinality ' => $ isCollection ? 'many ' : 'one ' ,
419+ ];
432420
433- $ relation = [
434- 'name ' => $ attribute ,
435- 'type ' => $ this ->getResourceShortName ($ className ),
436- 'cardinality ' => $ isOne ? 'one ' : 'many ' ,
437- ];
438-
439- // if we specify the uriTemplate, generates its value for link definition
440- // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
441- if ($ itemUriTemplate = $ propertyMetadata ->getUriTemplate ()) {
442- $ attributeValue = $ this ->propertyAccessor ->getValue ($ object , $ attribute );
443- $ resourceClass = $ this ->resourceClassResolver ->getResourceClass ($ attributeValue , $ className );
444- $ childContext = $ this ->createChildContext ($ context , $ attribute , $ format );
445- unset($ childContext ['iri ' ], $ childContext ['uri_variables ' ], $ childContext ['resource_class ' ], $ childContext ['operation ' ]);
446-
447- $ operation = $ this ->resourceMetadataCollectionFactory ->create ($ resourceClass )->getOperation (
448- operationName: $ itemUriTemplate ,
449- httpOperation: true
450- );
451-
452- $ components ['links ' ][$ attribute ] = $ this ->iriConverter ->getIriFromResource ($ object , UrlGeneratorInterface::ABS_PATH , $ operation , $ childContext );
453- }
421+ // if we specify the uriTemplate, generates its value for link definition
422+ // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
423+ if ($ itemUriTemplate = $ propertyMetadata ->getUriTemplate ()) {
424+ $ attributeValue = $ this ->propertyAccessor ->getValue ($ object , $ attribute );
425+ $ resourceClass = $ this ->resourceClassResolver ->getResourceClass ($ attributeValue , $ className );
426+ $ childContext = $ this ->createChildContext ($ context , $ attribute , $ format );
427+ unset($ childContext ['iri ' ], $ childContext ['uri_variables ' ], $ childContext ['resource_class ' ], $ childContext ['operation ' ]);
428+
429+ $ operation = $ this ->resourceMetadataCollectionFactory ->create ($ resourceClass )->getOperation (
430+ operationName: $ itemUriTemplate ,
431+ httpOperation: true
432+ );
454433
455- $ components ['relationships ' ][] = $ relation ;
456- $ isRelationship = true ;
434+ $ components ['links ' ][$ attribute ] = $ this ->iriConverter ->getIriFromResource ($ object , UrlGeneratorInterface::ABS_PATH , $ operation , $ childContext );
457435 }
458- } else {
459- if ($ type = $ propertyMetadata ->getNativeType ()) {
460- /** @var class-string|null $className */
461- $ className = null ;
462-
463- $ typeIsResourceClass = function (Type $ type ) use (&$ className ): bool {
464- return $ type instanceof ObjectType && $ this ->resourceClassResolver ->isResourceClass ($ className = $ type ->getClassName ());
465- };
466436
467- foreach ($ type instanceof CompositeTypeInterface ? $ type ->getTypes () : [$ type ] as $ t ) {
468- $ isOne = $ isMany = false ;
469-
470- if (TypeHelper::getCollectionValueType ($ t )?->isSatisfiedBy($ typeIsResourceClass )) {
471- $ isMany = true ;
472- } elseif ($ t ->isSatisfiedBy ($ typeIsResourceClass )) {
473- $ isOne = true ;
474- }
475-
476- if (!$ className || (!$ isOne && !$ isMany )) {
477- // don't declare it as an attribute too quick: maybe the next type is a valid resource
478- continue ;
479- }
480-
481- $ relation = [
482- 'name ' => $ attribute ,
483- 'type ' => $ this ->getResourceShortName ($ className ),
484- 'cardinality ' => $ isOne ? 'one ' : 'many ' ,
485- ];
486-
487- // if we specify the uriTemplate, generates its value for link definition
488- // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
489- if ($ itemUriTemplate = $ propertyMetadata ->getUriTemplate ()) {
490- $ attributeValue = $ this ->propertyAccessor ->getValue ($ object , $ attribute );
491- $ resourceClass = $ this ->resourceClassResolver ->getResourceClass ($ attributeValue , $ className );
492- $ childContext = $ this ->createChildContext ($ context , $ attribute , $ format );
493- unset($ childContext ['iri ' ], $ childContext ['uri_variables ' ], $ childContext ['resource_class ' ], $ childContext ['operation ' ]);
494-
495- $ operation = $ this ->resourceMetadataCollectionFactory ->create ($ resourceClass )->getOperation (
496- operationName: $ itemUriTemplate ,
497- httpOperation: true
498- );
499-
500- $ components ['links ' ][$ attribute ] = $ this ->iriConverter ->getIriFromResource ($ object , UrlGeneratorInterface::ABS_PATH , $ operation , $ childContext );
501- }
502-
503- $ components ['relationships ' ][] = $ relation ;
504- $ isRelationship = true ;
505- }
506- }
437+ $ components ['relationships ' ][] = $ relation ;
507438 }
508439
509- // if all types are not relationships, declare it as an attribute
510- if (!$ isRelationship ) {
440+ if ([] === $ relationships ) {
511441 $ components ['attributes ' ][] = $ attribute ;
512442 }
513443 }
0 commit comments