Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[Serializer] Addname_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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Conversation

alanpoulain
Copy link
Contributor

QA
Branch?master
Bug fix?no
New feature?yes
Deprecations?yes
TicketsN/A
LicenseMIT
Doc PRTo do

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:

services:app.serializer.normalizer.object.snake_case:class:Symfony\Component\Serializer\Normalizer\ObjectNormalizerautoconfigure:falsearguments:$nameConverter:'@serializer.name_converter.camel_case_to_snake_case'app.serializer.denormalizer.array.snake_case:class:Symfony\Component\Serializer\Normalizer\ArrayDenormalizerautoconfigure:falseapp.serializer.normalizer.datetime.snake_case:class:Symfony\Component\Serializer\Normalizer\DateTimeNormalizerautoconfigure:falseapp.serializer.encoder.json.snake_case:class:Symfony\Component\Serializer\Encoder\JsonEncoderautoconfigure:falseapp.serializer.snake_case:class:Symfony\Component\Serializer\Serializerautoconfigure:falsearguments:            -['@app.serializer.normalizer.datetime.snake_case', '@app.serializer.normalizer.object.snake_case', '@app.serializer.denormalizer.array.snake_case']            -['@app.serializer.encoder.json.snake_case']

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:

if ($normalizerinstanceof SerializerAwareInterface) {
$normalizer->setSerializer($this);
}
if ($normalizerinstanceof DenormalizerAwareInterface) {
$normalizer->setDenormalizer($this);
}
if ($normalizerinstanceof NormalizerAwareInterface) {
$normalizer->setNormalizer($this);
}
).

With this PR, you can just use the classical serializer with a flag given to the context:

$data =$this->normalizer->normalize($object,null, [    AbstractNormalizer::NAME_CONVERTER => CamelCaseToSnakeCaseNameConverter::class,]);

ro0NL, Shine-neko, Koc, sidz, EmilMassey, and dimadeush reacted with heart emoji
/**
* 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 = [])
Copy link
ContributorAuthor

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;
Copy link
ContributorAuthor

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.

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?

Copy link
ContributorAuthor

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);
Copy link
ContributorAuthor

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?

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.

@alanpoulain
Copy link
ContributorAuthor

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).
Failures in AppVeyor doesn't seem related.

@nicolas-grekasnicolas-grekas added this to thenext milestoneJan 20, 2020
Copy link
Member

@nicolas-grekasnicolas-grekas left a 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;

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);

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());

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.

Copy link
ContributorAuthor

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?

Copy link
Member

@nicolas-grekasnicolas-grekasJan 20, 2020
edited
Loading

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.

Copy link
ContributorAuthor

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.

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

@alanpoulain
Copy link
ContributorAuthor

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.

@nicolas-grekas
Copy link
Member

nicolas-grekas commentedJan 20, 2020
edited
Loading

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.

@dunglas
Copy link
Member

dunglas commentedJan 22, 2020
edited
Loading

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.

andrew-demb reacted with thumbs up emoji

@alanpoulain
Copy link
ContributorAuthor

@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).

@dunglas
Copy link
Member

What you're trying to achieve here can already be done user-land by creating a "smart" name converter implementing theAdvancedNameConverterInterface. It can use a chain of responsibility pattern or something similar to select how to normalize the property names depending of the context.

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.

@krewetka
Copy link

@dunglas :

What we need is an easy way to create several serializer instances, not introducing unneeded complexity in the component.

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

fabpot added a commit that referenced this pull requestSep 19, 2024
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       | MITThe 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)).```yamlserializer:  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:```yamlget_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:![image](https://github.com/symfony/symfony/assets/2445045/0468ccf3-8d5a-4c60-a639-09cfe918b163)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.defaultSymfony 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.api1Symfony 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.normalizerSymfony 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:```yamlserializer:  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 featureCommits-------29bd8ce [Serializer] Introduce named serializers
symfony-splitter pushed a commit to symfony/serializer that referenced this pull requestSep 19, 2024
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       | MITThe 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)).```yamlserializer:  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:```yamlget_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:![image](https://github.com/symfony/symfony/assets/2445045/0468ccf3-8d5a-4c60-a639-09cfe918b163)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.defaultSymfony 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.api1Symfony 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.normalizerSymfony 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:```yamlserializer:  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 featureCommits-------29bd8ce6c2 [Serializer] Introduce named serializers
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@nicolas-grekasnicolas-grekasnicolas-grekas left review comments

@dunglasdunglasAwaiting requested review from dunglas

Assignees
No one assigned
Projects
None yet
Milestone
5.1
Development

Successfully merging this pull request may close these issues.

5 participants
@alanpoulain@nicolas-grekas@dunglas@krewetka@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp