Skip to content

[Serializer] Add ChainNormalizer and ChainDenormalizer #52900

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

Open
wants to merge 10 commits into
base: 7.3
Choose a base branch
from

Conversation

Nyholm
Copy link
Member

@Nyholm Nyholm commented Dec 5, 2023

Q A
Branch? 7.1
Bug fix? no
New feature? yes
Deprecations? yes
Issues
License MIT

This PR is less of a feature and more refactoring.
Our Serializer currently implements 5 interfaces. You may serialize and normalize (and reverse). There are also a lot of logic to figure out context, error messages and what normalizer to use.

This PR suggest to separate responsibilities a bit by splitting the normalizing and denormalizing into ChainNormalizer and ChainDenormalizer.

I found this work when I was creating custom denormalizers. A low level class like MyBlogDenormalizer should not have a dependency on the high level Serializer. It would make more sense to use a ChainDenormalizer. (Yes, there is still an ugly circular reference).

I've marked SerializerAwareInterface as deprecated for this reason. Currently, SerializerAwareInterface only works if you combine it with NormalizerInterface, DenormalizerInterface or EncoderInterface. I deprecate it because one should use NormalizerAwareInterface instead.

@Nyholm Nyholm requested a review from dunglas as a code owner December 5, 2023 13:36
@carsonbot carsonbot added this to the 7.1 milestone Dec 5, 2023
@Nyholm Nyholm force-pushed the normalizer-aggregator branch from 16e5ed6 to 2bee4b6 Compare December 5, 2023 13:41
@carsonbot carsonbot changed the title Add Normalizer and Denormalizer aggregators [Serializer] Add Normalizer and Denormalizer aggregators Dec 5, 2023
@Nyholm Nyholm changed the title [Serializer] Add Normalizer and Denormalizer aggregators [Serializer] Add ChainNormalizer and ChainDenormalizer Dec 5, 2023
@Nyholm
Copy link
Member Author

Nyholm commented Dec 5, 2023

Hm.. Im considering renaming this to NormalizerAggregate instead. Similar to CacheWarmerAggregate.

EDIT:
Changed my mind. We do have ChainEncoder and ChainDecoder at the moment. Using "Chain" is good here.

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.

I really think that's a good idea to have a better separation between normalization and serialization, thanks for pushing it forward!

@Nyholm Nyholm force-pushed the normalizer-aggregator branch from 03f4233 to 72b85a5 Compare December 11, 2023 10:40
@Nyholm Nyholm requested a review from yceruto as a code owner December 11, 2023 11:07
@Nyholm Nyholm requested review from chalasr and mtarld December 11, 2023 11:11
@Nyholm
Copy link
Member Author

Nyholm commented Dec 11, 2023

Wohoo. All tests are passing.

While working on this PR I found an issues like #52992. Ie some code assume a certain behaviour without any contract.
One example that we have in 7.0 right now is that you can pass $context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS] to Serializer::denormalize() and never get a PartialDenormalizationException because you have used the wrong normalizers.

This is something we should fix and this PR is a tiny step towards that direction.

@Nyholm Nyholm force-pushed the normalizer-aggregator branch 3 times, most recently from a36a11a to 1846b99 Compare January 9, 2024 11:48
@Nyholm
Copy link
Member Author

Nyholm commented Jan 9, 2024

PR is rebased and all tests are green again.

@Nyholm Nyholm force-pushed the normalizer-aggregator branch 2 times, most recently from 5e913c5 to 98bdcb8 Compare January 31, 2024 20:14
@Nyholm
Copy link
Member Author

Nyholm commented Jan 31, 2024

PR is rebased =)

@Nyholm Nyholm force-pushed the normalizer-aggregator branch from 8910244 to d212a7d Compare February 12, 2024 02:03
@Nyholm
Copy link
Member Author

Nyholm commented Feb 12, 2024

@mtarld and @chalasr Can you give another review on this please?

@@ -59,7 +59,7 @@
"symfony/scheduler": "^6.4.4|^7.0.4",
"symfony/security-bundle": "^6.4|^7.0",
"symfony/semaphore": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0",
"symfony/serializer": "^7.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that bump really needed? AFAIK, we try to avoid as much as possible to bump dependencies (see #53160 (comment))

(same for other composer.json files)

Copy link
Member

Choose a reason for hiding this comment

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

FrameworkBundle often bumps the min version instead of trying to support multiple versions of the service definitions, for simplicity.
This is different for other package, which can be reused outside the fullstack framework and for which supporting compatibility with 2 different major versions of Symfony reduces the risk of dependency hell in those projects (the lower level the package is, the more useful this is).

Copy link
Member Author

@Nyholm Nyholm Jun 19, 2024

Choose a reason for hiding this comment

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

Thank you.

I've bumped version on FrameworkBundle and HttpKernel. I have reverted the other packages and make sure they (still) support both 6.4 and 7.2.

