From d132d897c72a625baf80fd56e42ddd054c24edf3 Mon Sep 17 00:00:00 2001 From: Audain <35590376+audain-dg@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:40:00 +0200 Subject: [PATCH 1/3] docs(dto): relations between mapped resources --- core/dto.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/core/dto.md b/core/dto.md index fa4b241bada..12dae5d7a77 100644 --- a/core/dto.md +++ b/core/dto.md @@ -152,6 +152,58 @@ The `ObjectMapperProcessor` receives the deserialized Input DTO. It uses then delegates to the underlying Doctrine processor (to persist the Entity). Finally, it maps the persisted Entity back to the Output DTO Resource. +### Relations Between Mapped Resources + +A relation typed on another DTO Resource needs one more thing: the object mapper builds objects and +has no identity map, so the related DTO has to be turned back into the **managed** Doctrine object +before the entity is flushed. Left alone, the DTO itself reaches the entity's property and +`PropertyAccess` fails: + +``` +Expected argument of type "?App\Entity\Author", "App\Api\Resource\Author" given at property path "author" +``` + +Declaring the reverse mapping is not enough either: the mapper then builds a *fresh* entity from the +DTO's scalars — the right identifier, but an instance Doctrine has never seen — and the flush raises +`A new entity was found through the relationship`. Cascading inserts a duplicate row instead. + +Use `ManagedEntityTransform` on the relation: + +```php +// src/Api/Resource/Book.php +namespace App\Api\Resource; + +use ApiPlatform\Doctrine\Common\State\ManagedEntityTransform; +use App\Entity\Book as BookEntity; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[Map(source: BookEntity::class)] +final class Book +{ + public ?int $id = null; + + public string $title = ''; + + #[Map(target: 'author', transform: ManagedEntityTransform::class)] + public ?Author $author = null; +} +``` + +Nothing is declared per relation: the managed class is read from the related resource's +`stateOptions`, and the identifier from its metadata — it is never assumed to be called `id`, so a +resource keyed on a natural code (an ISO code, a currency, a slug) works the same way. + +A to-many relation is handled by the same transform, which resolves every item of the collection: + +```php +#[Map(target: 'categories', transform: ManagedEntityTransform::class)] +public iterable $categories = []; +``` + +Only the write direction needs it. On read, a to-one is mapped by the object mapper itself — the +related resource declares `#[Map(source: Entity::class)]` — and a to-many by Symfony's +`MapCollection`. + ## 2. Automated Mapped Inputs and Outputs Ideally, your read and write models should differ. You might want to expose less data in a From 2932224cd54b99b265ced09bc3c651fe4b56ebc5 Mon Sep 17 00:00:00 2001 From: Audain <35590376+audain-dg@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:09:07 +0200 Subject: [PATCH 2/3] docs(dto): satisfy the lint and prettier checks The error-message block had no language, which MD040 rejects, and the emphasis around "fresh" used the asterisk form prettier rewrites to underscores. --- core/dto.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/dto.md b/core/dto.md index 12dae5d7a77..ced2ffc6b14 100644 --- a/core/dto.md +++ b/core/dto.md @@ -159,11 +159,11 @@ has no identity map, so the related DTO has to be turned back into the **managed before the entity is flushed. Left alone, the DTO itself reaches the entity's property and `PropertyAccess` fails: -``` +```txt Expected argument of type "?App\Entity\Author", "App\Api\Resource\Author" given at property path "author" ``` -Declaring the reverse mapping is not enough either: the mapper then builds a *fresh* entity from the +Declaring the reverse mapping is not enough either: the mapper then builds a _fresh_ entity from the DTO's scalars — the right identifier, but an instance Doctrine has never seen — and the flush raises `A new entity was found through the relationship`. Cascading inserts a duplicate row instead. From 1b5457cba4d10a8fc0e9351e9c7416887b6be257 Mon Sep 17 00:00:00 2001 From: Audain <35590376+audain-dg@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:23:22 +0200 Subject: [PATCH 3/3] docs(dto): ManagedEntityTransform lives in doctrine-orm --- core/dto.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/dto.md b/core/dto.md index ced2ffc6b14..f92360a2d4e 100644 --- a/core/dto.md +++ b/core/dto.md @@ -173,7 +173,7 @@ Use `ManagedEntityTransform` on the relation: // src/Api/Resource/Book.php namespace App\Api\Resource; -use ApiPlatform\Doctrine\Common\State\ManagedEntityTransform; +use ApiPlatform\Doctrine\Orm\State\ManagedEntityTransform; use App\Entity\Book as BookEntity; use Symfony\Component\ObjectMapper\Attribute\Map;