Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer] Change visibility of Serializer::getNormalizer and ::getDenormalizer from private to protected #52084

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

Closed
wants to merge 3 commits into from

Conversation

bradjones1
Copy link
Contributor

@bradjones1 bradjones1 commented Oct 16, 2023

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

This changes the visibility of Serializer::getNormalizer and ::getDenormalizer to public protected. Changing to public was proposed years ago in #24206 and subsequently closed, however the initial reasons for doing so were different and eventually resulted in performance improvements, instead - see #27049 and #27105.

That last change, #27105 ("Add ->hasCacheableSupportsMethod() to CacheableSupportsMethodInterface") actually paved the way for the functionality that I am working on which would depend on these methods being public more accessible to downstream code.

Over at Drupal, we are working on improving our out-of-the-box schema generation capabilities. This has resulted in a new SchematicNormalizerInterface which can be implemented by normalizers to express a JSON Schema for the resulting normalization. The method signature for the schema generation is identical to ::normalize(). (We plan to expand this concept to denormalization in the future, e.g. for different specs based on HTTP verb.)

Currently, NormalizerInterface only allows for normalization, but doesn't provide any visibility into what normalizer is used. We need to be able to take a supported/representative object and ask the normalizer what the resulting schema will be. That's only possible if we can have some visibility into the normalizer to be used.

Alternatively, it would be really amazing to perhaps add SchematicNormalizerInterface to Symfony, however I couldn't find anything in the issue queue demonstrating any existing demand for this. It would also add a bit of technical debt to the Serializer component that I'm not sure belongs in Symfony. If we did add it here, then we could instead add methods like ::getNormalizationSchema() to the serializer instead of changing the visibility.

As it stands, though, here is a PR to do the minimum change required to support our innovation downstream.

@nicolas-grekas closed the original PR for changing visibility citing a desire to generally reduce the public surface area (understandably) but mostly because the underlying performance reasons for doing so were to be handled in a different issue. I haven't seen anything in the prior issues to indicate that this change is bad for any other reasons. I do understand that this might result in some shenanigans by people directly retrieving the normalizer, however I can't really think of a situation in which that's like, a major problem either.

@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "6.4" but it seems your PR description refers to branch "6.4 for features".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot

@bradjones1 bradjones1 force-pushed the serializer-visibility branch from 0886863 to cedfa9c Compare October 16, 2023 21:25
@bradjones1
Copy link
Contributor Author

Let's compromise and make these protected? Serializer is not final so you can extend and get what you need there?

@bradjones1
Copy link
Contributor Author

Test failures look unrelated?

@dunglas
Copy link
Member

dunglas commented Oct 17, 2023

You may be interested in existing API Platform packages allowing to generate JSON Schema and adding support for extra normalization formats to the serializer such as JSON:API, JSON-LD and HAL: https://packagist.org/packages/api-platform/

Maybe can we collaborate on this topic?

(We managed to do that without changing the visibility).

@bradjones1
Copy link
Contributor Author

bradjones1 commented Oct 17, 2023

HI @dunglas, thanks so much for your comment and offer to explore this in a broader context. I took a fresh look at API Platform and really like what the project does to solve some really common use cases.

I think the Drupal implementation is different enough, though, that I'm not able to follow the pattern you have in SchemaFactory::buildSchema(). The main difference being that api-platform builds schema for classes that are (AFAICT, not an expert) more-or-less directly exposed over the public API. While Drupal does represent its entities in classes, the fields are not necessarily properties of the class and are loaded through an internal API. The field definitions are also dynamic in that they can be essentially subclassed through "bundles" that can be, but generally aren't, subclassed. Reflection doesn't tell you anything about the fields exposed on the API.

I'm sure the api-platform implementation is more complex than just "give me schema for X class's public properties" but in Drupal it's more like "give me the schema for the various normalizers that will be called for its fields, that can't be statically analyzed purely from the class structure."

Or perhaps more succinctly, api-platform's schema generation is invoked in a similar way to what we need for Drupal, however when it gets to

foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
  // Uses reflection
  $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options);
  ...
  $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format);
  ...
}

we have to do something like (pseudocode)

foreach ($entityFieldManager->getFieldsForEntity($entity) as $field) {
  // Can't use reflection here.
  $schema = $serializer->normalizer->getNormalizer($field->dataClass)->getSchema();
}

It would be amazing to be able to re-use some existing code from elsewhere in the PHP ecosystem, however Drupal's ORM (field, database and entity APIs) are home grown so we can't for instance borrow from Doctrine.

