This tutorial has a new version, check it out!

Serialization Event Subscriber

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 $10.00

I think the best part of doing API magic in Symfony is the serializer we've been using. We just give it objects - whether those are entities or something else - and it takes care of turning its properties into JSON. And we have control too: by using the exclusion policy and other annotations like @SerializedName that lets us control the JSON key a property becomes.

When does the Serializer Fail?

Heck, we can even add virtual properties! Just add a function inside your class, add the @VirtualProperty() annotation above it... and bam! You now have another field in your JSON response that's not actually a property on the class. That's great! It handles 100% of what we need! Right... right?

Ah, ok: there's still this last, nasty 1% of use-cases where virtual property won't work. Why? Well, imagine you want to include the URL to the programmer in its JSON representation. To make that URL, you need the router service. But can you access services from within a method in Programmer? No! We're in trouble!

This is usually where I get really mad and say "Never mind, I'm not using the stupid serializer anymore!" Then I stomp off to my bedroom to play video games.

But come on, we can definitely overcome this. In fact, there are two ways. The more interesting is with an event subscriber on the serializer.

Creating a Serializer Event Subscriber

In AppBundle, create a new directory called Serializer and put a fancy new class inside called LinkSerializationSubscriber. Set the namespace to AppBundle\Serializer:

... lines 1 - 2
namespace AppBundle\Serializer;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
class LinkSerializationSubscriber implements EventSubscriberInterface
{
... lines 9 - 11
}

To create a subscriber with the JMSSerializer, you need to implement EventSubscriberInterface... and make sure you choose the one from JMS\Serializer. There's also a core interface that, unfortunately, has the exact same name.

In PhpStorm, I'll open the Generate shortcut and select "Implement Methods". This will tell me all the methods that the interface requires. And, it's just one: getSubscribedEvents:

... lines 1 - 6
class LinkSerializationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
}
}

Stop: here's the goal. Whenever we serialize something, there are a few events we can hook into to customize that process. In this method, we'll tell the serializer exactly which events we want to hook into. One of those will allow us to add a new field... which will be the URL to whatever Programmer is being serialized.

Return an array with another array inside: we'll need a few keys here. The first is event - the event name we need to hook into. There are two for serialization: serializer.pre_serialize and serializer.post_serialize:

... lines 1 - 8
class LinkSerializationSubscriber implements EventSubscriberInterface
{
... lines 11 - 17
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.post_serialize',
... lines 23 - 25
)
);
}
}

We need the second because it lets you change the data that's being turned into JSON.

Add a method key and set it to onPostSerialize - we'll create that in a second:

... lines 1 - 21
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
... lines 24 - 30

Next, add format set to JSON:

... lines 1 - 21
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'format' => 'json',
... lines 25 - 30

This means the method will only be called when we're serializing into JSON... which is fine - that's all our API does.

Finally, add a class key set to AppBundle\Entity\Programmer:

... lines 1 - 21
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'format' => 'json',
'class' => 'AppBundle\Entity\Programmer'
... lines 26 - 30

This says, "Hey! Only call onPostSerialize for Programmer classes!".

Adding a Custom Serialized Field

Setup, done! Create that public function onPostSerialize(). Just like with core Symfony events, you'll be passed an event argument, which in this case is an instance of ObjectEvent:

... lines 1 - 8
class LinkSerializationSubscriber implements EventSubscriberInterface
{
public function onPostSerialize(ObjectEvent $event)
{
... lines 13 - 15
}
... lines 17 - 28
}

Now, we can start messing with the serialization process.

Before we go any further, go back to our test. The goal is for each Programmer to have a new field that is a link to itself. In testGETProgrammer(), add a new assert that checks that we have a uri property that's equal to /api/programmers/UnitTester:

... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
... lines 8 - 35
public function testGETProgrammer()
{
... lines 38 - 51
$this->asserter()->assertResponsePropertyEquals($response, 'uri', '/api/programmers/UnitTester');
}
... lines 54 - 227
}

Ok, let's see how we can use the fancy subscriber to add this field automatically.

Leave a comment!

This tutorial uses an older version of Symfony. The concepts of REST and serialization are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*", // 0.13.0
        "white-october/pagerfanta-bundle": "^1.0" // v1.2.4
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}