Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

UniqueEntity always rejects a resource's own unchanged value on standard PUT

オープン
#8,476 コメント 0 件 リアクション 1 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
静か
技術スタック
php, symfony
領域
api, backend

調査の方向性

まず、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:

  1. 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.

  1. That transient object is exactly what reaches validation. Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator decides "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

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

api-platform/core のほかの issue

api-platform/core の issue をすべて見る

似ている issue

PHP の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。