A "Normalizer Aware" Normalizer

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 being normalized, our UserNormalizer is called. After adding a dynamic group to $context, we want to send the User object back through the full normalizer system: we want the original, "core" normalizer - whatever class that might be - to do its magic. Right now, we're not quite accomplishing that because we're directly calling a specific normalizer - ObjectNormalizer.

So... how can we call the "normalizer chain" instead of this specific normalizer? By implementing a new interface: NormalizerAwareInterface.

... lines 1 - 6
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
... lines 8 - 10
class UserNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface
... lines 12 - 46

This requires us to have a single new method setNormalizer(). When the serializer system see that a normalizer has this interface, before it uses that normalizer, it will call setNormalizer() and pass a normalizer object... that really holds the "chain" of all normalizers. It, sort of, passes us the top-level normalizer - the one that is responsible for looping over all of the normalizers to find the correct one to use for this data.

So, we won't autowire ObjectNormalizer anymore: remove the constructor and that property. Instead, use NormalizerAwareTrait. That trait is just a shortcut: it has a setNormalizer() method and it stores the normalizer on a protected property.

... lines 1 - 7
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
... lines 9 - 10
class UserNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
... lines 14 - 44
}

The end result is that $this->normalizer is now the main, normalizer chain. We add the group, then pass User back through the original process.

Tip

Please, make sure you have Xdebug PHP extension installed and enabled, otherwise the page may just hang if you try to execute this code and you will need to restart your built-in web server or PHP-FPM.

And... some of you may already see a problem with this. When we hit Execute to try this... it runs for a few seconds, then... error!

Maximum function nesting level of 256 reached, aborting!

Yay! Recursion! We're changing the $context and then calling normalize() on the original normalizer chain. Well... guess what that does? It once again calls supportsNormalization() on this object, we return true, and it calls normalize() on us again. We're basically calling ourselves over and over and over again.

Wah, wah. Hmm. We only want to be called once per object being normalized. What we need is some sort of a flag... something that says that we've already been called so we can avoid calling ourselves again.

Avoiding Recursion with a $context Flag

The way to do this is by adding a flag to the $context itself. Then, down in supportsNormalization(), if we see that flag, it means that we've already been called and we can return false.

But wait... the $context isn't passed to supportsNormalization()... so that's a problem. Well... not a big problem - we can get that by tweaking our interface. Remove NormalizerInterface and replace it with ContextAwareNormalizerInterface. We can do that because it extends NormalizerInterface. The only difference is that this interface requires our method to have one extra argument: array $context.

... lines 1 - 6
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
... lines 8 - 10
class UserNormalizer implements ContextAwareNormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface
{
... lines 13 - 34
public function supportsNormalization($data, $format = null, array $context = [])
{
... lines 37 - 42
}
... lines 44 - 53
}

For the flag, let's add a constant: private const ALREADY_CALLED set to, how about, USER_NORMALIZER_ALREADY_CALLED. Now, in normalize(), right before calling the original chain, set this: $context[self::ALREADY_CALLED] = true. Finally, in supportsNormalization(), if isset($context[self::ALREADY_CALLED]), return false. That will allow the "normal" normalizer to be used on the second call.

... lines 1 - 10
class UserNormalizer implements ContextAwareNormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface
{
... lines 13 - 14
private const ALREADY_CALLED = 'USER_NORMALIZER_ALREADY_CALLED';
... lines 16 - 19
public function normalize($object, $format = null, array $context = array()): array
{
... lines 22 - 25
$context[self::ALREADY_CALLED] = true;
... lines 27 - 32
}
... line 34
public function supportsNormalization($data, $format = null, array $context = [])
{
// avoid recursion: only call once per object
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
return $data instanceof User;
}
... lines 44 - 53
}

We've done it! We've fixed the recursion! Let's celebrate by hitting Execute and... more recursion!

Removing hasCacheableSupportsMethod()

Because... we're missing one subtle detail. Find the hasCacheableSupportsMethod() method - this was generated for us - and return false:

... lines 1 - 49
public function hasCacheableSupportsMethod(): bool
{
return false;
}
... lines 54 - 55

Go back up, hit Execute and... it works! The phoneNumber field is still randomly included... because we have some random logic in our normalizer... but the @id and @type JSON-LD stuff is back!

