Using the Symfony Serializer with Doctrine relations — Part 1

Maarten de Boer
Cloudstek
Published in
5 min readFeb 28, 2018
Symfony 4 & Doctrine 2

The Symfony Serializer component is a very powerful and useful component when writing an API. It handles the conversion between your request data (JSON, XML, anything really…) and your classes (usually your entities).

If you're not familiar with the Symfony Serializer component, please read the documentation first.

The reason I wrote this article is because although it's awesome, by default it does not handle relations with existing doctrine entities.

The problem

I was working on an API with Symfony 4 and had quite a struggle with the serializer when deserializing entities with existing relations.

Let's say we have a simple entity BookShelf with contains nothing but an IDand a one-to-many relation with Book. A very simple example with one BookShelf which can have many Books.

For the sake of brevity I won't include the Doctrine mapping here but you can find all about it in their documentation.

Creating the controller

Now say we have an API endpoint where you can submit a new Book and add it to a new BookShelf. Using the Serializer component this is as simple as these few lines:

Creating a book

Now when you send a POST request like this, the serializer transforms our JSON into a new Book but as it's smart, it also creates a new BookShelf and adds it to our book as shelf. For this to work it's important you enable the property_info in your framework settings, see here.

POST request to create a book

So far so good, pretty epic right?

Well now the part which caused me some headaches. Say we have already created a shelf which has ID 3and we want to add a new book to it. To me it made sense you just set shelf property of your book to the ID of the shelf like this:

Unfortunately that leads to this beautiful error message:

The property path constructor needs a string or an instance of “Symfony\Component\PropertyAccess\PropertyPath”. Got: “integer”

As you'll hopefully understand, this error didn't get me very far. Actually changing the shelf value from an integer 3 to a string "3" still gave me the exact same error message. Whaat? yes.

Okay so what if I was wrong and shelf needed to be an object with its id set to the shelf we want to add our book to? Unfortunately that doesn't work either and instead the shelf ID is completely ignored. As you can see below a new shelf is created with an ID of 21 instead of 3 like we told it to.

Why doesn't it work?!

What happens is that the denormalizer stumbles upon the shelf property, finds it in our Book entity and sees it's value should be aBookShelf. In turn it starts denormalizing the value of shelf and expecting the data to represent a BookShelf and thus an object.

Instead of an object we gave it a string or an integer representing the ID of the BookShelf and not an object representing the BookShelf itself, so it freaks out and throws this very -cough- helpful exception.

The serializer checks each registered normalizer until one has been found that that says to support denormalization of the data (in our case 3 and property type (in our case BookShelf), this is done by calling supportsNormalization() on the normalizer. In our case there was no normalizer configured that supported denormalization of our BookShelf other than the ObjectNormalizer.. which misjudged the situation and threw an error later on.

The solution

In order for the serializer to understand what to do with our BookShelf id we need to write our own (de)normalizer. Our normalizer will take our id and look it up in the database. When it's found it'll return our BookShelf, when it's not it'll simply return null.

Now this sounds harder than it is, have a look yourself:

Make sure you register the normalizer as a service and tag it with the serializer.normalizer tag. Otherwise the serializer won't know of its existence and stuff still won't work.

What is this sorcery?

The first step in the process is to deserialize the JSON data into an array so PHP can work with the data. The next step is to denormalize the data into an actual object (our BookShelf). This is where stuff broke and where our EntityNormalizer comes in to fix it.

When the serializer attempts to denormalize data, it calls supportsDenormalization on all registered normalizers in order of priority. The first to say yes gets the job. Since our EntityNormalizer has a priority of 0 (default) and all other normalizers have a much lower priority, ranging from -900 to -1000, it means we get to choose first!

We check in supportsDenormalization() if the property which we're looking to denormalize is in a namespace starting withApp\Entity. Next we also want to make sure the data we're dealing with is either an integer or a string as it must represent a database id.

Actually denormalizing the data is nothing more than to tell Doctrine to go look for our entity by id and returning the result.

Giving it a spin…

Now let's repeat the POST request we did earlier to create a new book and put it on shelf 3. Let's actually repeat it a couple of times..

If all went well you should now see new Book being created and put on BookShelf number 3. Repeat the request and you should see the array of books grow.

Result after two requests. There are now two books on our shelf as you can see.

Thanks for reading my very first Medium article, be sure to give some claps if you enjoyed it. I hope this article saved someone a few sleepless nights.

--

--