The JMSSerializerBundle has a VersionExclusionStrategy, which allows you to serialize/deserialize objects for a specific version of your API. You can mark the properties that are available for different versions using the @Since and @Until annotations:

use JMS\SerializerBundle\Annotation\Type;
use JMS\SerializerBundle\Annotation\Since;
use JMS\SerializerBundle\Annotation\Until;

class Comment
{
    /**
     * @Type("DateTime")
     * @Since("1.2.0")
     */
    private $createdAt;

    /**
     * @Type("DateTime")
     * @Until("2.1.3")
     */
    private $updatedAt;
}

The only thing you have to do is tell the serializer which version to use, before you start using it:

$this->get('serializer')->setVersion('2.0.2');

Vendor MIME types

Many webservices allow clients to request data for a specific version of the API, by sending the prefered version number as part of the Accept header of the request, like

Accept: application/vnd.matthias-v2.0.1+xml

It would be nice if we could extract this version number from the Accept header and immediately parse it to the serializer, by calling it's setVersion() method.

The ApiVersionListener

We can accomplish this in a few steps. The right moment to look at the request headers and make some preparations before any controller gets called, is when the event kernel.request is fired. We should listen to that event, inspect the Accept header, extract the requested version from the MIME type and finally call setVersion() on the serializer:


namespace Matthias\RestBundle\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use JMS\SerializerBundle\Serializer\Serializer; class ApiVersionListener { private $serializer; public function setSerializer(Serializer $serializer) { $this->serializer = $serializer; } public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $acceptedMimeType = $request->headers->get('Accept'); // look for: application/vnd.matthias-v{version}+xml $versionAndFormat = str_replace('application/vnd.matthias', '', $acceptedMimeType); if (preg_match('/(\-v[0-9\.]+)?\+xml/', $versionAndFormat, $matches)) { $version = str_replace('-v', '', $matches[1]); $this->serializer->setVersion($version); } } }

Next, add a service for the listener and make sure the serializer from the JMSSerializerBundle will be set by calling setSerializer().

<?xml version="1.0" ?>
<container xmlns="https://symfony.com/schema/dic/services" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="matthias_rest.api_version_listener" class="Matthias\RestBundle\EventListener\ApiVersionListener">
            <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
            <call method="setSerializer">
                <argument type="service" id="serializer" />
            </call>
        </service>
    </services>
</container>

And we're done!

Suggestions

Though this is still quite a simple implementation, we would soon want to enhance the ApiVersionListener a bit. You might want to store the version as a request attribute (for instance "_api_version"), for later reference. Or make the vendor name configurable. Or provide a default version that should be used. It would also be nice (and not difficult) to support one other common and even more elegant way of specifying versions in MIME types:

Accept: application/vnd.matthias+xml; version=2.0.1
PHP Symfony2 annotations events request serializer service container
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Charith Mahawatta

I'm getting this error. Plz help.

FatalErrorException:
Error: Call to undefined method JMS\Serializer\Serializer::setVersion()
in ApiVersionListener.php
line 38

Chris Jones

The problem I've been having is how to route to different controllers based on the version.
ie. if we pass version=1 in the accept headers, go to Controller/V1/UserController etc

Matthias Noback

Right, the assumption here is that only the serialized data is different between versions. I've always been inclined to make new functionality available for all the older versions. Since extra functionality will not pose any compatibility problems for existing clients of the web API, this should not be problematic.

Chris Jones

So you are saying that essentially you provide only extra endpoints, and in existing endpoints you provide the extra functionality based on checking the version number of th API call?

The problem with that is that as time goes by your controllers are going to bloat with legacy code, with lots of switches for version numbers. It'd be much neater to keep legacy controllers old API version calls can get routed to, whilst having neat, clean new controllers for new versions, don't you think?

Or have I misunderstood your post?

Matthias Noback

That's what I meant, though I understand your concerns here. As for the switches: those are no good indeed.

The version switch concerning the serialized data is entirely handled by the JMSSerializer. In your controllers, there should be no version switches. If your API calls have side-effects which are different for different versions you should let some kind of factory object create a handler for the call which is specific for the current request, so inside the controller:

[php]
public function createAccountAction(Request $request)
{
$handler = $this->getCreateAccountHandlerFacrory()->createAccountHandlerFor($request);
return $handler->handle($request);
}
[/php]

Inside the factory you can check for the API version (which should be stored as a request attribute by some kernel.request event listener) and modify the behavior of the handler, or return a different handler for different versions.

Matthias Noback

Thanks, I changed it - though it is not my own bundle ;) It was created by Johannes Schmitt.

Renoir Boulanger

Hello. Just a small note. You have a HTML error syntax on your link to the GitHub page of your bundle.

It reads:

<a href="https://github.com/schmittj... target="

Nice work :)

Lukas

Nice, I want to add this kind of stuff to FOSRestBundle itself. But I also want people to automatically choose the controller based on the version too. For this to work I need to expand the Routing layer to also do the content type negotiation. However I got side tracked with all the work I have to put into the CMF.