diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 4b3084db27..08a52b3a7d 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -28,6 +29,7 @@ use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ContextTrait; use ApiPlatform\Serializer\TagCollectorInterface; +use ApiPlatform\State\ProviderInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; @@ -59,9 +61,25 @@ final class ItemNormalizer extends AbstractItemNormalizer private array $componentsCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct( + PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + PropertyMetadataFactoryInterface $propertyMetadataFactory, + IriConverterInterface $iriConverter, + ResourceClassResolverInterface $resourceClassResolver, + ?PropertyAccessorInterface $propertyAccessor = null, + ?NameConverterInterface $nameConverter = null, + ?ClassMetadataFactoryInterface $classMetadataFactory = null, + array $defaultContext = [], + ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, + ?ResourceAccessCheckerInterface $resourceAccessChecker = null, + protected ?TagCollectorInterface $tagCollector = null, + private readonly IdentifiersExtractorInterface $identifiersExtractor, + private readonly ProviderInterface $provider, + bool $useIriAsId = true + ) { parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); + $this->useIriAsId = $useIriAsId; } /** @@ -125,8 +143,14 @@ public function normalize(mixed $object, ?string $format = null, array $context $includedResourcesData = $this->getRelatedResources($object, $format, $context, $allRelationshipsData); + $id = $context['iri']; + if (!$this->useIriAsId) { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($object); + $id = (string) array_values($identifiers)[0]; + } + $resourceData = [ - 'id' => $context['iri'], + 'id' => $id, 'type' => $this->getResourceShortName($resourceClass), ]; @@ -174,10 +198,21 @@ public function denormalize(mixed $data, string $class, ?string $format = null, throw new NotNormalizableValueException('Update is not allowed for this operation.'); } - $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri( - $data['data']['id'], - $context + ['fetch_data' => false] - ); + $context += ['fetch_data' => false]; + if ($this->useIriAsId) { + $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri( + $data['data']['id'], + $context + ); + } else { + $operation = $context['operation'] ?? $context['api_platform_operation'] ?? null; + $uriVariables = $context['uriVariables'] ?? []; + $context[self::OBJECT_TO_POPULATE] = $this->provider->provide( + $operation, + $uriVariables, + $context + ); + } } // Merge attributes and relationships, into format expected by the parent normalizer @@ -225,7 +260,41 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope } try { - return $this->iriConverter->getResourceFromIri($value['id'], $context + ['fetch_data' => true]); + $context += ['fetch_data' => true]; + if ($this->useIriAsId) { + return $this->iriConverter->getResourceFromIri($value['id'], $context); + } else { + $targetClass = $propertyMetadata->getBuiltinTypes()[0]->getClassName(); + $resourceMetadata = $this->resourceMetadataCollectionFactory + ->create($targetClass); + + $getOperation = null; + foreach ($resourceMetadata as $resource) { + foreach ($resource->getOperations() as $operation) { + if ($operation instanceof Get) { + $getOperation = $operation; + break 2; + } + } + } + if (null === $getOperation) { + throw new ItemNotFoundException(sprintf( + 'No GET operation found for resource "%s".', + $targetClass + )); + } + + $uriVariablesDefinition = $getOperation->getUriVariables(); + + $uriVariables = []; + foreach ($uriVariablesDefinition as $varName => $link) { + foreach ($link->getIdentifiers() as $identifier) { + $uriVariables[$identifier] = (string) $value['data']['id']; + } + } + return $this->provider->provide($getOperation, $uriVariables, $context); + } + } catch (ItemNotFoundException $e) { if (!isset($context['not_normalizable_value_exceptions'])) { throw new RuntimeException($e->getMessage(), $e->getCode(), $e); @@ -273,10 +342,15 @@ protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $rel return $normalizedRelatedObject; } + $id = $iri; + if (!$this->useIriAsId) { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($relatedObject); + $id = (string) array_values($identifiers)[0]; + } $context['data'] = [ 'data' => [ + 'id' => $id, 'type' => $this->getResourceShortName($resourceClass), - 'id' => $iri, ], ]; diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 2d23932693..67058ced40 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -159,7 +159,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerOAuthConfiguration($container, $config); $this->registerOpenApiConfiguration($container, $config, $loader); $this->registerSwaggerConfiguration($container, $config, $loader); - $this->registerJsonApiConfiguration($formats, $loader, $config); + $this->registerJsonApiConfiguration($container, $formats, $loader, $config); $this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config); $this->registerJsonHalConfiguration($formats, $loader); $this->registerJsonProblemConfiguration($errorFormats, $loader); @@ -630,7 +630,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.swagger_ui.extra_configuration', $config['openapi']['swagger_ui_extra_configuration'] ?: $config['swagger']['swagger_ui_extra_configuration']); } - private function registerJsonApiConfiguration(array $formats, PhpFileLoader $loader, array $config): void + private function registerJsonApiConfiguration(ContainerBuilder $container, array $formats, PhpFileLoader $loader, array $config): void { if (!isset($formats['jsonapi'])) { return; @@ -642,6 +642,9 @@ private function registerJsonApiConfiguration(array $formats, PhpFileLoader $loa $loader->load('jsonapi.php'); $loader->load('state/jsonapi.php'); + + $container->getDefinition('api_platform.jsonapi.normalizer.item') + ->addArgument($config['jsonapi']['use_iri_as_id']); } private function registerJsonLdHydraConfiguration(ContainerBuilder $container, array $formats, PhpFileLoader $loader, array $config): void diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 354b7b1f2d..bbea0cf5c1 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -99,6 +99,16 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('jsonapi') + ->setDeprecated('api-platform/core', '5.0') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('use_iri_as_id') + ->setDeprecated('api-platform/core', '5.0', 'Using IRI in JSON:API output will be no longer supported and replaced by "type" + "id".') + ->defaultTrue() + ->end() + ->end() + ->end() ->arrayNode('eager_loading') ->canBeDisabled() ->addDefaultsIfNotSet() diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.php b/src/Symfony/Bundle/Resources/config/jsonapi.php index 4e760e4963..8605b662b8 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.php +++ b/src/Symfony/Bundle/Resources/config/jsonapi.php @@ -62,6 +62,8 @@ service('api_platform.metadata.resource.metadata_collection_factory'), service('api_platform.security.resource_access_checker')->ignoreOnInvalid(), service('api_platform.http_cache.tag_collector')->ignoreOnInvalid(), + service('api_platform.api.identifiers_extractor'), + service('api_platform.state_provider'), ]) ->tag('serializer.normalizer', ['priority' => -890]); diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 62c8cef82d..35b53f486b 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -235,6 +235,9 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'hydra_prefix' => null, ], 'enable_phpdoc_parser' => true, + 'jsonapi' => [ + 'use_iri_as_id' => true + ], ], $config); }