Symfony2 & JMSSerializerBundle: Vendor MIME types and API versioning
Matthias Noback
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