UniqueEntity always rejects a resource's own unchanged value on standard PUT
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
調査の方向性
まず、issue に記載された標準的な PUT フローを再現し、次に指定されたパスにある DeserializeListener.php と UniqueEntityValidator.php を調査します。バリデーションに渡されるオブジェクトと repository の結果を追跡し、変更されていない一意の値に対するリグレッションカバレッジを追加します。標準的な PUT が violation なしで 200 を返し、既存の PATCH の動作が正しいままであれば完了です。
索引モデルが issue の本文から書いたものです。
説明
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.
- 主要言語
- PHP
- スター
- 2.6k
- フォーク
- 982
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 59
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
api-platform/core のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
api-platform/core#8573 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
api-platform/core#8571 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
api-platform/core#8564 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
api-platform/core#8495 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
api-platform/core#8471 ·
api-platform/core の issue をすべて見る
似ている issue
-
tooling
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
UX
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
ProfessionalWiki/NeoWiki#1525 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
OpenConext/OpenConext-engineblock#2122 ·
-
Bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
Automattic/safe-publish#594 ·