Easy Api Versioning in Symfony2 Using Accept Header

2015/10/20

This article will show how to use Symfony2 framework to version an api using the Accept Header, the expression language, an event listener and routing conditions.

In order to see the theory behind this, please read this excellent article: The ultimate solution versioning rest api: Content Negotiation

Step 1: Create the ApiVersionListener

ApiVersionListener listens to kernel.request event and uses Symfony2 AcceptHeader class to extract the version attribute from the Accept header. If a version is found, then the version value is set as a request attribute, so that it can be used later on routing conditions.

The Accept header should have the following format in order for the ApiVersionListener to work:

Accept: application/vnd.custom.api+json;version=1
<?php
// src/AppBundle/EventListener/ApiVersionListener.php

namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\AcceptHeader;

class ApiVersionListener
{

    /**
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
            return;
        }

        $request = $event->getRequest();

        $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept'))->get('application/vnd.custom.api+json');

        if (!is_null($acceptHeader)) {
            $version = $acceptHeader->getAttribute('version');
            $request->attributes->set('version', $version);
        }
        
    }
}

Step 2: Register ApiVersionListener as a service

Put the following in AppBundle/Resources/config/services.yml:

kernel.listener.api_version_listener:
    class: AppBundle\EventListener\ApiVersionListener
    tags:
        - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 100 }

The most important thing in the above configuration is the priority. We need to make sure that this listener is triggered before the Router listener (before the route is matched). As you can see in kernel.request documentation, Router listener has priority 32, so if we set priority 100 (higher priority is triggered first) for ApiVersionListener we will not face any problems.

Step 3: Routing magic

Now using the route conditions and the expression language we can do the following:

get_customer_v1:
    path:     /api/customer/{id}
    defaults: { _controller: AppBundle:CustomerV1:GetClient }
    condition: "request.attributes.get('version') == 1"

get_customer_v2:
    path:     /api/customer/{id}
    defaults: { _controller: AppBundle:CustomerV2:GetClient }
    condition: "request.attributes.get('version') == 2"

so if the request includes the following header

Accept: application/vnd.custom.api+json;version=1

then AppBundle:CustomerV1Controller with handle it.

If it includes

Accept: application/vnd.custom.api+json;version=2

it will be handled by AppBundle:CustomerV2Controller, otherwise the router will not match a route and a 404 response will be returned.

That’s it. The only thing I do not know is if it possible to force the router to respond with HTTP Error 406 (Not Acceptable) instead of 404. Any ideas?

Also please let me know if you know any other, more efficient way to version an api using the Accept Header.

Thanks for reading

Additional resources used:

Categories: posts Tags: php symfony api versioning