UniqueEntity always rejects a resource's own unchanged value on standard PUT
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
Direzione di ricerca
Inizia riproducendo il flusso PUT standard descritto nell’issue, quindi esamina DeserializeListener.php e UniqueEntityValidator.php nei percorsi indicati. Segui l’oggetto passato alla validazione e il risultato del repository, quindi aggiungi una copertura di regressione per un valore univoco invariato. Il lavoro è completato quando il PUT standard restituisce 200 senza una violazione, mentre il comportamento esistente di PATCH rimane corretto.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
API Platform version(s) affected: 4.3.17 (root cause also present on main)
Description
On a "standard" PUT, replacing a resource with its own, unchanged value for a field covered by a Symfony UniqueEntity constraint incorrectly fails validation with "This value is already used.", even though the value legitimately belongs to the very record being updated.
The cause is a mismatch between two independent pieces of framework logic:
- For a standard PUT, API Platform deliberately does not populate the entity Doctrine already manages for that id. Instead it denormalizes the request body into a brand-new, transient object. This is intentional, it's what gives PUT its "full replace" semantics (any field omitted from the body resets to its default, unlike PATCH):
// vendor/api-platform/symfony/EventListener/DeserializeListener.php
$assignObjectToPopulate = 'POST' === $method
|| 'PATCH' === $method
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
For a standard PUT (standard_put defaulting to true), this is false, so AbstractItemNormalizer instantiates a fresh object rather than mutating the one fetched from the database.
- That transient object is exactly what reaches validation.
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidatordecides "this match is fine, it's the same record" purely by PHP object identity:
// vendor/symfony/doctrine-bridge/Validator/Constraints/UniqueEntityValidator.php
$result = $repository->{$constraint->repositoryMethod}(...$arguments);
...
if (!$result || (1 === \count($result) && current($result) === $value)) {
return; // no violation: it's the same entity
}
$value is the object currently being validated. current($result) is whatever Doctrine's repository returns for the field(s) in question — which, thanks to Doctrine's identity map, is normally the same managed PHP instance as $value when you're updating an already-loaded entity (that's exactly how this self-exclusion is supposed to work, and it's why the analogous PATCH flow — which does populate the existing entity — behaves correctly).
For a standard PUT, though, $value is the disconnected transient object from step 1. It can never be === to the entity Doctrine loads from the database, no matter how the values compare. So the self-exclusion branch can never be taken, and every unique field submitted back unchanged is reported as a conflict against itself.
How to reproduce
Any Doctrine ORM/ODM resource with a standard PUT (allowCreate: true or otherwise, standard_put at its default of true) and a UniqueEntity constraint on at least one field:
#[ApiResource(operations: [new Get(), new Put(allowCreate: true)])]
#[UniqueEntity(fields: ['foo'])]
#[ORM\Entity]
class Example
{
#[ORM\Id, ORM\Column]
public ?int $id = null;
#[ORM\Column]
public string $foo = '';
}
# 1. Create the resource
PUT /examples/1
Content-Type: application/ld+json
{"foo": "a"}
=> 201 Created
# 2. Replace it with the exact same value
PUT /examples/1
Content-Type: application/ld+json
{"foo": "a"}
=> 422 Unprocessable Content
{
"violations": [
{"propertyPath": "foo", "message": "This value is already used."}
]
}
Step 2 should return 200 OK — nothing changed — but instead fails as if foo: "a" belonged to a different row.
- Lingua principale
- PHP
- Stelle
- 2.6k
- Fork
- 982
- Merge medio
- 1g 16h
- PR unite (30g)
- 59
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di api-platform/core
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
api-platform/core#8573 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
api-platform/core#8571 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
api-platform/core#8564 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
api-platform/core#8495 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
api-platform/core#8471 ·
Tutte le issue di api-platform/core
Issue simili
-
tooling
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
-
UX
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
ProfessionalWiki/NeoWiki#1525 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
OpenConext/OpenConext-engineblock#2122 ·
-
Bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
Automattic/safe-publish#594 ·