From 36e24a94baeab79edba7b0a9307144bfa81b9d97 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 11:34:45 +0200 Subject: [PATCH] fix(doctrine): resolve parent link toProperty during PUT create The PUT create-via-allowCreate branch of PersistProcessor ignored Link::toProperty and Link::fromClass and wrote the raw scalar URI identifier onto the current entity, overwriting its own id and leaving the parent relation null. Only the PUT-with-previous-data branch handled this correctly. When a uri variable Link declares a toProperty + fromClass, resolve a managed reference via ManagerRegistry::getManagerForClass and assign it to the target property instead of writing the scalar id. Composite identifier links pass an associative array to getReference. Falls back to the legacy scalar write when no parent manager is found or the property is missing, so existing self-link paths remain unchanged. Fixes #7819 --- .../Common/State/PersistProcessor.php | 27 +++++++- .../Tests/State/PersistProcessorTest.php | 63 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/Doctrine/Common/State/PersistProcessor.php b/src/Doctrine/Common/State/PersistProcessor.php index 2afd1f368ab..85b331ddb0c 100644 --- a/src/Doctrine/Common/State/PersistProcessor.php +++ b/src/Doctrine/Common/State/PersistProcessor.php @@ -81,16 +81,37 @@ public function process(mixed $data, Operation $operation, array $uriVariables = // We create a new entity through PUT } else { foreach (array_reverse($links) as $link) { - if ($link->getExpandedValue() || !$link->getFromClass()) { + if ($link->getExpandedValue() || !($fromClass = $link->getFromClass())) { continue; } $identifierProperties = $link->getIdentifiers(); $hasCompositeIdentifiers = 1 < \count($identifierProperties); + $toProperty = $link->getToProperty(); + $parentManager = $toProperty ? $this->managerRegistry->getManagerForClass($fromClass) : null; + + if ($toProperty && $parentManager && isset($reflectionProperties[$toProperty]) && method_exists($parentManager, 'getReference')) { + if ($hasCompositeIdentifiers) { + $referenceIdentifier = []; + foreach ($identifierProperties as $identifierProperty) { + $referenceIdentifier[$identifierProperty] = $this->getIdentifierValue($identifiers, $identifierProperty); + } + } else { + $referenceIdentifier = $this->getIdentifierValue($identifiers); + } + + $reflectionProperties[$toProperty]->setValue($newData, $parentManager->getReference($fromClass, $referenceIdentifier)); + + continue; + } foreach ($identifierProperties as $identifierProperty) { - $reflectionProperty = $reflectionProperties[$identifierProperty]; - $reflectionProperty->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null)); + if (!isset($reflectionProperties[$identifierProperty])) { + $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null); + continue; + } + + $reflectionProperties[$identifierProperty]->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null)); } } diff --git a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php index 66aa87a695b..59da208971e 100644 --- a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php +++ b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php @@ -17,9 +17,12 @@ use ApiPlatform\Doctrine\Common\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Doctrine\Common\Tests\Fixtures\TestBundle\Entity\DummyWithUninitializedProperties; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use ApiPlatform\State\ProcessorInterface; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; @@ -141,4 +144,64 @@ public function testHandleLazyObjectRelationsSkipsUninitializedProperties(): voi $result = (new PersistProcessor($managerRegistryProphecy->reveal()))->process($dummy, new Post(map: true)); $this->assertSame($dummy, $result); } + + public function testPersistPutCreateResolvesParentLinkViaToProperty(): void + { + $device = new PersistProcessorTestDeviceStub(); + + $userReference = new PersistProcessorTestUserStub(); + $userReference->id = 'user-uuid'; + + $deviceClassMetadata = new ORMClassMetadata(PersistProcessorTestDeviceStub::class); + $deviceClassMetadata->identifier = ['id']; + + $deviceManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $deviceManagerProphecy->getClassMetadata(PersistProcessorTestDeviceStub::class)->willReturn($deviceClassMetadata); + $deviceManagerProphecy->contains($device)->willReturn(false); + $deviceManagerProphecy->persist($device)->shouldBeCalled(); + $deviceManagerProphecy->flush()->shouldBeCalled(); + $deviceManagerProphecy->refresh($device)->shouldBeCalled(); + + $userManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $userManagerProphecy->getReference(PersistProcessorTestUserStub::class, 'user-uuid') + ->willReturn($userReference) + ->shouldBeCalledTimes(1); + $userManagerProphecy->contains($userReference)->willReturn(true); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(PersistProcessorTestDeviceStub::class)->willReturn($deviceManagerProphecy->reveal()); + $managerRegistryProphecy->getManagerForClass(PersistProcessorTestUserStub::class)->willReturn($userManagerProphecy->reveal()); + + $operation = new Put( + extraProperties: ['standard_put' => true], + uriVariables: [ + 'userId' => new Link(toProperty: 'user', fromClass: PersistProcessorTestUserStub::class, identifiers: ['id']), + 'id' => new Link(fromClass: PersistProcessorTestDeviceStub::class, identifiers: ['id']), + ], + ); + + $result = (new PersistProcessor($managerRegistryProphecy->reveal()))->process( + $device, + $operation, + ['userId' => 'user-uuid', 'id' => 'device-uuid'], + ['previous_data' => null], + ); + + $this->assertSame($device, $result); + $this->assertSame($userReference, $device->user); + $this->assertSame('device-uuid', $device->id); + } +} + +/** @internal */ +class PersistProcessorTestUserStub +{ + public ?string $id = null; +} + +/** @internal */ +class PersistProcessorTestDeviceStub +{ + public ?string $id = null; + public ?PersistProcessorTestUserStub $user = null; }