Buy

Using a Serializer

We're turning Programmers into JSON by hand inside serializeProgrammer():

158 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 15
class ProgrammerController extends BaseController
{
... lines 18 - 147
private function serializeProgrammer(Programmer $programmer)
{
return array(
'nickname' => $programmer->getNickname(),
'avatarNumber' => $programmer->getAvatarNumber(),
'powerLevel' => $programmer->getPowerLevel(),
'tagLine' => $programmer->getTagLine(),
);
}
}

That's pretty ok with just one resource, but this will be a pain when we have a lot more - especially when resources start having relations to other resources. It'll turn into a whole soap opera. To make this way more fun, we'll use a serializer library: code that's really good at turning objects into an array, or JSON or XML.

The one we'll use is called "JMS Serializer" and there's a bundle for it called JMSSerializerBundle. This is a fanstatic library and incredibly powerful. It can get complex in a few cases, but we'll cover those. You should also know that this library is not maintained all that well anymore and you'll see a little bug that we'll have to work around. But it's been around for years, it's really stable and has a lot of users.

Symfony itself ships with a serializer, Symfony 2.7 has a lot of features that JMS Serializer has. There's a push inside Symfony to make it eventually replace JMS Serialize for most use-cases. So, keep an eye on that. Oh, and JMS Serializer is licensed under Apache2, which is a little bit less permissive than MIT, which is Symfony's license. If that worries you, look into it further.

With all that out of the way, let's get to work. Copy the composer require line and paste it into the terminal:

composer require jms/serializer-bundle

While we're waiting, copy the bundle line and add this into our AppKernel:

40 lines app/AppKernel.php
... lines 1 - 5
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
... lines 11 - 19
new \JMS\SerializerBundle\JMSSerializerBundle(),
);
... lines 22 - 31
return $bundles;
}
... lines 34 - 40

This gives us a new service calld jms_serializer, which can turn any object into JSON or XML. Not unlike a Harry Potter wizarding spell.... accio JSON! So in the controller, rename serializeProgrammer to serialize and make the argument $data, so you can pass it anything. And inside, just return $this->container->get('jms_serializer') and call serialize() on that, passing it $data and json:

151 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 15
class ProgrammerController extends BaseController
{
... lines 18 - 144
private function serialize($data)
{
return $this->container->get('jms_serializer')
->serialize($data, 'json');
}
}

PhpStorm is angry, just because composer hasn't finished downloading yet: we're working ahead.

Find everywhere we used serializeProgrammer() and change those. The only trick is that it's not returning an array anymore, it's returning JSON. So I'll say $json = $this->serialize($programmer). And we can't use JsonResponse anymore, or it'll encode things twice. Create a regular Response instead. Copy this and repeat the same thing in showAction(). Use a normal Response here too:

151 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 21
public function newAction(Request $request)
{
... lines 24 - 33
$json = $this->serialize($programmer);
$response = new Response($json, 201);
... lines 36 - 39
$response->headers->set('Location', $programmerUrl);
return $response;
}
... lines 44 - 48
public function showAction($nickname)
{
... lines 51 - 61
$json = $this->serialize($programmer);
$response = new Response($json, 200);
return $response;
}
... lines 68 - 151

For listAction, life gets easier. Just put the $programmers array inside the $data array and then pass this big structure into the serialize() function:

151 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 72
public function listAction()
{
$programmers = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findAll();
$json = $this->serialize(['programmers' => $programmers]);
$response = new Response($json, 200);
return $response;
}
... lines 84 - 151

The serializer has no problem serializing arrays of things. Make the same changes in updateAction():

151 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 88
public function updateAction($nickname, Request $request)
{
... lines 91 - 108
$json = $this->serialize($programmer);
$response = new Response($json, 200);
return $response;
}
... lines 114 - 151

Great! Let's check on Composer. It's done, so let's try our entire test suite:

phpunit -c app

Ok, things are not going well. One of them says:

Error reading property "avatarNumber" from available keys
(id, nickname, avatar_number, power_level)

The responses on top show the same thing: all our properties are being underscored. The JMS Serializer library does this by default... which I kinda hate. So we're going to turn it off.

The library has something called a "naming strategy" - basically how it transforms property names into JSON or XML keys. You can see some of this inside the bundle's configuration. They have a built-in class for doing nothing: it's called the "identical" naming strategy. Unfortunately, the bundle has a bug that makes this not configurable in the normal way. Instead, we need to go kung-foo on it.

Open up config.yml. I'll paste a big long ugly new parameter here:

79 lines app/config/config.yml
... lines 1 - 5
parameters:
# a hack - should be configurable under jms_serializer, but the property_naming.id
# doesn't seem to be taken into account at all.
jms_serializer.camel_case_naming_strategy.class: JMS\Serializer\Naming\IdenticalPropertyNamingStrategy
... lines 10 - 79

This creates a new parameter called jms_serializer.camel_case_naming_strategy.class. I'm setting this to JMS\Serializer\Naming\IdenticalPropertyNamingStrategy. That is a total hack - I only know to do this because I went deep enough into the bundle to find this. If you want to know how this works, check out our Journey to the Center of Symfony: Dependency Injection screencast: it's good nerdy stuff. The important thing for us is that this will leave our property names alone.

So now if we run the test:

phpunit -c app

we still have failures. But in the dumped response, our property names are back!

Leave a comment!

  • 2017-01-04 weaverryan

    Awesome all around :D. Good luck and thanks for the nice words!

  • 2017-01-03 Greg

    Hey Ryan

    Thank you for your answer, it is exactly what I am thinking. When I tried to use Symfony serializer in this tuto I got back all my entity with the entire user's relation (so included the password). JMSSerializer has a great powerful annotation to handle this problem.
    I saw in the documentation the @Groups annotation for SF Serializer but I don't know yet how it is working ;)
    thanks for the tip about the SF Serializer's configuration.

    Again have a great year for all the KnpUniversity's team and make us like usually worderful tutos
    Cheers

  • 2017-01-02 weaverryan

    Hey Greg!

    Wow - happy new year to you too - and all the same wonderful wishes :).

    This is a GREAT question... and one that I struggle with myself. The JMSSerializer is still more powerful in my opinion than the Symfony serializer, with many more annotations to help you customize things. However, it's also basically abandoned, and much more complex. The Symfony serializer is still under active development and is a very high quality library!

    So, right now, the best option comes down to a case-by-case basis, based on the *developer*. With JMSSerializer, since it has so many annotations, you can usually serialize your entities directly. If you need to tweak the name of a property (e.g. firstName should be "user_first_name" on JSON), that's easy to do. For more junior developers, I think its flexibility through all the annotations gives it a lower barrier to entry (the Symfony serializer *does* however have the @Groups annotation, which handles most cases - but isn't as user-friendly as the @Expose in JMS). For more senior developers, I recommend using the Symfony serializer. The trick is that if you have a JSON representation that looks sufficiently different than your entity, there are no annotations you can use that will help you "tweak" things so that you can serialize your entity directly. Instead, when I use the Symfony serializer, I usually create specific "model" classes and serialize them. For example, instead of serializing a Product entity, I'll create an API\Product class with the exact properties that I want in my JSON. I will then manually create the API\Product class from my Product entity's data and then serialize it. This is more work (and can seem like a lot to some developers). But, it also means that you have a really clean API: you can just open a directory full of simple PHP classes that each perfectly model a resource.

    Phew! I hope that helps! About your code question, it's kind of right... but you're doing too much work! In the Symfony framework, Symfony comes with a pre-configured serializer service (you just need to active it in config.yml). Then you can just $this->get('serializer')->serialize($data, 'json').

    Cheers!

  • 2017-01-01 Greg

    Hey KnpUniversity

    In the first time, Happy new year and all my best wishes for this new year (love, peace and more symfony code :p )
    I have just a little question about the serialization, now with sf 3 is it better to use the Symfony serializer ?
    And if it is yes is it correct to replace this method like that ?


    private function serialize($data)
    {
    $encoders = array(new XmlEncoder(), new JsonEncoder());
    $normalizers = array(new ObjectNormalizer());

    $serializer = new Serializer($normalizers, $encoders);
    return $serializer->serialize($data, 'json');
    }

    Thanks again for your good work.