-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[JsonEncoder][Serializer] Introducing the component #51718
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
Conversation
src/Symfony/Component/Serializer/Deserialize/Config/DeserializeConfig.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/DataModel/CollectionNode.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Splitter/SplitterInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Serialize/Mapping/PropertyMetadataLoaderInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Serialize/Mapping/PropertyMetadata.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Serialize/DataModel/DataModelBuilder.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Template/TemplateGeneratorInterface.php
Outdated
Show resolved
Hide resolved
What a job ! thanks for the work on this, the serializer really needs some love. I look through most of the implementation design it's really well done with nice interface layers. I think there is too many extension point for a start, maybe some of them are not needed ? But we should provide an integration / extension example with api platform to have feedback on those extensions point. Are you not afraid about the generated code implementation on maintenance burden, i think there a 2 ways on this, use this implementation or use the php-parser library but i'm not sure which one is better in terms of maintenance ? Really hope this get accepted into symfony |
is the planned changes also affect the Normalizer / Denormalizer part of the Serializer? when using Symfony HTTPClient, it already decodes the data for me into Array, Or depending on the HTTPClient (like Guzzle?) i could use the Data from a Response Stream? |
Many thanks @joelwurtz! I truly think as well that this component deserves more love!
Yes, I can agree! I began by exposing as many extension points as I could, as it's easier to reduce them instead of adding them. But we need to define which extension point is relevant for a start.
Indeed, it'll reduce the added code a lot (and the maintenance burden), but at the same time complicate template generation and PHP AST optimization code as right now nodes are designed for these purposes. |
@Hanmac, yes, it'll affect that part. Indeed, the performance improvement relies on somehow moving the normalization/denormalization step to cache (by the computing data shape only once) For your specific use case, you can leverage the response's |
@mtarld my problem there: I use the nomalizer part to read from an API Like for Products,
My normalizer notices that the Sector part is incomplete, so it automatically calls To make the normalizer access, the current Client call, I have it access the client via Context Would that still be possible? |
I think they have a very strong BC policy. Rector, Phpstan, Psalm are built in top of it |
src/Symfony/Component/Serializer/Deserialize/Template/Template.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/DependencyInjection/SerializablePass.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer_experimental.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Instantiator/EagerInstantiator.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Instantiator/LazyInstantiator.php
Outdated
Show resolved
Hide resolved
@Hanmac, I think that this is the way that denormalizers should not be used. Indeed, normalizers are meant to turn objects to array and vice versa, nothing more. Here, doing HTTP calls on the fly, or querying a database for example, will imply be a big lack of separation of concerns. The process of retrieving the actual data behind a URL must live in a custom service of yours. |
src/Symfony/Component/Serializer/Attribute/SerializeFormatter.php
Outdated
Show resolved
Hide resolved
For work, this is one of the APIs i was going to map: when load 100+ of Product with one query, i want them to load the "ModelReference" as well, like for example their sectors or ticketDefinition. i implemented a logic in the Denormalizer to load as less objects as possible, for example:
|
Wow that's quite a big work, congrats @mtarld . That being said, for what it worth I have to admit I'm on the edge with this proposal as of now 😅. The perf showcase definitely makes it look awesome, but here's a few user's POV thoughts coming to my mind:
About the why, it seems there are 2 clear intentions: improving performance, and easing design/maintenance/debugging.
Why? Imho the serializer should not be intended to work with specific objects. Doc states that
Does that mean that if my app does not use generics, I might end up with bad generated templates? Or is it just better if I use them, i.e. no drawback in not using them compared to the current implementation?
It is missing quite a big BC break here imo, dropping support for normalizers/denormalizers, while it is probably one of the most used extension points of the current implementation. Futhermore, normalizers/denormalizers are also used to be injected on their own, when you don't need to go through the full serialization. What is the suggested replacement? Could you showcase a simple normalizer/denormalizer and what it would become, would be great?
That's what normalizers/denormalizers are advertised for 😅
I agree on the general note that there are too many extension points, but on the other hand it is not super clear yet where they all kick in or could be useful. Maybe if you can showcase the workflow as simply as possible with all relevant extension points, could give a better clue. Hopefully these are constructive enough questions/thoughts 😇, and congrats again for the huge work! Looking forward to see what this PR becomes! |
@mtarld just a quick heads-up that the last paragraph ( |
Great. Thank you for this. Really impressed with the work you have done. Can you please give me a simple usage example without the framework? I would like to make sure I set things up properly before I rerun my own performance tests. |
To support @n-valverde's point, I wonder if this PR shouldn't be a new component of its own. This is a better and more powerful alternative to It could be the The current Serializer has a lot of features that will be hard to keep in your implementation (especially with a fully functional BC layer), and supporting as many features and formats as the Serializer will likely "bloat" the new code. Also, there are hundreds of projects that use the Serializer, and forcing all of them to migrate will be hard. In most (JSON) cases, it will be possible to use the JsonEncoder component instead of the Serializer, and maybe at some point, we'll be able to feature-freeze it, and then deprecate it. But I doubt that we'll manage to do that anytime soon. Anyway, I can't wait to get this PR merged into Symfony! |
src/Symfony/Component/Serializer/DependencyInjection/RuntimeSerializerServicesPass.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerTemplateCacheWarmer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Attribute/DeserializeFormatter.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Attribute/SerializeFormatter.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Template/EagerTemplateGenerator.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Deserialize/Template/LazyTemplateGenerator.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Serialize/Template/JsonTemplateGenerator.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonEncoder/Mapping/Decode/AttributePropertyMetadataLoader.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the fact that most classes are tagged as internal
.
Great work here, I'm not a big fan of the ::create()
methods but I can see how they are useful, maybe something to revisit at some point.
Thank you @mtarld. |
This PR was merged into the 7.3 branch. Discussion ---------- [FrameworkBundle][JsonEncoder] Wire services | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Follow-up of * #51718 The FrameworkBundle part of the JsonEncoder component introduction. The component related config is quite simple: ```yaml framework: json_encoder: paths: App\EncodableDto\: '../src/EncodableDto/*' ``` Plus, the framework integration proposes the following bindings: - `EncoderInterface $jsonEncoder` to the `json_encoder.encoder` service - `DecoderInterface $jsonDecoder` to the `json_encoder.decoder` service --- As this PR is based on top of #51718, only the last commit should be considered. Commits ------- e213884 [FrameworkBundle] [JsonEncoder] Wire services
This PR introduces new component:
JsonStreamer
(initially namedJsonEncoder
- and has been renamed in #59863)Note
This is the continuation of a Serializer revamp trial, the previous PR description is available here.
Why?
The
Serializer
component is a library designed to normalize PHP data structures in raw associative arrays, and then encode them in a large variety of formats, which offers a high degree of flexibility.However, that flexibility has some drawbacks:
getSupportedTypes()
.Plus, that degree of flexibility isn't that often needed. Indeed, there are many use cases where we use the Serializer component to serialize data without intensive modification (IE: without custom normalization). And in these cases, the flexibility degrades a lot of performances for nothing.
That's why this PR introduces the
JsonStreamer
component, which is focused on performance to address the above use case for the specific JSON format. The DNA of that component is to be a fast and modern JSON parser and streaming encoder. It fixes many issues of the nativejson_encode
andjson_decode
PHP functions: streaming, on-demand parsing, generics handling, ability to create strongly typed objects instead of raw associative arrays in one pass, etc.We can see the difference between the Serializer component and the JsonStreamer component like the difference between Doctrine ORM and Doctrine DBAL.
Indeed, the DBAL can be considered as a sub-layer of ORM, and when precise and performance-related stuff is needed, developers will skip the ORM layer to deal with the DBAL one directly.
And it's the very same difference between the Serializer and the JsonStreamer, when precise and performance-related stuff is needed, developers will skip the normalization layer, by fine-tuning the data mapping in their userland and deal with the encoding layer directly.
API
Contrary to the
Symfony\Component\Serializer\SerializerInterface
, which has two methodsserialize
anddeserialize
, the new design will instead introduce four new interfaces.These compose the main part of the available API.
As you can notice, there is no
$format
parameter.It is indeed logical because a streamer knows how to deal with only one format.
Usage example
Install the component
Configure PHP attributes:
Add the proper value transformers:
Then use the stream reader/writer:
Main ideas
Cache
The main trick to improve performance is to rely on the cache.
During cache warm-up (or on the fly once and for all), the data structure is computed and used to generate a cache PHP file that we could name "template".
Then, the generated template is called with the actual data to deal with encoding/decoding.
Template generation is the main costly thing. And because the template is computed and written once, only the template execution will be done all the other times, which implies lightning speed!
Here is the workflow during runtime:
By the way, because it is intended to work mostly with DTOs, it'll work well with an automapping tool.
Stream
To improve memory usage, encoding, and decoding are relying on generators.
In that way, the whole JSON string will never be at once in memory.
Here is for example a simple encoding template PHP file:
Configuration and context
Contrary to the actual
Serializer
implementation, a difference has been made between "configuration" and "context".Performance showcase
With all these ideas, performance has been greatly improved.
When serializing 10k objects to JSON, it is about 10 times faster than the existing, and can even be compared to the

json_encode
native function.And it consumes about 2 times less memory.

When deserializing a JSON file to a list of 50k objects, iterating one the 9999 firsts and reading the 10000th eagerly, it is more than 10 times faster than the legacy deserialization, and can even be compared to the

json_decode
native function!In terms of memory consumption, the new implementation is comparable to the existing one when reading eagerly.
And when reading lazily, it consumes about 10 times less memory!

And it doesn't stop there, @dunglas is working on a PHP extension compatible with that new version of the component leveraging simdjson to make JSON serialization/deserialization even faster.
These improvements will benefit several big projects such as Drupal, Sylius, and API Platform (some integration tests already have been made for this).
It'll also benefit many other tiny projects as many are dealing with serialization.
The code of the used benchmark can be found here.
Extension points
PropertyMetadataLoaderInterface
The
Symfony\Component\JsonStreamer\{Read,Write}\Stream{Reader,Writer}Generator
calls aSymfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface
to retrieve object's properties, with their name, their type, and their formatters.Therefore, it is possible to decorate (or replace) the
Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface
.In that way, it'll be possible for example to read extra custom PHP attributes, ignore specific object's properties, and rename every properties, ...
As an example, in the component, there are:
PropertyMetadataLoader
which reads basic properties information.AttributePropertyMetadataLoader
which reads properties attributes such asEncodedName
,EncodeFormatter
,DecodeFormatter
, andMaxDepth
to ignore, rename or add formatters on the already retrieved properties.GenericTypePropertyMetadataLoader
which updates properties' types according to generics, and cast date-times to strings.DateTimeTypePropertyMetadataLoader
which updates properties' types to cast date-times to strings and vice-versa.For example, you can hide sensitive data of sensitive classes and a sensitive marker: