The Symfony Serializer: a great, but complex component

Hi everyone, since the beginning of the year, i’ve been working on a proof of concept based on PHP Symfony 4 and a lot of JS frameworks. I’ve already wrote some articles about this work. But there is something i missed a lot: working with the symfony serializer.

If you have read my previous articles you should have notice that my work is based on a simple but explicit model. Like the ApiPlatform project, my model has book and reviews. But it also has a lot more. ApiPlatform samples only use One to Many sample, so it doesn’t explain a lot of use cases. So i added some Many to Many relations with authors and editors !

Simple library model

During the project i created a PHP component to rebuild PHP object from a JSON string. It was able to restore the complete model from the book to the list of authors with their Job. And because i liked it i built it around Symfony ParamConverter to get automatic restoration and validation for required routes.

It was nice and quite easy to build. But indeed, i was unsatisfied because i knew that the Symfony Serializer should do quite the same thing. So i investigate and play a lot with this component (thank’s to Xdebug). I asked some people to explain me how to perform those operations with this component. And very often, the answer was that i shoud do custom Normalizer… :-( still unsatisfied. This component should do what i need !

I looked at thoses slides: https://speakerdeck.com/dunglas/mastering-the-symfony-serializer
And i understood more thing about the component. One of them is that i could rely on the entities comments to make everything work. Then i was sure that i could do everything without custom normalizer. I don’t say that custom Normalizers are useless, i just say that i may not need it. That’s not the same thing.

So i wrote unit tests to play again. Test with simple book, then a book with a serie, then a book with a collection of series (yes it’s not in the original model but i need to understand how it works).

The first thing i noticed in the slides on peackerdeck, is that the following code is really, really important:

// https://speakerdeck.com/dunglas/mastering-the-symfony-serializer?slide=77
use Doctrine\Common\Annotations\AnnotationReader;
use Rebolon\Tests\Fixtures\Entity\Book;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$classMetaDataFactory = new ClassMetadataFactory(
new AnnotationLoader(
new AnnotationReader()
)
);
$objectNormalizer = new ObjectNormalizer($classMetaDataFactory, null, null, new PhpDocExtractor());
$serializer = new Serializer([
new ArrayDenormalizer(),
$objectNormalizer,
], [
new JsonEncoder(),
]);

This will give a huge power to the Serializer: it will be able to read annotation and PHPDoc. Then it will use it to normalize and deserialize the string. Have a look at the order of the used normalizers: ArrayDenormalizer is before ObjectNormalizer. This is really important.

Then i notice another point: the main JSON node must not contains the key of the main entity. That was my first issue. When i send a book, i encapsulate it inside a book node. I was wrong, you don’t need this root node.

// BAD
$bookBad = <<<JSON
{
"book": {
"title": "Zombies in western culture"
}
}
JSON;
// GOOD
$bookGood = <<<JSON
{
"title": "Zombies in western culture"
}
JSON;

The second thing is that i had to set right comments into Entities. Don’t forget to use the @var annotation.
But, when you work with Doctrine you will have ArrayCollection for properties implies in relationships. And that was a complicated part. First, i used only @var ArrayCollection` but it always throw this kind of Exception: `Symfony\Component\Serializer\Exception\NotNormalizableValueException : The property path constructor needs a string or an instance of “Symfony\Component\PropertyAccess\PropertyPath”. Got: “integer”`
Then i was completely lost in Symfony Space ;-)
And then i remember this slide from Kevin Dunglas talk: https://speakerdeck.com/dunglas/mastering-the-symfony-serializer?slide=70
Look at the red circle with this string: sptrinf('%s[]', Conference::class) 
And then i wondered if the Serializer would understand this kind of comment, so i give it a try:

class Book
{
/**
*
@var Serie[]
*/
private $series;
}

Oh yeah ! it works well.

Hey, but it’s not cool. Now in my IDE, i don’t have auto completion on ArrayCollection, because indeed, $series is an ArrayCollection of Serie.
And the solution is: just pipe the comment like this:

class Book
{
/**
*
@var Serie[] | ArrayCollection
*/
private $series;
}

And now you have the best of both world !

But that’s not the end. Using XDebug, i understood how the Serializer (in fact, the Normalizer) set the items from a collection into an entity.
A lot of things happens in the PropertyAccessor::writeProperty methods. I won’t explain all the mecanism, but shortly: if you have methods addXXX and removeXXX, then addXXX will be used in priority. Take care ! if you only have the addXXX method, then it won’t be used… I don’t know why they did this but there is certainly a good reason. 
The second options is the setter. If it exists (and you don’t have removeXXX or addXXX), then it will be used to add the items from the collections.

Now i hope you understand a bit more how the fantastic Serializer component works.

Here is the project where you will find samples: https://github.com/Rebolon/SymfonySerializerSamples

Thank’s for reading

Like what you read? Give Benjamin RICHARD a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.