Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Diving into the Normalizer Internals

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When a User object is normalized - whether it's a single User object or a collection of User objects - our new UserNormalizer class is now 100% responsible for that process. Whenever the serializer needs to normalizer a User object, it loops over all of the normalizers and stops when it finds the first one whose supportsNormalization() method returns true. That's now our normalizer.

And... what we're doing is simple: adding a custom group... then, because we autowired the ObjectNormalizer - that's the "main" normalizer Symfony uses to normalize objects - we're calling it and making it do all of the heavy lifting. But we discovered in the last chapter that this isn't quite working like we want: each User record is missing the JSON-LD fields. The embedded cheeseListings data has them... but each user does not.

What's going on?

The Many Core Normalizers

When you work with API Platform, the Symfony serializer has many normalizers. I'll hit Shift+Shift and search for a class called ItemNormalizer. There are a bunch of these - open the one in the JsonLd directory. This is one of those normalizers. And, if you followed its parent class, you would find that it eventually extends ObjectNormalizer. Basically, this class uses the normal functionality of the parent ObjectNormalizer - that's the one that reads data off of our getter methods - but then adds the JSON-LD fields - like @context and @id.

Actually, let's back up even further... and pretend like our custom UserNormalizer doesn't exist. Out-of-the-box, when API Platform normalizes a single User object, many normalizers are used. First, it loops over all of the normalizers and finds the one normalizer that can turn a User object into an array of data. That's normally handled by the JsonLd ItemNormalizer, which reads all the data from the User object and adds a few more JSON-LD specific fields. Once this finishes, the serializer then loops over the individual pieces of data - like the phoneNumber string or the cheeseListings collection - and sends each of these through the normalization process again, asking each normalizer if they support this piece of data until it finds one that does. Or... if none do, it just uses the value as the final value - that happens for simple scalar fields like phoneNumber.

This creates a super powerful system. For example, another normalizer - the DateTimeNormalizer - is responsible for normalizing any DateTime objects... like if you had a createdAt property. It normalizes DateTime objects into a string that can used in JSON or XML.

So each piece of data - from the top-level User down to each property... and even further for related objects - is normalized by exactly one normalizer.

Why is the JSON-LD Info Gone?

Cool! So then... back to our question: why are the JSON-LD fields missing? Well, when we autowired the ObjectNormalizer and then called it... we're not really calling the correct, core normalizer. Nope, instead of asking the serializer to loop over all of the normalizers to find the correct one to use, we accidentally autowired just one specific normalizer and are using it. Basically, instead of using the ItemNormalizer that does all the ObjectNormalizer goodness and then adds the JSON-LD fields, we're using the ObjectNormalizer directly. Hence... we lost those fields!

This is a long way of saying that what we really want to do is modify the $context and then send the User object back through the entire normalization chain again so it can find the core normalizer that's usually responsible for normalizing objects.

How do we do that? It's easy! Um, and... kinda tricky. It involves recursion... well... hopefully avoiding recursion. Now that we understand what's going on, let's fix it next.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
userVoice