Skip to content

Commit a9a0689

Browse files
authored
fix(doctrine): get reference with identifier value (#6019)
* fix(doctrine): get reference with identifier value * cs
1 parent 2e1dba9 commit a9a0689

File tree

3 files changed

+139
-28
lines changed

3 files changed

+139
-28
lines changed

features/main/standard_put.feature

+45
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,48 @@ Feature: Spec-compliant PUT support
4747
"bar": ""
4848
}
4949
"""
50+
51+
@createSchema
52+
@!mongodb
53+
Scenario: Create a new resource identified by an uid
54+
When I add "Content-Type" header equal to "application/ld+json"
55+
And I send a "PUT" request to "/uid_identifieds/fbcf5910-d915-4f7d-ba39-6b2957c57335" with body:
56+
"""
57+
{
58+
"name": "test"
59+
}
60+
"""
61+
Then the response status code should be 201
62+
And the response should be in JSON
63+
And the JSON should be equal to:
64+
"""
65+
{
66+
"@context": "/contexts/UidIdentified",
67+
"@id": "/uid_identifieds/fbcf5910-d915-4f7d-ba39-6b2957c57335",
68+
"@type": "UidIdentified",
69+
"id": "fbcf5910-d915-4f7d-ba39-6b2957c57335",
70+
"name": "test"
71+
}
72+
"""
73+
74+
@!mongodb
75+
Scenario: Replace an existing resource
76+
When I add "Content-Type" header equal to "application/ld+json"
77+
And I send a "PUT" request to "/uid_identifieds/fbcf5910-d915-4f7d-ba39-6b2957c57335" with body:
78+
"""
79+
{
80+
"name": "bar"
81+
}
82+
"""
83+
Then the response status code should be 200
84+
And the response should be in JSON
85+
And the JSON should be equal to:
86+
"""
87+
{
88+
"@context": "/contexts/UidIdentified",
89+
"@id": "/uid_identifieds/fbcf5910-d915-4f7d-ba39-6b2957c57335",
90+
"@type": "UidIdentified",
91+
"id": "fbcf5910-d915-4f7d-ba39-6b2957c57335",
92+
"name": "bar"
93+
}
94+
"""

src/Doctrine/Common/State/PersistProcessor.php

+26-28
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
use ApiPlatform\Metadata\Operation;
1818
use ApiPlatform\Metadata\Util\ClassInfoTrait;
1919
use ApiPlatform\State\ProcessorInterface;
20-
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
21-
use Doctrine\ORM\Mapping\ClassMetadataInfo;
2220
use Doctrine\Persistence\ManagerRegistry;
2321
use Doctrine\Persistence\ObjectManager as DoctrineObjectManager;
2422

@@ -50,48 +48,48 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
5048
// PUT: reset the existing object managed by Doctrine and merge data sent by the user in it
5149
// This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported:
5250
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
53-
if ($operation instanceof HttpOperation && HttpOperation::METHOD_PUT === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) {
51+
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) {
5452
\assert(method_exists($manager, 'getReference'));
55-
// TODO: the call to getReference is most likely to fail with complex identifiers
5653
$newData = $data;
57-
if (isset($context['previous_data'])) {
58-
$newData = 1 === \count($uriVariables) ? $manager->getReference($class, current($uriVariables)) : clone $context['previous_data'];
59-
}
60-
6154
$identifiers = array_reverse($uriVariables);
6255
$links = $this->getLinks($class, $operation, $context);
6356
$reflectionProperties = $this->getReflectionProperties($data);
6457

65-
if (!isset($context['previous_data'])) {
66-
foreach (array_reverse($links) as $link) {
67-
if ($link->getExpandedValue() || !$link->getFromClass()) {
58+
// TODO: the call to getReference is most likely to fail with complex identifiers
59+
if ($previousData = $context['previous_data']) {
60+
$classMetadata = $manager->getClassMetadata($class);
61+
$identifiers = $classMetadata->getIdentifierValues($previousData);
62+
$newData = 1 === \count($identifiers) ? $manager->getReference($class, current($identifiers)) : clone $previousData;
63+
64+
foreach ($reflectionProperties as $propertyName => $reflectionProperty) {
65+
// // Don't override the property if it's part of the subresource system
66+
if (isset($identifiers[$propertyName]) || isset($uriVariables[$propertyName])) {
6867
continue;
6968
}
7069

71-
$identifierProperties = $link->getIdentifiers();
72-
$hasCompositeIdentifiers = 1 < \count($identifierProperties);
70+
// Skip URI variables as sometime an uri variable is not the doctrine identifier
71+
foreach ($links as $link) {
72+
if (\in_array($propertyName, $link->getIdentifiers(), true)) {
73+
continue 2;
74+
}
75+
}
7376

74-
foreach ($identifierProperties as $identifierProperty) {
75-
$reflectionProperty = $reflectionProperties[$identifierProperty];
76-
$reflectionProperty->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null));
77+
if (($newValue = $reflectionProperty->getValue($data)) !== $reflectionProperty->getValue($newData)) {
78+
$reflectionProperty->setValue($newData, $newValue);
7779
}
7880
}
7981
} else {
80-
foreach ($reflectionProperties as $propertyName => $reflectionProperty) {
81-
// Don't override the property if it's part of the subresource system
82-
if (isset($uriVariables[$propertyName])) {
82+
foreach (array_reverse($links) as $link) {
83+
if ($link->getExpandedValue() || !$link->getFromClass()) {
8384
continue;
8485
}
8586

86-
foreach ($links as $link) {
87-
$identifierProperties = $link->getIdentifiers();
88-
if (\in_array($propertyName, $identifierProperties, true)) {
89-
continue;
90-
}
87+
$identifierProperties = $link->getIdentifiers();
88+
$hasCompositeIdentifiers = 1 < \count($identifierProperties);
9189

92-
if (($newValue = $reflectionProperty->getValue($data)) !== $reflectionProperty->getValue($newData)) {
93-
$reflectionProperty->setValue($newData, $newValue);
94-
}
90+
foreach ($identifierProperties as $identifierProperty) {
91+
$reflectionProperty = $reflectionProperties[$identifierProperty];
92+
$reflectionProperty->setValue($newData, $this->getIdentifierValue($identifiers, $hasCompositeIdentifiers ? $identifierProperty : null));
9593
}
9694
}
9795
}
@@ -115,7 +113,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
115113
private function isDeferredExplicit(DoctrineObjectManager $manager, $data): bool
116114
{
117115
$classMetadata = $manager->getClassMetadata($this->getObjectClass($data));
118-
if (($classMetadata instanceof ClassMetadataInfo || $classMetadata instanceof ClassMetadata) && method_exists($classMetadata, 'isChangeTrackingDeferredExplicit')) {
116+
if ($classMetadata && method_exists($classMetadata, 'isChangeTrackingDeferredExplicit')) { // @phpstan-ignore-line metadata can be null
119117
return $classMetadata->isChangeTrackingDeferredExplicit();
120118
}
121119

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\Put;
18+
use Doctrine\ORM\Mapping as ORM;
19+
use Symfony\Component\Serializer\Annotation\Ignore as LegacyIgnore;
20+
use Symfony\Component\Serializer\Annotation\SerializedName as LegacySerializedName;
21+
use Symfony\Component\Serializer\Attribute\Ignore;
22+
use Symfony\Component\Serializer\Attribute\SerializedName;
23+
use Symfony\Component\Uid\Uuid;
24+
use Symfony\Component\Validator\Constraints as Assert;
25+
26+
#[Put(
27+
allowCreate: true,
28+
extraProperties: [
29+
'standard_put' => true,
30+
]
31+
)]
32+
#[ORM\Entity]
33+
class UidIdentified
34+
{
35+
/**
36+
* The entity ID.
37+
*/
38+
#[ORM\Id]
39+
#[ORM\Column(type: 'integer')]
40+
#[ORM\GeneratedValue]
41+
#[ApiProperty(identifier: false)]
42+
#[Ignore]
43+
#[LegacyIgnore]
44+
private ?int $id = null;
45+
46+
#[ORM\Column(type: 'symfony_uuid', unique: true, nullable: false)]
47+
#[ApiProperty(identifier: true)]
48+
#[SerializedName('id')]
49+
#[LegacySerializedName('id')]
50+
private ?Uuid $uuid = null;
51+
52+
/**
53+
* A nice person.
54+
*/
55+
#[ORM\Column]
56+
#[Assert\NotBlank]
57+
public string $name = '';
58+
59+
public function getId(): ?int
60+
{
61+
return $this->id;
62+
}
63+
64+
public function getUuid(): ?Uuid
65+
{
66+
return $this->uuid;
67+
}
68+
}

0 commit comments

Comments
 (0)