Conversation
audain-dg
force-pushed
the
feat/managed-entity-transform
branch
from
September 20, 2026 16:37
6ac61dd to
230285e
Compare
A resource may declare its mapping in the read direction only —
`#[Map(source: Entity::class)]` on the resource — which keeps the entity free
of any presentation concern. Reading works; writing does not. A relation typed
on another resource is never converted, reaches the entity's property as-is,
and PropertyAccess throws a 500:
Expected argument of type "?Author", "AuthorResource"
given at property path "author"
Declaring the reverse mapping only moves the failure: the mapper then builds a
fresh entity from the resource's scalars — right identifier, an instance
Doctrine has never seen — and the flush raises "A new entity was found through
the relationship". Cascading inserts a duplicate row instead.
ManagedEntityTransform resolves the related resource to the managed object it
stands for. Nothing is declared per relation: the managed class comes from the
related resource's state options, the identifiers from IdentifiersExtractor —
never assumed to be called `id`. A to-many arrives as an iterable and every
item is resolved.
Complements api-platform#7698: PersistProcessor::handleLazyObjectRelations() swaps an
unmanaged ENTITY for a reference, which is reached when the mapper already
produced one; it never converts a resource into an entity.
audain-dg
force-pushed
the
feat/managed-entity-transform
branch
from
September 20, 2026 17:05
230285e to
f6a8cf0
Compare
soyuka
requested changes
Sep 21, 2026
soyuka
left a comment
Member
There was a problem hiding this comment.
I think I understand the use case, it'd be preferable to have the functional use case in the PR description instead of the IA garbage.
I don't like the implementation of the transform, I think that this will be solved by the Symfony implementation where a property, being another mapped object, should be automatically be applied. For now I won't merge this I need more manual investigation.
Contributor
Author
|
This what i have in on of my project as a temp fix @soyuka |
The doctrine-common suite runs standalone in CI, where neither
symfony/object-mapper nor api-platform/doctrine-orm is installed.
ManagedEntityTransform implements TransformCallableInterface and its test
builds an ORM State\Options, so both are fatal there:
Class "ApiPlatform\Doctrine\Orm\State\Options" not found
Interface "Symfony\Component\ObjectMapper\TransformCallableInterface" not found
Tests: 38, Assertions: 127, Errors: 6.
symfony/object-mapper joins require-dev and conflict in doctrine-common and
doctrine-orm, the way api-platform/state already declares it.
The test moves to doctrine-orm: getStateOptionsClass() resolves the managed
class by instanceof against the concrete ORM, ODM and Eloquent options, so
nothing can stand in for one, and doctrine-common deliberately requires no
api-platform sibling package — only doctrine/orm and doctrine/mongodb-odm.
audain-dg
force-pushed
the
feat/managed-entity-transform
branch
from
September 21, 2026 18:07
280bdd1 to
d0c0561
Compare
Contributor
Author
The doctrine-orm lowest and minimal-changes jobs install the released
doctrine-common instead of linking the workspace, so a test living there cannot
see a class this branch adds to doctrine-common:
Error: Class "ApiPlatform\Doctrine\Common\State\ManagedEntityTransform" not found
Tests: 303, Assertions: 988, Errors: 6.
Resolving the managed class through getStateOptionsClass() needs a concrete
State\Options, which is ORM, ODM or Eloquent and never doctrine-common, so the
class belongs with its test in doctrine-orm. doctrine-common is untouched again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In my project the API resources are DTOs that keep the entities free of any
presentation concern, so they map in one direction only:
#[Map(source: Author::class)]on the resource, nothing on the entity.
Reading works, writing does not.
POST /bookswith{"title": "…", "author": "/authors/1"}denormalizes
authorinto anAuthorResource, the mapper has no reason to convert it,and it lands on
Book::$author:Adding the reverse mapping does not help, it moves the failure: the mapper then builds a
fresh
Authorfrom the resource's scalars — the right id, an instance Doctrine has neverseen — and the flush raises
A new entity was found through the relationship. Withcascade persist it inserts a duplicate row.
I have been carrying the transform I posted in the comments in that project for a while.
This PR is that class, generalised:
The managed class comes from the related resource's state options and the identifier from
IdentifiersExtractor, so a resource keyed on acoderather than anidworks too —that is a real case for us, and hardcoding
->idsilently resolves nothing there. Ato-many arrives as an iterable and every item is resolved.
It stays opt-in: you name it on the property like any other
transform:. It could beapplied automatically for a property whose declared type is a resource backed by a managed
class, but that changes behaviour for every mapped resource, so I left it out.
This does not overlap #7698:
handleLazyObjectRelations()swaps an unmanaged entity for areference, which only happens once the mapper has already produced an entity. It never
turns a resource into one, so the
source:-only direction still ends in the type errorabove.
Tested by
StateOptionTest::testPostWithRelationMappedFromTheResourceOnly, on fixtureswhose entity carries no mapping attribute at all; dropping the
transform:reproduces the500. If you would rather the object mapper handle this by itself, that test still pins the
bug — keep it and drop my transform.