api-platform / api-platform/core

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

Đang mở
#8,438 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
PHP
Star
2.6k
Fork
980
Merge trung bình
2 ngày 4 giờ
Pull request đã merge (30 ngày)
49

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
php, symfony
Lĩnh vực
api, backend, database
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
55/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.