Description
Hey everyone 👋
This issue is here to standardize how we should think about mapping in future and a proposition on how we could harmonize all theses ideas in one component.
Context
Today we have some PR that are about mapping and we all have ideas on how we should do things. And we probably also already have use-case for all those ideas.
Currently when you think about mapping (object to object mapping, atleast), you will require to use the Serializer not exactly as it was designed. You'll need to normalize your source object then denormalize to the targeted object.
This is a bit complex for what we want to do and it brings some limitations:
- We will have an intermediate array state, the bigger the source object is, the bigger the intermediate array will be;
- If we want to customize some part of that mapping, we will need to write a Normalizer/Denormalizer for the whole transformation.
Due to these limitations, we think that there is a need for a new component that will map objects.
Current implementations
Let's list the current two main implementations:
-
A talk was given by @weaverryan about separating Doctrine Entities from POPOs, which resulted in the micro-mapper repository. From there @soyuka made a PR for a new Mapper component: [ObjectMapper] Object to Object mapper component #51741
-
Some times ago, a PR came for an AutoMapper component by @joelwurtz, based on code generation and it was abandoned because of it was too far from the Serializer interface. After that we made it as an external repository which we are still working on after all these years: https://github.com/jolicode/automapper. Last year I tried to revive that last PR with the latest stuff from the external repository.
Both theses implementations are trying to tackle object to object mapping. Indeed we have some different features because of how we implemented things:
- @soyuka's PR focus on DX & simple implementation. The code is really easy to understand and it will do object to object mapping really well. It's mostly based onto PropertyAccessor component.
jolicode/automapper
is focused on performance and can map a bit more than from an object to an object. It's more complex due to AST usage and is mostly based on PropertyInfo component (which will switch on TypeInfo once 7.1 is released)
Future
We talked with some core contributors during SymfonyLive Paris 2024 (28/29 march) and with @nicolas-grekas we came to the conclusion that we won't find a perfect implementation to merge as it is and we should iterate on all theses ideas.
In my opinion, the component should be focused on one main interface:
interface ObjectMapperInterface
{
/**
* @template T
* @template K
*
* @param array|\stdClass|T $source
* @param class-string<K>|'array'|K $target
* @return array|K
*/
public function map(array|object $source, string|object $target, array $context = []): array|object;
}
This interface will handle object to object mapping and array to object (or inverse) mapping.
I added array to object mapping (or inverse) because I do think we should it for some cases like handling a Request query parameters of to handle mapping Form values to an object.
For now, best thing would be to merge @soyuka's PR and iterate from that point towards theses interfaces with both implementations. Having two implementations isn't a big issue, we already have components working that way like DependencyInjection or ExpressionLanguage. Here the goal is to have two way to map objects:
- Using reflection
- Generating code
Using reflection makes the code way simpler and generating code is more complex but gives amazing performance.
Once the ObjectMapper PR is merged we need to make a wide and very good test suite so we can start adding code generating and validate we have same behavior with both implementations.
That way we can use any implementation whenever one fit better than the other for our use-case.
Metadata
Here is a section about metadata management within the ObjectMapper, I'll try to explain how metadata retrieval should work. There is 3 ways to collect metadata: from the source, from the target or from both.
We will fetch metadata from the source or from the target only when the source or target is either an array
or a \stdClass
. We will always fetch metadata from the non-array (or stdClass) object and try to match similar fields within the array or stdClass. When mapping to an array or stdClass, it's easy since all the fields are to create so there won't be matching issues. But when mapping from an array or stdClass, we will need to check if the fields does match properties in the target object, when it's matching we will map but if it's not matching we won't map that field.
Mapping an array to array (or stdClass) should throw an exception and is not possible.
Then, last method is getting metadata from both the source and target objects:
When doing that we will fetch all properties to map from both objects and if one of the source object property matches one of the target property, we will map that field. A property can match another property if its name match in both objects (or same configured name, if MapTo/MapFrom brings some new name).
Conclusion
I really do think ObjectMapper is an awesome component for Symfony. Today it's complex to improve the Serializer and I do think the "next" Serializer will be coming from another component or another set of component.
With the RFC as it is, we could replace the Normalization part of the Serializer.
In addition to component like @mtarld's JsonEncoder it could make a complete serialization replacement like Mathias & I showed at SymfonyLive 2024 with the TurboSerializer.