Skip to content

[Serializer] Add NormalizedValueInterface that can be returned by NormalizerInterface::normalize to enable receiving a value object for normalized values. #43498

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 3 commits into
base: 6.0
Choose a base branch
from

Conversation

bbrala
Copy link

@bbrala bbrala commented Oct 14, 2021

Q A
Branch? 6.0
Bug fix? no
New feature? yes
Deprecations? no
License MIT
Doc PR symfony/symfony-docs#...

I've built this PR to see if we can make the migration to Symfony 6 for Drupal a little less painfull. This also enables some new handy feature for the serializer where you could for example create a Cache dependency tree while serializing a tree of objects.

This PR is a result of the discussion in #43021.

Drupal serialization

Drupal uses de serializer component for a few things. One of these things is serializing our entity objects to something that can be returned as json. The entity objects contain fields which can have one or more values, so we start high up and normalize all the way to the bottom of the stack, the field values.

Creating sensible cache metadata using a value object

In order to get the combined cache tag information from every level of serialization we've chosen to track that in the normalizer. It knows what it did and we track this in a normalized value object (called CachableNormalization). This means we can consolidate the proper caching dependency tree information of the object without having to do some weird gymnastics to track this information somewhere else.

It would help us loads if we would have some sort of NormalizedValueInterface that can be returned by the normalize method. This would enable us to get things working as intented and keep a less complex cache-tracking system in place. An interface like this would work for us quite handily.

…NormalizerInterface::normalize` to enable receiving a value object for normalized values.
@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 5.4 branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

…me the method `getNormalization` to `getValue`
@bbrala
Copy link
Author

bbrala commented Oct 14, 2021

The errors by psalm are about missing return type on some of the serializer classes. I was thinking they shouldn't by default return a NormalizedValue since that would be a BC break. This should be an kind of opt-in feature imo.

I wonder what your thoughts are on that @dunglas :)

@@ -161,6 +162,10 @@ public function normalize(mixed $data, string $format = null, array $context = [
return $normalizer->normalize($data, $format, $context);
}

if ($data instanceof NormalizedValueInterface) {
Copy link
Member

Choose a reason for hiding this comment

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

In my opinion the serializer class should return the wrapper object too.

We have a use case in API Platform: we'll be able to return an object containing the serialized data and some additional metadata. We need to be able to access the metadata from the outside.

Copy link
Author

Choose a reason for hiding this comment

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

Awesome to hear there is an reason to have this in api platform. I will have a look later today too see how that might work.

I didn't initially since i thought that would be a bigger bc break and if you want to access the data you could just have your own serializer.

I do see your point though

@derrabus
Copy link
Member

If this is a new feature, this PR must target 5.4.

@derrabus
Copy link
Member

I'm really scratching my head over this. I mainly fail to understand why caching would be the concern of a serialization layer.

What we're basically doing here is fundamentally changing the contract of normalizers. Be aware that the return value of a normalizer is not only consumed by the serializer but also by other normalizers that currently do not expect this new type of return type.

@gabesullice
Copy link

I mainly fail to understand why caching would be the concern of a serialization layer.
I agree that it is unintuitive and that returning an object breaks one's understanding of what a normalizer does. However, the question is "where else?"

Sorry for this long explanation below. Maybe it's an indication we're overthinking things :P


Consider an object representing a bank account. It has a balance property represented by a Balance object. Only the bank tellers and customers are supposed to see the balance value and customer support staff should not. When that account is serialized into a representation to be transferred to the user, the balance should be omitted from one version of the serialization and included in another.

Drupal's object model is quite complex and its serializations are costly to compute. Therefore, we cache them (much like one would cache a bit of rendered HTML). To safely cache them, we need to know under what contexts the serialization is valid for a particular user (i.e. something like the user's role: teller, customer, or support) so that we can compute a cache key (e.g. accountId:42;user.role:teller)

Which object should be concerned with access to the balance value? We decided it should be the Balance object's responsibility. If the Balance object implements NormalizableInterface and calling its normalize method returns null sometimes and 42 other times, then this conditionality has to be captured somewhere and it needs to "bubble" up to the code that called normalize() on its parent bank account object in order to incorporate that nuance into the cache entry.

We could have some sort of "pre-normalization" step in which we compute a tree of value objects that carry cache information before passing that tree into the Symfony serializer to be normalized to keep the serialization abstraction pure, but it seems a little silly since the Symfony serializer is already traversing a tree to begin with.

@stof
Copy link
Member

stof commented Oct 14, 2021

@gabesullice to me, a solution to do that in the current architecture of the Serializer component is to make the BalanceNormalizer responsible for that logic instead, deciding how to normalize the Balance object based what it gets (whether the user is a reseller or no can be available in the context, or be something that the BalanceNormalizer gets from elsewhere, but using the context argument might solve the bubbling issue).

Btw, to me, this PR cannot be complete as is. One of the consumers of the normalized data are encoders, that are left unchanged in that PR and so don't handle your NormalizedValue.

@bbrala
Copy link
Author

bbrala commented Oct 14, 2021

(whether the user is a reseller or no can be available in the context, or be something that the BalanceNormalizer gets from elsewhere, but using the context argument might solve the bubbling issue).

@stof Would you mean by context, just passing context by reference perhaps and keeping track there?

Btw, to me, this PR cannot be complete as is. One of the consumers of the normalized data are encoders, that are left unchanged in that PR and so don't handle your NormalizedValue.

Yes, this is true, but the discussion on this is very valuable. I wouldn't mind going through the whole motion of getting this PR all done, but only if we can agree on this is probably "a good idea" (TM) :)

@dunglas you mention that API platform would benifit from this in an earlier comment. Perhaps you could ellaborate a little further on that?

@dunglas
Copy link
Member

dunglas commented Oct 14, 2021

For instance, we need to keep track of the IRIs of all serialized resources. Currently we indeed use a hack involving a context in passed by reference. This works but its a bit hacky.

@nicolas-grekas
Copy link
Member

Currently we indeed use a hack involving a context in passed by reference

Wouldn't using a state object in the context work instead?

@fabpot fabpot modified the milestones: 6.0, 6.1 Nov 21, 2021
@fabpot fabpot modified the milestones: 6.1, 6.2 May 20, 2022
@nicolas-grekas nicolas-grekas modified the milestones: 6.2, 6.3 Nov 5, 2022
@nicolas-grekas nicolas-grekas modified the milestones: 6.3, 6.4 May 23, 2023
@nicolas-grekas nicolas-grekas modified the milestones: 6.4, 7.1 Nov 15, 2023
@xabbuh xabbuh removed this from the 7.1 milestone May 15, 2024
@xabbuh xabbuh added this to the 7.2 milestone May 15, 2024
@fabpot fabpot modified the milestones: 7.2, 7.3 Nov 20, 2024
@fabpot fabpot modified the milestones: 7.3, 7.4 May 26, 2025
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