So in the Drupal pseudocode above, the catch is being able to do ->getNormalizer() so we can introspect its schema.

@stof
Copy link
Member

stof commented Oct 17, 2023

In any case, this is too late to land this in 6.4 or 7.0 (the feature freeze was 2 weeks ago, so we are only merging PRs that were already in progress at that time and for which we decided to include their review in the initial part of the feature freeze period).
As the feature freeze for 7.1 is in 5.5 months, this gives the time to identify whether the API-Platform architecture could inspire a Drupal architecture that does not need to extend the public API.

@stof
Copy link
Member

stof commented Oct 17, 2023

Side note: wouldn't the suggested Drupal architecture require every normalizer to implement the Drupal interface, forcing to re-implement all core normalizers of Symfony as well ?

@bradjones1 bradjones1 changed the title [Serializer] Change visibility of Serializer::getNormalizer and ::getDenormalizer [Serializer] Change visibility of Serializer::getNormalizer and ::getDenormalizer from private to protected Oct 17, 2023
@bradjones1
Copy link
Contributor Author

Thanks @stof for your help and input. Good to know about the schedule. This isn't likely to land in Drupal core for a few months at least - still lots to hash out - so while Drupal 10 is currently on Symfony 6 I don't think it's a huge lift if this component might be required at 7.1.

...time to identify whether the API-Platform architecture could inspire a Drupal architecture that does not need to extend the public API.

I'm curious about Kévin's take on my feedback above, however I am pretty confident that api-platform's approach won't work here. To be clear though, I've updated this from my initial thought that it needed to be public to protected instead - we can subclass the serializer and make it public ourselves, but it need not be so in Symfony.

Side note: wouldn't the suggested Drupal architecture require every normalizer to implement the Drupal interface, forcing to re-implement all core normalizers of Symfony as well?

Most of Drupal's normalizers are custom, honestly. We have a "Typed Data" API which provides value objects for field properties, and those are (IIRC) 100% covered by entirely custom normalizers. I don't even think we're using any of the core Symfony normalizers at all. But if we were/someone is, the calling code definitely has to check that the retrieved normalizer implements our new SchematicNormalizerInterface, use reflection, or just output a default. However because of our entity and field API, most user custom field types rely on these core typed data primitives, so they will likely not need to change anything. That's an implementation detail though, I don't think it really impacts whether these methods can be changed to protected. But good question and hopefully that answers it.

@derrabus
Copy link
Member

I'm not convinced yet that exposing the normalizer resolution is the best solution to your problem. Your normalizers could for instance be wrapped (see TraceableNormalizer) which would break all your instanceof checks.

Can we explore possible alternatives? For instance, your normalizers could react to the $format parameter and emit a JSON schema when being called with $format === 'json_schema'.

Either way, consider taking a look at the extensions API Platform provides for the serializer. This very much sounds like the kind of issue that they've solved already.

@bradjones1
Copy link
Contributor Author

Thanks, @derrabus. It sounds like consensus around changing the visibility on these is not going to materialize, so I appreciate the suggestion of using an alternative $format for the normalizer to get a schema. I didn't know about TraceableNormalizer but that makes sense.

I am still struggling to find a real analog to the challenge we're facing in Drupal to API Platform's structure. API Platform is great but as I tried to outline above it seems like our data models are sufficiently different that I haven't yet found a pattern that would translate. You mention extensions to the serializer but I haven't yet found something that fits that description from my analysis of the code. Would you be so kind as to point me to an example that might apply here?

@soyuka
Copy link
Contributor

soyuka commented Oct 19, 2023

Hi @bradjones1 you wrote

foreach ($entityFieldManager->getFieldsForEntity($entity) as $field) {
  // Can't use reflection here.
  $schema = $serializer->normalizer->getNormalizer($field->dataClass)->getSchema();
}

As you saw in API Platform, we're using Metadata Factories to build our schemas. It's definitely possible to create metadata based on your field manager no? We're not using reflection in most of these. Then you would just rely on the SchemaFactory to create schemas. Could you provide more details about this (but probably through another channel like the Symfony slack, or by mail)? Thanks.

@bradjones1
Copy link
Contributor Author

Hi @soyuka, yes, I'm already refactoring based on the feedback here and the overwhelming consensus that this change won't be accepted. I appreciate everyone's help. Likely will be pursuing an approach where the resolved normalizer will get passed a $format parameter of json_schema or something along those lines.

@bradjones1 bradjones1 closed this Oct 19, 2023
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.

6 participants