diff --git a/features/main/crud.feature b/features/main/crud.feature index 1fc310d63bb..bda482e6505 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -771,3 +771,29 @@ Feature: Create-Retrieve-Update-Delete "hydra:totalItems": 3 } """ + + @!mongodb + @createSchema + Scenario: Replace an existing resource that doesn't expose its internal identifier + Given there is an HiddenIdentifierDummy object + When I add "Content-Type" header equal to "application/json" + And I send a "PUT" request to "/hidden_identifier_dummies/prettyid" with body: + """ + { + "foo": "A nice dummy" + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should be equal to "/hidden_identifier_dummies/prettyid" + And the JSON should be equal to: + """ + { + "@context": "/contexts/HiddenIdentifierDummy", + "@id": "/hidden_identifier_dummies/prettyid", + "@type": "HiddenIdentifierDummy", + "visibleId":"prettyid", + "foo": "A nice dummy" + } + """ \ No newline at end of file diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index f93c31e03f6..6cb2e411b19 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -53,6 +53,14 @@ public function getIdentifiersFromItem(object $item, Operation $operation = null return ['id' => $this->propertyAccessor->getValue($item, 'id')]; } + if ($operation && ($ormIds = $operation->getExtraProperties()['ormIds'] ?? null) && \array_key_exists('orm', $context)) { + return array_reduce($ormIds, function ($carry, $id) use ($item) { + $carry[$id] = $this->propertyAccessor->getValue($item, $id); + + return $carry; + }); + } + if ($operation && $operation->getClass()) { return $this->getIdentifiersFromOperation($item, $operation, $context); } diff --git a/src/Doctrine/Common/State/PersistProcessor.php b/src/Doctrine/Common/State/PersistProcessor.php index cf3f0b5a75e..2b8744b6973 100644 --- a/src/Doctrine/Common/State/PersistProcessor.php +++ b/src/Doctrine/Common/State/PersistProcessor.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Doctrine\Common\State; +use ApiPlatform\Api\IdentifiersExtractorInterface; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Util\ClassInfoTrait; @@ -27,7 +28,8 @@ final class PersistProcessor implements ProcessorInterface use ClassInfoTrait; use LinksHandlerTrait; - public function __construct(private readonly ManagerRegistry $managerRegistry) + public function __construct(private readonly ManagerRegistry $managerRegistry, + private readonly ?IdentifiersExtractorInterface $identifiersExtractor = null) { } @@ -55,7 +57,12 @@ public function process(mixed $data, Operation $operation, array $uriVariables = // TODO: the call to getReference is most likely to fail with complex identifiers $newData = $data; if (isset($context['previous_data'])) { - $newData = 1 === \count($uriVariables) ? $manager->getReference($class, current($uriVariables)) : clone $context['previous_data']; + if ($ormId = $operation->getExtraProperties()['ormIds'] ?? null) { + $id = $this->identifiersExtractor->getIdentifiersFromItem($context['previous_data'], $operation, ['orm' => true]); + } else { + $id = current($uriVariables); + } + $newData = 1 === \count($uriVariables) ? $manager->getReference($class, $id) : clone $context['previous_data']; } $identifiers = array_reverse($uriVariables); @@ -79,7 +86,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = } else { foreach ($reflectionProperties as $propertyName => $reflectionProperty) { // Don't override the property if it's part of the subresource system - if (isset($uriVariables[$propertyName])) { + if (isset($uriVariables[$propertyName]) || + \in_array($propertyName, $operation->getExtraProperties()['ormIds'] ?? [], true)) { continue; } diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index 112c4f6d252..f2a7edf4061 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -19,6 +19,7 @@ + diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index dd510039fd8..96a8c81c329 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -139,6 +139,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Greeting; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\HiddenIdentifierDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InitializeInput; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InternalUser; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\IriOnlyDummy; @@ -2147,6 +2148,19 @@ public function thereIsAResourceUsingEntityClassAndDateTime(): void $this->manager->flush(); } + /** + * @Given there is an HiddenIdentifierDummy object + */ + public function thereIsAnHiddenIdentifierDummy(): void + { + $dummy = new HiddenIdentifierDummy(); + $dummy->id = 1; + $dummy->visibleId = 'prettyid'; + $dummy->foo = 'fooValue'; + $this->manager->persist($dummy); + $this->manager->flush(); + } + private function isOrm(): bool { return null !== $this->schemaTool; diff --git a/tests/Fixtures/TestBundle/Entity/HiddenIdentifierDummy.php b/tests/Fixtures/TestBundle/Entity/HiddenIdentifierDummy.php new file mode 100644 index 00000000000..80d0388ddaf --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/HiddenIdentifierDummy.php @@ -0,0 +1,53 @@ + + * + * 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\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Put; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ApiResource( + operations : [ + new Get(), + new Put(allowCreate: true), + ], + normalizationContext : ['groups' => ['HiddenIdentifierDummy::out']], + denormalizationContext: ['groups' => ['HiddenIdentifierDummy::in']], + extraProperties : [ + 'standard_put' => true, + 'ormIds' => ['id'], + ], +)] +#[Entity] +class HiddenIdentifierDummy +{ + #[Id] + #[Column] + #[ApiProperty(identifier: false)] + public ?int $id = null; + + #[Column] + #[ApiProperty(identifier: true)] + #[Groups(['HiddenIdentifierDummy::out'])] + public ?string $visibleId = null; + + #[Column] + #[Groups(['HiddenIdentifierDummy::out', 'HiddenIdentifierDummy::in'])] + public string $foo = ''; +}