@@ -80,6 +82,9 @@

class SerializerTest extends TestCase
{
/**
* @group legacy
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that we can add the non-legacy version as well

@xabbuh xabbuh removed this from the 7.1 milestone May 15, 2024
@xabbuh xabbuh added this to the 7.2 milestone May 15, 2024
@dunglas
Copy link
Member

dunglas commented Jun 17, 2024

It's a good step forward. I'm +1 for the idea.

@Nyholm Nyholm force-pushed the normalizer-aggregator branch 2 times, most recently from d18dd6d to 7fe79f3 Compare June 19, 2024 11:27
@Nyholm Nyholm force-pushed the normalizer-aggregator branch from ee04567 to 5e56886 Compare June 19, 2024 15:58
Copy link
Member

@yceruto yceruto left a comment

Choose a reason for hiding this comment

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

Nice refactoring! I left minor comments only.

/**
* @param array<NormalizerInterface|DenormalizerInterface> $normalizers
* @param array<EncoderInterface|DecoderInterface> $encoders
*/
public function __construct(
private array $normalizers = [],
array $normalizers = [],
Copy link
Member

Choose a reason for hiding this comment

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

defining the new signature of the constructor as being new Serializer([], $encoders, $chainNormalizer, $chainDenormalizer) looks weird to me. It don't think the array as first argument should be kept in the new signature

Copy link
Member

Choose a reason for hiding this comment

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

alternatively, we could keep the current constructor signature, making the ChainNormalizer and ChainDenormalizer internal implementation details, as done for the ChainEncoder and ChainDecoder (for which we classify them as being encoders, decoders or both inside the constructor). As most implementations are implementing both interfaces in the same class, it allows passing a single list in instantiation, making the configuration easier

Btw, this would mean that the chain classes would not need to implement the *aware interfaces (as the injection would be handled by the Serializer on each registered class)

Copy link
Member Author

@Nyholm Nyholm Jun 20, 2024

Choose a reason for hiding this comment

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

Yeah. I dont like the current signature too.

I want to have a signature like:

new Serializer($encoders, $normalizer, $denormalizer);
// or
new Serializer($normalizer, $denormalizer, $encoders);

Should I immediately go to that signature or should we change it in SF8?


On your separate topic of making the Chain* classes an implementation detail.. That was an earlier version of the PR, but we decided against that to make the Serializer more simple. See #52900 (comment)

Treating them like $encoders is a valid idea. However, you have 1-3 encoders MAX. But you may have 100+ normalizers/denormalizers. So I think it makes more sense to keep them separate and give the developer more control.

Copy link
Member

Choose a reason for hiding this comment

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

Should I immediately go to that signature or should we change it in SF8?

You must support the new signature immediately. Otherwise doing the change in 8.0 will be forbidden as it would be a BC break without migration path.

On your separate topic of making the Chain* classes an implementation detail.. That was an earlier version of the PR, but we decided against that to make the Serializer more simple. See #52900 (comment)

That comment was actually suggesting to also change the way encoders are injected (we should avoid making 2 successive signature changes on the constructor, as that would be a pain to have 2 deprecated signatures to support in the BC layer)

Treating them like $encoders is a valid idea. However, you have 1-3 encoders MAX. But you may have 100+ normalizers/denormalizers. So I think it makes more sense to keep them separate and give the developer more control.

but then, you make it more likely to have nested ChainNormalizer instance, making it necessary to be 100% sure about the reliability of getSupportedTypes for instance (my suggested return ['*' => false]` will be reliable, even though it might not be the most optimized one).

}

$class = $definition->getClass();
if (null === $class) {
Copy link
Member

Choose a reason for hiding this comment

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

this logic looks wrong to me. As this compiler pass is registered as a before-optimization pass, the definition class might not be known yet as a class name:

  • when using ChildDefinition, it can be null (if the class will be inherited from the parent), but this does not mean that skipping the registration of the service is the valid processing (btw, this is a BC break in case someone uses a ChildDefinition to configure a normalizer today)
  • if a service definition uses a parameter in the class name (which is supported since 2.0), the parameter is not yet resolved at that point, requiring to resolve it explicitly

public function addNormalizer(NormalizerInterface $normalizer): void
{
if ($normalizer instanceof NormalizerAwareInterface) {
$normalizer->setNormalizer($this);
Copy link
Member

Choose a reason for hiding this comment

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

if we make the ChainNormalizer available publicly, this is flawed as it breaks when nesting a ChainNormalizer inside another ChainNormalizer (the normalizers inside the inner ChainNormalizer won't get access to the main normalizer supporting everything)

Copy link
Member Author

Choose a reason for hiding this comment

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

Would a solution be to have ChainNormalizer also implement the NormalizerAwareInterface?

@fabpot fabpot modified the milestones: 7.2, 7.3 Nov 20, 2024
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.

9 participants