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
I'm getting this error. Plz help.
FatalErrorException:
Error: Call to undefined method JMS\Serializer\Serializer::setVersion()
in ApiVersionListener.php
line 38
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
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.
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?
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.Thanks, I changed it - though it is not my own bundle ;) It was created by Johannes Schmitt.
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 :)
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.