The hasCacheableSupportsMethod() is an optional method that each normalizer can have... which relates to this optional interface - CacheableSupportsMethodInterface. The purpose of this interface - and the method - is performance.

Because every piece of the object graph is normalized, the serializer calls supportsNormalization()... a lot of times. If your supportsNormalization() method only relies on the $format and the class of $data - basically $data instanceof User, then you can return true from hasCacheableSupportsMethod(). When you do this, the serializer will only call supportsNormalization() once per class. That speeds things up.

But as soon as you rely on the $data itself or the $context, you need to return false... or remove this interface entirely, both have the same result. This forces the serializer to call our supportNormalization() method each time the chain is called. That does have a performance impact, but as long as your logic is fast, it should be minor. And most importantly, it fixes our issue!

Next, let's add the proper security logic to our class and then investigate another superpower of normalizers: the ability to add completely custom fields. We'll add a strange... but potentially useful boolean field to User called isMe.

Leave a comment!

  • 2020-01-30 hanene

    Vladimir Sadicov I resolved when I clearing the cache for the dev environment :))))

  • 2020-01-29 hanene

    Vladimir Sadicov the code looks allright there must be an issue somewhere else :(((

  • 2020-01-29 Vladimir Sadicov

    hm this look weird! Why there is so many groups?

  • 2020-01-29 hanene

    Vladimir Sadicov
    Error
    *in \src/Serializer/Normalizer/UserNormalizer.php (line 37)
    private function userIsOwner(User $user): bool
    *UserNormalizer->userIsOwner(object(User))
    UserNormalizer->normalize(object(User), 'jsonld', array('groups' => array('owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read', 'owner:read',......

  • 2020-01-29 Vladimir Sadicov

    Have you passed all chapter and put every line of code at needed place? Looks like you have some object that doesn't stops recursive normalization. Can you show exact error, and your normalizer class?

    Cheers!

  • 2020-01-29 hanene

    Hi , I solve the cheeselisting & user Repository error but what about this error message "Maximum function nesting level of '500' reached, aborting!" after I increased the maximum nesting level of function !!

  • 2020-01-28 Vladimir Sadicov

    Hey @henene

    Sounds like some bundle or symfony version was updated, have you tried to change type-hint, as described in error?

    Cheers

  • 2020-01-27 hanene

    Hello Victor , I got the same error .. I set the xdebug.max_nesting_level up to 500 in php .ini and restart all services when I m running the command composer update Im getting error in my cheeselistingRepository :
    Cannot autowire service "App\Repository\CheeseListingRepository": argument "$registry" of method "__construct()" references interface "Symfony\
    Bridge\Doctrine\RegistryInterface" but no such service exists. Try changing the type-hint to "Doctrine\Persistence\ManagerRegistry" instead.

    I get Xdebug working with PhpStorm..
    in php .ini the Xdebug section:
    [Xdebug]
    zend_extension="c:\xampp\php\ext\php_xdebug-2.9.1-7.3-vc15.dll"
    xdebug.remote_enable=1
    xdebug.remote_port=9000
    xdebug.max_nesting_level=500

  • 2019-11-29 Victor Bocharsky

    Hey AymDev,

    Oh, sorry for this again! Seems it's more heavy than I thought :/ Yeah, that exact "Maximum function nesting level of 256 reached, aborting!" message is coming from Xdebug, IIRC the directive for this is "xdebug.max_nesting_level=256", so you can control it in your PHP ini config but only when Xdebug extension is installed :)

    Cheers!

  • 2019-11-29 AymDev

    Oh, right ! I don't have Xdebug installed...
    Well, maybe restarting php-fpm does the trick but the computer completely froze, I had to do an evil manual reboot. Thank you for the advices !

  • 2019-11-29 Victor Bocharsky

    Hey AymDev,

    Wooops, we are sorry about that! :) Yeah, without Xdebug installed it might hang :p Btw, in theory you can just restart your php-fpm and it should do the trick. We probably need to add a warning if you don't have Xdebug installed :)

    Cheers!

  • 2019-11-26 AymDev

    My computer did not stop at 256 recursion levels, I just restarted it. Thanks, team ! 😂