Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Lucky you! You found an early release chapter - it will be fully polished and published shortly!

Custom Normalizer

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

Copy the test method - testOwnerCanSeeIsPublishedField. We just added some magic so that admin users can see the isPublished property. This method tests for our next mission: that owners or a DragonTreasure can also see this.

Run it with:

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField

And... it fails; expected null to be the same as false, because the field isn't showing up at all.

To fix this, over in DragonTreasure, I'm going to add a third special group called owner:read.

Can you see where we're going with this? If we are the owner of a DragonTreasure, we will add this group, and then the field will get included. However, this is tricky. As we talked about in the last video, normalization groups start static: they live up here in our config. The context builder allows us to make these groups dynamic per request. So, if we're an admin user, we can add an extra admin:read group, which will be used when serializing every object for this entire request.

But in this situation, we need to make the group dynamic per object. Imagine if we're returning 10 DragonTreasures. The user may only own one of them. So only that one DragonTreasure should have this extra group.

The Job of Normalizers

To handle this level of control, we need a custom normalizer. Normalizers are core to Symfony's serializer and they're responsible for turning a piece of data - like an ApiResource object or a DateTime object that lives on a property - into a scalar value or an array. By creating a custom normalizer, you can do pretty much any weird thing you want to with your data.

Actually, find your terminal and run:

php  bin/console debug:container --tag=serializer.normalizer

I love this: this shows us every single normalizer in our app! You can see stuff tat's responsible for normalizing UUIDs. This is responsible for normalizing any of our ApiResource objects to JSON-LD, here's one for a DateTime.... there's a ton of interesting stuff.

Our goal is to create our own normalizer, decorate an existing core normalizer, but then add the dynamic group before that core normalizer is called.

Creating the Normalizer Class

So let's get to work! Over in src/ - it doesn't really matter how we organize things - I'm going to create a new directory called Normalizer. Let me collapse a few things so it's easier to look at. Inside that, add a new class called, how about, AddOwnerGroupsNormalizer. All normalizers must implement NormalizerInterface... then go to Code -> Generate or Command + N on a Mac and select "Implement Methods" to add the two we need.

Here's how this works: as soon as we implement NormalizerInterface, anytime any piece of data is being normalized, it will call our supportsNormalization() method. there, we can decide whether or not we know how to normalize that thing. If we return true, the serializer will then call normalize(), pass us that data, and then we return the normalized version.

And actually, to avoid some deprecation errors, let me pop open the parent class. The return type is this crazy array thingy. Copy that... and add it as the retur type. You don't have to do this - everything would work without it - but you'd get a deprecation warning in your tests.

Down for supportsNormalization(), in the Symfony 7, there will be an array $context argument... and the method will return a bool.

Which Service do We Decorate?

Before we fill this in or set up decoration, we need to think about which core service we're going to decorate. Here's my idea: if we replace the main core normalizer service with this class, we could add the group then call the decorated normalizer... so that everything works like it usually does, except that it uses the extra group.

Back at the terminal, run

bin/console debug:container normalizer

We get back a bunch of results. That makes sense: there's a main normalizer, but then the normalizer itself has lots of other normalizers inside of it to handle different types of data. So... where is the top level normalizer? It's actually not even in this list: it called serializer. Though, as we'll see next, even that isn't quite right.

Leave a comment!

Login or Register to join the conversation
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0