api-platform / api-platform/core

ObjectMapper: PersistProcessor::handleLazyObjectRelations() breaks cascade persist of new relations with app-assigned ids (UUID in constructor)

オープン
#8,438 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
PHP
スター
2.6k
フォーク
980
平均マージ
2日 4時間
マージ済み PR(30日)
49

説明

**API Platform version(s) affected**: 4.3.5 (also present on `4.3` and `main` branches)

**Description**

When using the ObjectMapper (`stateOptions` + input DTO) on a `Post` operation, creating a **new** nested related entity fails with a foreign key constraint violation when the related entity uses an **application-assigned identifier** (e.g. a UUID generated in the constructor).

`PersistProcessor::handleLazyObjectRelations()` runs for every mapped operation (`$operation->canMap()`) and replaces any relation value that is *not managed* but *has non-null identifiers* with `$manager->getReference($class, $identifiers)`:

```php
// Do not get reference for partial objects or objects with null identifiers
if (!$identifiers || \count($identifiers) !== \count(array_filter($identifiers, static fn ($v) => null !== $v))) {
continue;
}

$reflectionProperty->setValue($data, $relManager->getReference($relClass, $identifiers));
```

This heuristic assumes "has an id" means "row exists in the database". That holds for `#[ORM\GeneratedValue]` entities (id is null pre-flush, so the swap is skipped and `cascade: ['persist']` works — this is the case covered by the `Issue7689` fixtures where `Issue7689Category` has a nullable auto-increment id). But with app-assigned ids (UUIDv4/v7 assigned in the constructor, a common pattern), a freshly mapped nested entity is indistinguishable from a reference to an existing one:

1. The mapper creates a new related entity; its constructor assigns a UUID.
2. `handleLazyObjectRelations()` sees an unmanaged object with a non-null identifier and swaps it for a `getReference()` proxy.
3. The proxy counts as managed, so `cascade: ['persist']` on the owning side never persists it.
4. Flush inserts the owning entity with an FK pointing to a nonexistent row:

```
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row:
a foreign key constraint fails (FOREIGN KEY ("condition_id") REFERENCES "..." ("id"))
```

**How to reproduce**

Entities (owning side cascades persist to the relation; both use constructor-assigned UUIDs):

```php
#[ORM\Entity]
class TaskRule
{
public function __construct(
#[ORM\ManyToOne(targetEntity: TaskRuleCondition::class, cascade: ['persist'])]
#[ORM\JoinColumn(nullable: true)]
public ?TaskRuleCondition $condition = null,
#[ORM\Id, ORM\Column]
private(set) UuidV7 $id = new UuidV7(),
) {
}
}

#[ORM\Entity]
class TaskRuleCondition
{
public function __construct(
#[ORM\Column]
public string $completeWhen,
#[ORM\Id, ORM\Column]
private(set) UuidV7 $id = new UuidV7(),
) {
}
}
```

Input DTOs mapped with the ObjectMapper:

```php
#[Map(target: TaskRule::class)]
final readonly class TaskRulePostInput
{
public function __construct(
public ?TaskRuleConditionInput $condition = null,
) {
}
}
```

`POST` with a nested `condition` payload -> FK violation on flush. Persisting the owning entity manually in a custom processor *before* `PersistProcessor` runs works around it, because the cascade marks the relation managed and `handleLazyObjectRelations()` then skips it via the `contains()` check.

**Possible Solution**

Only swap values that are genuinely placeholders for existing rows, instead of assuming any unmanaged object with an id is one:

- only replace actual uninitialized lazy objects (`ReflectionClass::isUninitializedLazyObject()` on PHP >= 8.4), and/or
- consult `UnitOfWork::getEntityState($value)` — `STATE_NEW` objects should be left alone so cascade persist can handle them.

**Additional Context**

- Introduced/extended in cc2f8855 ("fix(doctrine): post with mapped relation", #7689) — the accompanying test fixtures only cover DB-generated ids, where new entities still have null ids pre-flush.
- Related but distinct: #7735/#7738 fixed uninitialized-property crashes in the same method; #7801 concerns nested mapping on the output side.

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

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

調査の方向性

Start by locating PersistProcessor::handleLazyObjectRelations() and the existing Issue7689 fixtures/tests mentioned in the issue. Reproduce a mapped POST with a nested relation whose UUID is assigned in the constructor. Done means new unmanaged related entities with non-null ids are left for cascade persist, while references to existing rows still behave correctly.

索引モデルが issue の本文から書いたものです。

評価

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

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

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