-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Serializer] Add name_converter
flag and use an array of name converters
#35374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
/** | ||
* Sets the {@link ClassMetadataFactoryInterface} to use. | ||
*/ | ||
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = []) | ||
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, /* array */ $nameConverter/*s*/ = [], array $defaultContext = []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it's possible to change or deprecate the name of a constructor argument since it could be used by DI with a named argument.
$this->nameConverter = $nameConverter; | ||
} else { | ||
foreach ($nameConverter as $nameConverterInstance) { | ||
$this->nameConverters[\get_class($nameConverterInstance)] = $nameConverterInstance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a good idea to use the FQCN for referencing a name converter?
We could also use the service id.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope, it's not: it prevents passing two differently configured instances of the same class
instead, why not pass a service locator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use a service locator too.
We would keep the name converter and we would add one more argument, am I right?
Or maybe we can replace the name converter with the service locator. I'm not sure to like it since it means not having a clear dependency between the normalizer and the name converter and having to use a default name converter service id.
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; | ||
$key = $paramName; | ||
if (\count($this->nameConverters) > 0) { | ||
$nameConverter = $this->nameConverters[$context[static::NAME_CONVERTER] ?? null] ?? reset($this->nameConverters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the given name converter in context doesn't correspond to one of the name converters, the first name converter is used instead.
The user is not warned, but I think it's the right approach. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an exception should be thrown instead to me
having a service locator would naturally throw a service-bot-found exception btw.
Low deps tests are failing in Travis but it seems logical to me (since the modified configuration of the serializer cannot be applied to an old version of the code). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider passing a name converter instance as a context option?
$this->nameConverter = $nameConverter; | ||
} else { | ||
foreach ($nameConverter as $nameConverterInstance) { | ||
$this->nameConverters[\get_class($nameConverterInstance)] = $nameConverterInstance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope, it's not: it prevents passing two differently configured instances of the same class
instead, why not pass a service locator?
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; | ||
$key = $paramName; | ||
if (\count($this->nameConverters) > 0) { | ||
$nameConverter = $this->nameConverters[$context[static::NAME_CONVERTER] ?? null] ?? reset($this->nameConverters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an exception should be thrown instead to me
having a service locator would naturally throw a service-bot-found exception btw.
@@ -323,7 +390,7 @@ protected function getNormalizerForMaxDepth(): NormalizerInterface | |||
protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface | |||
{ | |||
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); | |||
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); | |||
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, [], new PhpDocExtractor()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null should still be accepted, no need to deprecate it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you want to keep passing null? What would it mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary deprecations should be avoided, that's my point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK 👍
Serializer | ||
---------- | ||
|
||
* Deprecated passing a name converter directly to the second argument of the constructor of `AbstractNormalizer`, pass an array of name converters instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why? it looks fine to me
I did consider passing an instance in the context but it would not seem clean to me. It has the advantage of not deprecating anything though. |
Actually it would be cleaner: the current approach requires some "long-distance" correlation between what is provided to the constructor and what string is passed in the context. That's quite fragile, with the same drawbacks as documented in "service locators are bad" articles. But then, there is this cacheability consideration. To be investigated. |
For such cases, instead of introducing such complexity, wouldn't it be better to use several Serializer instances? One instance to serialize your internal data structure, one other to deserialize the data coming from an external API? I would like to have a factory mechanism to create as many serializers as needed in FrameworkBundle, as we do for Cache. It would also allow to improve the overall performance of the Serializer by registering less useless normalizers for a given instance. |
@dunglas That's the issue here. Defining a specific serializer is cumbersome and can lead to many issues if not defined properly (the main issue being the necessity to redefine all the normalizers and encoders you need to use, see my explanation in the description of the PR). Moreover I think it's not very clear for a newcomer to understand how changing the name converter can be done (the name converter is not defined in the serializer but in the object normalizer). |
What you're trying to achieve here can already be done user-land by creating a "smart" name converter implementing the Also, I've the feeling that it's not the good way to fix this issue. What we need is an easy way to create several serializer instances, not introducing unneeded complexity in the component. I'm closing for now. Thanks for your PR anyway. Don't hesitate to reopen if I'm missing the point. |
@dunglas :
is there maybe any way to achive it already now in newest version of symfony? I also have use case with 2-3 separate serializers needed ( with different name converters) -> one for consuming one external API, another for another integration + another one for exposing our own API |
This PR was squashed before being merged into the 7.2 branch. Discussion ---------- [Serializer] Introduce named serializers | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT The idea behind this PR is to allow configuring multiple serializer instances with different default contexts, name converters, and sets of normalizers and encoders. This is useful when your application is communicating with multiple APIs, each with different rules. Similar ideas have been mentioned [before](#35374 (comment)). ```yaml serializer: named_serializers: api1: name_converter: 'serializer.name_converter.camel_case_to_snake_case' default_context: enable_max_depth: true api2: default_context: enable_max_depth: false ``` The different serializers can be injected using named aliases: ```php #[AsController] class HomeController { #[Route('/', name: 'app_home')] public function index( SerializerInterface $serializer, // Default serializer SerializerInterface $api1Serializer, // api1 serializer #[Target('api2.serializer')] // api2 serializer SerializerInterface $someName, ) { // ... } } ``` Multiple normalizer/encoder instances with different arguments are created as child services of the default ones. ~~I also ensured that the same normalizer/encoder instances are reused between different serializers that have the same default context and name converter to minimize the number of child services created.~~ Custom normalizers/encoders can target specific serializers using the `serializer` tag attribute: ```yaml get_set_method_normalizer: class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer autoconfigure: false # This is needed so that it's not included in the default serializer tags: # single serializer - serializer.normalizer: { serializer: 'api1' } # or multiple ones - serializer.normalizer: { serializer: [ 'api1', 'api2' ] } # use * to include the service in all serializers including the default one - serializer.normalizer: { serializer: '*' } ``` For BC reasons, not setting the `serializer` tag attribute is the same as setting it to the default one: ```yaml tags: - serializer.normalizer # same as - serializer.normalizer: { serializer: 'default' } ``` The profiler has been updated to support multiple serializer instances:  All normalizers/encoders are tagged with additional named serializer specific tags to help with debugging. To get the priority of normalizers/encoders for a certain serializer, use the `serializer.normalizer.<name>` or `serializer.encoder.<name>` tags: ```shell $ bin/console debug:container --tag serializer.normalizer.default Symfony Container Services Tagged with "serializer.normalizer.default" Tag ========================================================================== ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- Service ID priority Class name ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer serializer.normalizer.flatten_exception -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- $ bin/console debug:container --tag serializer.normalizer.api1 Symfony Container Services Tagged with "serializer.normalizer.api1" Tag ======================================================================= ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- Service ID priority Class name ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer get_set_method_normalizer Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer serializer.normalizer.flatten_exception -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- $ bin/console debug:container --tag serializer.normalizer Symfony Container Services Tagged with "serializer.normalizer" Tag ================================================================== ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- Service ID standard priority serializer Class name ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer get_set_method_normalizer ["api1","api2"] Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer serializer.normalizer.flatten_exception 1 -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array 1 -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object 1 -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- ``` Since Symfony comes with some pre-registered normalizers and encoders, I added options to exclude those in case someone wants to use only custom ones: ```yaml serializer: named_serializers: api1: include_built_in_normalizers: false include_built_in_encoders: true name_converter: 'serializer.name_converter.camel_case_to_snake_case' default_context: enable_max_depth: true ``` TBH, I have doubts about the usefulness of this, please let me know your thoughts. I've split the PR into two commits to ease reviewing: - the first commit only rearranges the `SerializerPass` without adding any features - the second commit implements the feature Commits ------- 29bd8ce [Serializer] Introduce named serializers
This PR was squashed before being merged into the 7.2 branch. Discussion ---------- [Serializer] Introduce named serializers | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT The idea behind this PR is to allow configuring multiple serializer instances with different default contexts, name converters, and sets of normalizers and encoders. This is useful when your application is communicating with multiple APIs, each with different rules. Similar ideas have been mentioned [before](symfony/symfony#35374 (comment)). ```yaml serializer: named_serializers: api1: name_converter: 'serializer.name_converter.camel_case_to_snake_case' default_context: enable_max_depth: true api2: default_context: enable_max_depth: false ``` The different serializers can be injected using named aliases: ```php #[AsController] class HomeController { #[Route('/', name: 'app_home')] public function index( SerializerInterface $serializer, // Default serializer SerializerInterface $api1Serializer, // api1 serializer #[Target('api2.serializer')] // api2 serializer SerializerInterface $someName, ) { // ... } } ``` Multiple normalizer/encoder instances with different arguments are created as child services of the default ones. ~~I also ensured that the same normalizer/encoder instances are reused between different serializers that have the same default context and name converter to minimize the number of child services created.~~ Custom normalizers/encoders can target specific serializers using the `serializer` tag attribute: ```yaml get_set_method_normalizer: class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer autoconfigure: false # This is needed so that it's not included in the default serializer tags: # single serializer - serializer.normalizer: { serializer: 'api1' } # or multiple ones - serializer.normalizer: { serializer: [ 'api1', 'api2' ] } # use * to include the service in all serializers including the default one - serializer.normalizer: { serializer: '*' } ``` For BC reasons, not setting the `serializer` tag attribute is the same as setting it to the default one: ```yaml tags: - serializer.normalizer # same as - serializer.normalizer: { serializer: 'default' } ``` The profiler has been updated to support multiple serializer instances:  All normalizers/encoders are tagged with additional named serializer specific tags to help with debugging. To get the priority of normalizers/encoders for a certain serializer, use the `serializer.normalizer.<name>` or `serializer.encoder.<name>` tags: ```shell $ bin/console debug:container --tag serializer.normalizer.default Symfony Container Services Tagged with "serializer.normalizer.default" Tag ========================================================================== ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- Service ID priority Class name ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer serializer.normalizer.flatten_exception -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- $ bin/console debug:container --tag serializer.normalizer.api1 Symfony Container Services Tagged with "serializer.normalizer.api1" Tag ======================================================================= ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- Service ID priority Class name ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer get_set_method_normalizer Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer serializer.normalizer.flatten_exception -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ------------------------------------------------------------------------------------------- $ bin/console debug:container --tag serializer.normalizer Symfony Container Services Tagged with "serializer.normalizer" Tag ================================================================== ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- Service ID standard priority serializer Class name ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- serializer.denormalizer.unwrapping 1 1000 Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer get_set_method_normalizer ["api1","api2"] Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer serializer.normalizer.flatten_exception 1 -880 Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer ... serializer.denormalizer.array 1 -990 Symfony\Component\Serializer\Normalizer\ArrayDenormalizer serializer.normalizer.object 1 -1000 Symfony\Component\Serializer\Normalizer\ObjectNormalizer ------------------------------------------------- ---------- ---------- ----------------- ------------------------------------------------------------------------------------------- ``` Since Symfony comes with some pre-registered normalizers and encoders, I added options to exclude those in case someone wants to use only custom ones: ```yaml serializer: named_serializers: api1: include_built_in_normalizers: false include_built_in_encoders: true name_converter: 'serializer.name_converter.camel_case_to_snake_case' default_context: enable_max_depth: true ``` TBH, I have doubts about the usefulness of this, please let me know your thoughts. I've split the PR into two commits to ease reviewing: - the first commit only rearranges the `SerializerPass` without adding any features - the second commit implements the feature Commits ------- 29bd8ce6c2 [Serializer] Introduce named serializers
The aim of this PR is to bring the possibility to modify the name converter to use at runtime.
Imagine you use the classical serializer for your (de)normalization needs (if you use some DTOs for instance). You also need to call an API using snake case and you want to use the serializer too.
In the current state, you need to do something like this:
You have to reconfigure all the (de)normalizers because if you use the ones from Symfony in the declaration of your snake case serializer, they can use the snake case serializer instead of the classical one when you (de)normalize your data (mainly because of this:
symfony/src/Symfony/Component/Serializer/Serializer.php
Lines 77 to 87 in af4035d
With this PR, you can just use the classical serializer with a flag given to the context: