Skip to content

[Serializer] Introduce named serializers #56823

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

Merged
merged 1 commit into from
Sep 19, 2024
Merged

Conversation

HypeMC
Copy link
Contributor

@HypeMC HypeMC commented May 20, 2024

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.

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:

#[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:

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:

    tags:
        - serializer.normalizer
        # same as
        - serializer.normalizer: { serializer: 'default' }

The profiler has been updated to support multiple serializer instances:

image

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:

$ 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:

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

@carsonbot

This comment has been minimized.

@carsonbot carsonbot added this to the 7.1 milestone May 20, 2024
@HypeMC HypeMC force-pushed the named-serializers branch 2 times, most recently from ecd355d to 63409d5 Compare May 20, 2024 11:52
@OskarStark OskarStark modified the milestones: 7.1, 7.2 May 20, 2024
@alamirault alamirault mentioned this pull request May 20, 2024
fabpot added a commit that referenced this pull request May 21, 2024
This PR was merged into the 7.1 branch.

Discussion
----------

Fix singular phpdoc

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exists, explain below instead -->
| License       | MIT

(Found while reviewing #56823)

Commits
-------

e0f6258 Fix singular phpdoc
@HypeMC HypeMC force-pushed the named-serializers branch from 63409d5 to fecf52b Compare June 6, 2024 23:18
@HypeMC HypeMC force-pushed the named-serializers branch 3 times, most recently from 3e3ef44 to f1c17d7 Compare August 12, 2024 19:27
@dunglas
Copy link
Member

dunglas commented Aug 13, 2024

Good work! This will help improve performance and reduce the complexity of projects needing multiple serializers to deal with different contexts (ex: serializing data exposed through a public API and deserializing data coming from a third-party service). I'm +1 to merge this.

autoconfigure: false # This is needed so that it's not included in the default serializer

Maybe could we tweak the autoconfigurator to automatically exclude definitions already having an explicit serializer tag? This will improve the DX a bit.

Copy link
Contributor

@mtarld mtarld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really great addition!

Just a quick question, why mixing up standard and default as name describing default stuff? Shouldn't we stick to default only?

fabpot added a commit that referenced this pull request Aug 14, 2024
…hild()` shortcut method (HypeMC)

This PR was merged into the 7.2 branch.

Discussion
----------

[DependencyInjection] Add `ContainerBuilder::registerChild()` shortcut method

| Q             | A
| ------------- | ---
| Branch?       | 7.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

Extracted from #56823 as suggested in #56823 (comment)

Commits
-------

7ba430c [DependencyInjection] Add `ContainerBuilder::registerChild()` shortcut method
@stof
Copy link
Member

stof commented Aug 14, 2024

Reusing the same normalizer instances is a very bad idea as it would break for any normalizer implementing any of the *Aware interfaces of the component (as instantiating the second serializer would replace the instance that those shared normalizers are aware of)

@HypeMC HypeMC force-pushed the named-serializers branch from f1c17d7 to 764b46f Compare August 14, 2024 17:11
@HypeMC HypeMC force-pushed the named-serializers branch from 7d850a0 to e3b4b23 Compare August 15, 2024 22:57
@HypeMC
Copy link
Contributor Author

HypeMC commented Aug 15, 2024

Rebase needed after #58013

@chalasr Done

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

@chalasr
Copy link
Member

chalasr commented Sep 18, 2024

fabbot's suggestion looks legit, can you apply it?

@HypeMC
Copy link
Contributor Author

HypeMC commented Sep 18, 2024

$names = array_unique(['default', ...$serializerNames]);
}

if ($tag['built-in'] ?? false) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with the code base (like extended_type for Forms), I think it should be:

Suggested change
if ($tag['built-in'] ?? false) {
if ($tag['built_in'] ?? false) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HypeMC HypeMC force-pushed the named-serializers branch 2 times, most recently from aee8227 to ca834e5 Compare September 19, 2024 09:01
@fabpot
Copy link
Member

fabpot commented Sep 19, 2024

Thank you @HypeMC.

@fabpot fabpot merged commit ce228d3 into symfony:7.2 Sep 19, 2024
4 of 6 checks passed
@HypeMC HypeMC deleted the named-serializers branch September 19, 2024 09:17
@fabpot fabpot mentioned this pull request Oct 27, 2024
ruudk added a commit to ruudk/symfony that referenced this pull request Jan 27, 2025
Named serializers were introduced in symfony#56823 and they work great.

We noticed a small bug when using custom name convertors.

The MimeMessageNormalizer holds a reference to `serializer.normalizer.property`.
But when using named serializers, it would use the specific child normalizer instead.

With this change, we fix this problem for any service that starts with `serializer.normalizer.`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants