diff --git a/features/main/patch.feature b/features/main/patch.feature index eeddbc5a1e0..8724a0196fc 100644 --- a/features/main/patch.feature +++ b/features/main/patch.feature @@ -58,3 +58,25 @@ Feature: Sending PATCH requets } } """ + + Scenario: Patch a relation with uri variables that are not `id` + When I add "Content-Type" header equal to "application/merge-patch+json" + And I send a "PATCH" request to "/betas/1" with body: + """ + { + "alpha": "/alphas/2" + } + """ + Then the response should be in JSON + And the response status code should be 200 + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "/contexts/Beta", + "@id": "/betas/1", + "@type": "Beta", + "betaId": 1, + "alpha": "/alphas/2" + } + """ diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 3a1bdd0eaf1..6120e93266f 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -20,6 +20,7 @@ use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -512,12 +513,7 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert $collectionKeyType = $type->getCollectionKeyTypes()[0] ?? null; $collectionKeyBuiltinType = $collectionKeyType?->getBuiltinType(); - $childContext = $this->createChildContext(['resource_class' => $className] + $context, $attribute, $format); - unset($childContext['uri_variables']); - if ($this->resourceMetadataCollectionFactory) { - $childContext['operation'] = $this->resourceMetadataCollectionFactory->create($className)->getOperation(); - } - + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); $values = []; foreach ($value as $index => $obj) { if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) { @@ -637,8 +633,7 @@ protected function getAttributeValue(object $object, string $attribute, string $ } $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); - $childContext = $this->createChildContext($context, $attribute, $format); - unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); + $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format); return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); } @@ -653,12 +648,7 @@ protected function getAttributeValue(object $object, string $attribute, string $ } $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); - $childContext = $this->createChildContext($context, $attribute, $format); - $childContext['resource_class'] = $resourceClass; - if ($this->resourceMetadataCollectionFactory) { - $childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); - } - unset($childContext['iri'], $childContext['uri_variables']); + $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format); return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); } @@ -670,9 +660,9 @@ protected function getAttributeValue(object $object, string $attribute, string $ unset($context['resource_class']); unset($context['force_resource_class']); + // Anonymous resources if ($type && $type->getClassName()) { $childContext = $this->createChildContext($context, $attribute, $format); - unset($childContext['iri'], $childContext['uri_variables']); $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true; return $this->serializer->normalize($attributeValue, $format, $childContext); @@ -680,7 +670,6 @@ protected function getAttributeValue(object $object, string $attribute, string $ if ($type && 'array' === $type->getBuiltinType()) { $childContext = $this->createChildContext($context, $attribute, $format); - unset($childContext['iri'], $childContext['uri_variables']); return $this->serializer->normalize($attributeValue, $format, $childContext); } @@ -804,12 +793,7 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value && $this->resourceClassResolver->isResourceClass($className) ) { $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); - $childContext = $this->createChildContext($context, $attribute, $format); - $childContext['resource_class'] = $resourceClass; - unset($childContext['uri_variables']); - if ($this->resourceMetadataCollectionFactory) { - $childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); - } + $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format); return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext); } @@ -899,4 +883,29 @@ private function setValue(object $object, string $attributeName, mixed $value): // Properties not found are ignored } } + + private function createOperationContext(array $context, string $resourceClass = null): array + { + if (isset($context['operation']) && !isset($context['root_operation'])) { + $context['root_operation'] = $context['operation']; + $context['root_operation_name'] = $context['operation_name']; + } + + unset($context['iri'], $context['uri_variables']); + if (!$resourceClass) { + return $context; + } + + unset($context['operation'], $context['operation_name']); + $context['resource_class'] = $resourceClass; + if ($this->resourceMetadataCollectionFactory) { + try { + $context['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); + $context['operation_name'] = $context['operation']->getName(); + } catch (OperationNotFoundException) { + } + } + + return $context; + } } diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index c51f911e1d0..e8c719dab3a 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -86,11 +86,7 @@ private function updateObjectToPopulate(array $data, array &$context): void private function getContextUriVariables(array $data, $operation, array $context): array { - if (!isset($context['uri_variables'])) { - return ['id' => $data['id']]; - } - - $uriVariables = $context['uri_variables']; + $uriVariables = $context['uri_variables'] ?? $data; /** @var Link $uriVariable */ foreach ($operation->getUriVariables() as $uriVariable) { diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5736/Alpha.php b/tests/Fixtures/TestBundle/ApiResource/Issue5736/Alpha.php new file mode 100644 index 00000000000..195b8085a07 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5736/Alpha.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5736; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; + +#[Get( + provider: [Alpha::class, 'provide'], +)] +final class Alpha +{ + public function __construct(#[ApiProperty(identifier: true)] public int $alphaId) + { + } + + public static function provide(Operation $operation, array $uriVariables = []): self + { + return new self(alphaId: $uriVariables['alphaId']); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5736/Beta.php b/tests/Fixtures/TestBundle/ApiResource/Issue5736/Beta.php new file mode 100644 index 00000000000..1644756d192 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5736/Beta.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5736; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Patch; + +#[Patch( + processor: [Beta::class, 'process'], + provider: [Beta::class, 'provide'], +)] +final class Beta +{ + public function __construct(#[ApiProperty(identifier: true)] public int $betaId, public ?Alpha $alpha = null) + { + } + + public static function provide(Operation $operation, array $uriVariables = []): self + { + return new self(betaId: $uriVariables['betaId']); + } + + public static function process($body) + { + return $body; + } +}