[ObjectMapper] Infer nested target class from destination property type - #65970
[ObjectMapper] Infer nested target class from destination property type#65970iz-ahmad wants to merge 1 commit into
Conversation
|
I see the CI is red on |
…e of the destination property A class that declares #[Map(source: ...)] describes how it is built from that source. Until now the mapper only used that declaration for the object it maps at the top level, or for a nested object whose target class was listed in a class map. A nested object whose own class declares nothing was copied as it is, which fails when the destination property is typed with the resource class. PropertyTypeMappingMetadataFactory resolves that case. It decorates another metadata factory and answers only when that factory reports no mapping for the value: it reads the class typing the property the value is written into, and maps the value to it when that class declares a matching #[Map(source: ...)]. It is the outermost decorator, both in the chain the no-argument ObjectMapper constructor builds and in the container, so an explicit mapping always wins. The mapper passes the destination to the metadata factory through the context, under the "target" and "target_property" keys, and only when it can write that property. Resolution therefore never runs for a value the mapper would throw away. A property typed with a union, an intersection or nothing at all is left alone, as is a property whose type the value already satisfies. A property typed with an abstract class or an interface that declares a matching source is refused with a MappingException, since the mapper cannot instantiate it.
d98e731 to
71dc628
Compare
|
Thank you for your patience on this one, and for the diagnosis. The gap you found is real: a class that declares I reworked where it runs. In the branch as it stood, the resolution lived in
It also helped that your So the resolution now lives in a new On the fixtures: I checked that turning this on by default changes no case that works today. Everything it newly handles fails with a One trade-off worth knowing: The branch is squashed into one commit with your authorship, rebased on 8.2, and the component and FrameworkBundle suites are green. I also rewrote the description as documentation for the eventual docs pull request. |
|
/cc @soyuka WDYT? |
A class that declares
#[Map(source: ...)]says how it is built from that source. The mapper used that declaration for the object it maps at the top level, and for a nested object whose target class was listed in a class map, but not for a nested object in the general case. Such a value was copied as it is, so this failed:NestedEntityResourcedeclares#[Map(source: NestedEntity::class)], and$nestedis typed with it, so there is enough information to build it. In a Symfony application this already worked, because the bundle collects every service carrying#[Map]into a class map. Standalone, or for a class that is not a service, it did not.What this adds
Symfony\Component\ObjectMapper\Metadata\PropertyTypeMappingMetadataFactory, a metadata factory that decorates another one:It answers only when the factory it decorates reports no mapping for the value. Then it reads the class typing the property the value is written into, and maps the value to that class if it declares a matching
#[Map(source: ...)].It is enabled by default.
new ObjectMapper()builds it aroundReflectionObjectMapperMetadataFactory, andobject_mapper.metadata_factory.property_typedecoratesobject_mapper.metadata_factoryin the container. In both it is the outermost decorator, so an explicit#[Map], a class map entry and enum conversion all take precedence. Pass your own factory toObjectMapperto leave it out; the factory you pass is then the only authority on mappings.When it applies
All of these must hold:
#[Map(source: S)]with the value being anS.A property typed with a union, an intersection or nothing at all is left alone, and so is a property whose type the value already satisfies, so
?Animalreceiving aDogstill stores theDog.How it reports problems
#[Map(source: ...)]on the destination property type match the value, for example one naming the class and one naming an interface it implements:MappingException, "Ambiguous mapping ... Use the "if" parameter to specify conditions." Add anifcondition to one of them.MappingException,Cannot infer a mapping target for "X" from property "Y::$z": class "T" is not instantiable.Type the property with a concrete class, or give the#[Map]atransformthat returns the instance.New API
The
$contextargument ofObjectMapperMetadataFactoryInterface::create()now carries two keys the mapper fills for a nested value, documented on the interface:target, the class the value is written into, andtarget_property, the property it is written into. They are set only when the mapper can write that property. Custom factories may read them; they are optional and absent for other calls.Points for the documentation
#[Map], then the class map, then this inference.ObjectMapperopts out unless you composePropertyTypeMappingMetadataFactoryyourself. This matters for the commonnew ObjectMapper(new ReflectionObjectMapperMetadataFactory(), $propertyAccessor).MappingExceptionmessages and what to do about each.