Disclaimer: this article has been published in 2015. The solution described may be out of date and the current implementation may be different from what is described.
If you have already made a RESTful API with symfony with a lot of clients, you maybe had some trouble changing it. If you remove a field from the API, all the client that use this field will crash.
The solution is to make multiple versions of your API. You can create a v2 and let the v1 as default. Your clients will continue to use the v1 and you will be free to make any change you want in the v2.
This is what we will do in this tutorial. All the sources are available in a github repository. There is one tag by step.
As an initial state to start this tutorial, we have a symfony project withFOSRestBundle and JMSSerializerBundle installed. These are essential bundles to make a RESTful API. If you don't know them, I advice you to read their documentation.
Our Model contains only one entity. This is a very basic ++code>User++/code> entity with 5 attributes : ++code>id++/code>, ++code>username++/code>, ++code>email++/code>, ++code>birthday++/code> and ++code>firstname++/code>. Only 4 attributes are exposed in the API : ++code>id++/code>, ++code>username++/code>, ++code>email++/code> and ++code>birthday++/code>.
++pre class=" language-php">++code class=" language-php">namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* User
* @JMS\ExclusionPolicy("all")
* @ORM\Table()
* @ORM\Entity
*/
class User
{
/**
* @var integer
*
* @JMS\Expose()
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @JMS\Expose()
*
* @ORM\Column(name="username", type="string", length=255)
*/
private $username;
/**
* @var string
*
* @JMS\Expose()
*
* @ORM\Column(name="email", type="string", length=255)
*/
private $email;
/**
* @var \DateTime
*
* @JMS\Expose()
* @JMS\Type("DateTime<'Y-m-d'>")
*
* @ORM\Column(name="birthday", type="datetime")
*/
private $birthday;
/**
* @var string
*
* @ORM\Column(name="firstname", type="string", length=255)
*/
private $firstname;
// getters and setters
}
++/code>++/pre>
We created an API route to get an user by his id. The controller is as simple as possible. We use the ParamConverter to get the user and the ++code>@View++/code> annotation to not to have to generate a response.
++pre class=" language-php">++code class=" language-php">namespace AppBundle\Controller;
use AppBundle\Entity\User;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* @RouteResource("User")
*/
class UserController extends FOSRestController
{
/**
* Get one user.
*
* @Rest\View()
*/
public function getAction(User $user)
{
return $user;
}
}
++/code>++/pre>
Of course the route is tested. I use ++code>LiipFunctionalTestBundle++/code> to generate a new database for each test, and ++code>DoctrineFixturesBundle++/code> to create 2 fake users.
++pre> class=" language-php">++code class=" language-php">namespace AppBundle\Tests\Controller;
use Liip\FunctionalTestBundle\Test\WebTestCase;
use AppBundle\Entity\User;
/**
* Test the user Api routes.
*/
class UserControllerTest extends WebTestCase
{
/**
* Test [GET] /api/user/{id}.
*/
public function testGet()
{
$user = $this->getOneUser();
$client = static::createClient();
$client->request('GET', '/api/users/'.$user->getId());
// Response is OK
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$result = $client->getResponse()->getContent();
$content = json_decode($result, true);
// All key are presents
$keys = array('id', 'username', 'email', 'birthday');
foreach ($keys as $key) {
$this->assertArrayHasKey($key, $content);
}
// There is no extra key
foreach ($content as $key => $value) {
$this->assertContains($key, $keys);
}
// all values are valid
$this->assertEquals($user->getId(), $content['id']);
$this->assertEquals($user->getUsername(), $content['username']);
$this->assertEquals($user->getEmail(), $content['email']);
$this->assertEquals($user->getBirthday()->format('Y-m-d'), $content['birthday']);
}
// Setup and getOneUser methods
}
++/code>++/pre>
Our goal is to remove the ++code>birthday++/code> and add the ++code>firstname++/code> in a next version of the API. At every step, we will check if the test continue to pass.
We want to add the ++code>firstname++/code> field in the API. So let's add the ++code>@JMS\Expose()++/code>annotation.
++pre> class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Entity;
class User
{
/**
* @var string
*
* @JMS\Expose()
*
* @ORM\Column(name="firstname", type="string", length=255)
*/
private $firstname;
}
++/code>++/pre>
Now the attribute is added. But the test doesn't like it. It didn't expected a ++code>firstname++/code> key in response array.
++pre>++code>1) AppBundle\Tests\Controller\UserControllerTest::testGet
Failed asserting that an array contains 'firstname'.
/var/www/src/AppBundle/Tests/Controller/UserControllerTest.php:50
++/code>++/pre>
Fortunately, the ++code>JMSSerializerBundle++/code> have two annotations named ++code>@JMS\Until++/code>and ++code>@JMS\Since++/code> to help us. These annotations allow to expose or not attribute depending of a version number. Let's use its in our ++code>User++/code> class.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Entity;
class User
{
/**
* @var \DateTime
*
* @JMS\Expose()
* @JMS\Type("DateTime<'Y-m-d'>")
* @JMS\Until("1")
*
* @ORM\Column(name="birthday", type="datetime")
*/
private $birthday;
/**
* @var string
*
* @JMS\Expose()
* @JMS\Since("2")
*
* @ORM\Column(name="firstname", type="string", length=255)
*/
private $firstname;
}
++/code>++/pre>
If we specify the version 1 in our controller, the test passes.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Controller
class UserController extends FOSRestController
{
/**
* Get one user.
*
* @Rest\View()
*/
public function getAction(User $user)
{
$view = $this->view($user);
$view->getSerializationContext()->setVersion(1);
return $view;
}
}
++/code>++/pre>
This is nice but we have no way to change the version used.
We will not write the logic to get the version to use in the controller. We will need to get it in all our API controller and maybe in some services. We must be sure we always get the version at the same place in a same way.
So we will create a ++code>VersionGrabber++/code> class. This class will get the version from the ++code>Request++/code> object. To get the ++code>Request++/code>, we will use the ++code>RequestStack++/code> service. If you are in Symfony < 2.4, just add the Request in the ++code>grabVersion++/code> parameters.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Services;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Grab the version form the Request object.
*/
class VersionGrabber
{
const DEFAULT_VERSION = 1;
/**
* @var RequestStack
*/
private $requestStack;
/**
* @param RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* Get the current version used by the API.
*
* @return int
*/
public function grabVersion()
{
return DEFAULT_VERSION;
}
}
++/code>++/pre>++pre class=" language-yaml">++code class=" language-yaml"># app/config/services.yml
services:
version_grabber:
class: "AppBundle\Services\VersionGrabber"
arguments: ["@request_stack"]
++/code>++/pre>
Now, let's complete the ++code>grabVersion++/code> method. There is nothing particularly difficult here. I choose to be able to send the version with a HTTP header or a GET parameter.
++pre class=" language-php">++code class=" language-php">use AppBundle\Exception\VersionNotFoundException;
class VersionGrabber
{
public function grabVersion()
{
try {
return $this->getProvidedVersion();
} catch (VersionNotFoundException $e) {
return self::DEFAULT_VERSION;
}
}
public function getProvidedVersion()
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
throw new VersionNotFoundException();
}
if ($request->headers->has('X-VERSION')) {
return $request->headers->get('X-VERSION');
} elseif ($request->query->has('version')) {
return $request->query->get('version');
}
throw new VersionNotFoundException();
}
}
++/code>++/pre>
After updating the controller, we can check that the test continue to pass.
++pre class=" language-php">++code class=" language-php"> public function getAction(User $user)
{
$view = $this->view($user);
$version = $this->get('version_grabber')->grabVersion();
$view->getSerializationContext()->setVersion($version);
return $view;
}
++/code>++/pre>
To be sure everything is working fine, I made a new test which quite the same as the first one, but for the v2. I will not show you here. But it is available in the repository.
Now we have an API with two different versions. All the changes made can be seen here.
Damn ! I made a mistake. It's not ++code>firstname++/code> but ++code>firstName++/code> (in two words). I changed it and ... the tests failed.
++pre>++code>1) AppBundle\Tests\Controller\UserControllerTest::testGetV2
Failed asserting that an array has the key 'firstname'.
++/code>++/pre>
This is the main problem with this technique. We can't make big changes in our model without affecting the API.
Yeah I can add ++code>@JMS\SerializedName("firstname")++/code>, but what if I want ++code>first_name++/code>in the v3? What if I have a ++code>zipCode++/code> and a ++code>cityName++/code> attributes that I want to move in a ++code>Town++/code> entity? I have to call all the clients which use my API to let them know of the changes?
One solution of this problem is to create representations. Representations are a duplicated model which is used only by the API.
These entities are composed mainly by exposed attribute and are not supposed to be changed in the life cycle of the application.
In computer science, we really don't like duplication. The problem is, when you change a piece of code somewhere, you have to make the same change everywhere the code is duplicated. Here we don't want that the change impact our API. The disadvantages of duplication become a benefit. We are decoupling the API and the model.
So let's create our first representations. One for the v1 and one for the v2.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Representation\V1;
use JMS\Serializer\Annotation as JMS;
/**
* V1 Representation of a User.
*
* @JMS\ExclusionPolicy("none")
*/
class UserRepresentation
{
/**
* @var int
*/
private $id;
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $email;
/**
* @var \DateTime
*
* @JMS\Type("DateTime<'Y-m-d'>")
*/
private $birthday;
// getter and setters
}
++/code>++/pre>++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Representation\V2;
use JMS\Serializer\Annotation as JMS;
/**
* V2 Representation of A user.
*
* @JMS\ExclusionPolicy("none")
*/
class UserRepresentation
{
/**
* @var int
*/
private $id;
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $email;
/**
* First Name in one word !
*
* @var string
*/
private $firstname;
// getter and setters
}
++/code>++/pre>
We just removed all the doctrine annotations and make all the attributes expose by default. We also added a setter for the id.
To be able to use its, we need a bridge between between our model and these representations. To make this bridge, we will use transformers. Transformers are object which create a representation from an Entity and vice versa.
Here the transformer for the v1 representation. The one for the v2 is quite the same.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Transformer\V1;
use AppBundle\Representation\V1\UserRepresentation;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\DataTransformerInterface;
use AppBundle\Entity\User;
use Symfony\Component\Form\Exception\TransformationFailedException;
class UserTransformer implements DataTransformerInterface
{
private $userRepository;
public function __construct(EntityRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* Transform a Model User to its representation.
*
* @param User|null $user
*
* @return UserRepresentation|null
*/
public function transform($user)
{
if (!$user) {
return;
}
$representation = new UserRepresentation();
$representation->setId($user->getId());
$representation->setUsername($user->getUsername());
$representation->setEmail($user->getEmail());
$representation->setBirthday($user->getBirthday());
return $representation;
}
/**
* Get back an user form its representation.
*
* @param UserRepresentation|null $representation
*
* @return User|null
*/
public function reverseTransform($representation)
{
if (!$representation) {
return;
}
if ($representation->getId()) {
$user = $this->userRepository->find($representation->getId());
if (!$user) {
throw new TransformationFailedException('User with the representation id not found');
}
} else {
$user = new User();
}
$user->setEmail($representation->getEmail());
$user->setUsername($representation->getUsername());
$user->setBirthday($representation->getBirthday());
return $user;
}
}
++/code>++/pre>
In this transformer you can put complex logic to hide complex refactoring for your API. This is the only thing that you will have to change during the evolution of your application.
To simply handle the dependency of the transformers, we registered its as services. Now we can use its in the controller and the test pass again.
++pre class=" language-yaml">++code class=" language-yaml"># app/config/services.yml
services:
user_repository:
class: "Doctrine\ORM\EntityRepository"
factory_service: "doctrine.orm.entity_manager"
factory_method: "getRepository"
arguments: ["AppBundle:User"]
user_transformer_v1:
class: "AppBundle\Transformer\V1\UserTransformer"
arguments: ["@user_repository"]
user_transformer_v2:
class: "AppBundle\Transformer\V2\UserTransformer"
arguments: ["@user_repository"]
++/code>++/pre>++pre class=" language-php">++code class=" language-php"> public function getAction(User $user)
{
$version = $this->get('version_grabber')->grabVersion();
if ($version == 1) {
$transformer = $this->get('user_transformer_v1');
} elseif ($version == 2) {
$transformer = $this->get('user_transformer_v2');
} else {
throw new BadVersionException();
}
return $transformer->transform($user);
}
++/code>++/pre>
We have seen how we can reduce the coupling between our model and the API. All the change made can be seen here.
Now we will try to improve the way transformer are loaded.
The way we choose the transformer in the controller is not quite scalable. If we want to add a new version, we have to edit all the place where the transformer are loaded. We should create a Factory.
The role of this factory is to give us the right transformer according to the entity we want to transform and the current version. There is several ways to do it. I will show you how to do with a configuration file. I also implemented another architecture with a ++code>isSupporting++/code> method. You can find it in the ++code>alternative-transformer-factory++/code> branch of the repository.
The factory is a new service which depends on the version grabber and our two transformers. It provide an ++code>createTransformer++/code> method which take a label name for the entity and return the transformer to use according to the current version.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\Factory;
use AppBundle\Exception\TransformerNotFoundException;
use AppBundle\Services\VersionGrabber;
use Symfony\Component\Form\DataTransformerInterface;
/**
* Factory for the representation transformer.
*/
class TransformerFactory
{
/**
* Configuration of the facotory
* @var array
*/
private $config = array(
'user' => array(
'1' => 'user_transformer_v1',
'2' => 'user_transformer_v2',
),
);
/**
* @var VersionGrabber
*/
private $versionGrabber;
/**
* The transformer that the factory can return.
*
* @var array
*/
private $transformers = array();
public function __construct(
VersionGrabber $versionGrabber,
DataTransformerInterface $userV1Transormer,
DataTransformerInterface $userV2Transormer
) {
$this->versionGrabber = $versionGrabber;
$this->transformers['user_transformer_v1'] = $userV1Transormer;
$this->transformers['user_transformer_v2'] = $userV2Transormer;
}
/**
* Get the transformer to use for the entity.
*
* @param mixed $entity
*
* @return DataTransformerInterface
*/
public function createTransformer($entityName)
{
if (! array_key_exists($entityName, $this->config)) {
throw new TransformerNotFoundException('Unknow entity name "'. $entityName.'".');
}
$version = (string) $this->versionGrabber->grabVersion();
if (! array_key_exists($version, $this->config[$entityName])) {
throw new TransformerNotFoundException('Unknow version '.$version.' for the entity name "'. $entityName.'".');
}
$serviceName = $this->config[$entityName][$version];
if (! array_key_exists($serviceName, $this->transformers)) {
throw new TransformerNotFoundException('Service "'.$serviceName.'" not injected.');
}
return $this->transformers[$serviceName];
}
}
++/code>++/pre>++pre class=" language-yaml">++code class=" language-yaml"># app/config/services.yml
services:
transformer_factory:
class: "AppBundle\Factory\TransformerFactory"
arguments: ["@version_grabber", "@user_transformer_v1", "@user_transformer_v2"]
++/code>++/pre>++pre class=" language-php">++code class=" language-php">class UserController extends FOSRestController
{
/**
* Get one user.
*
* @Rest\View()
*/
public function getAction(User $user)
{
$transformer = $this->get('transformer_factory')->createTransformer('user');
return $transformer->transform($user);
}
}
++/code>++/pre>
As you can see, the controller is easier to understand. It will not be changed again, even if you you have to add new versions.
The factory use a ++code>$config++/code> attribute to handle the mapping between the entity name, the transformers and the version. To let the other developers change it without editing the factory code, we can move it in a symfony yaml configuration.
Our goal is now to move our configuration attribute in a yaml file. To be clear on what we want, we will start by creating the file :
++pre class=" language-yaml">++code class=" language-yaml"># app/config/api.yml
app: # this must be the name of your bundle in snake_case
transformer_configuration:
user:
1: user_transformer_v1
2: user_transformer_v2
++/code>++/pre>++pre class=" language-yaml">++code class=" language-yaml"># app/config/config.yml
imports:
- { resource: api.yml }
++/code>++/pre>
We want now inject the content of ++code>transformer_configuration++/code> in our factory:
++pre class=" language-php">++code class=" language-php">class TransformerFactory
{
/**
* Configuration of the facotory
* @var array
*/
private $config;
public function __construct(
array $config,
VersionGrabber $versionGrabber,
DataTransformerInterface $userV1Transormer,
DataTransformerInterface $userV2Transormer
) {
$this->config = $config;
$this->versionGrabber = $versionGrabber;
$this->transformers['user_transformer_v1'] = $userV1Transormer;
$this->transformers['user_transformer_v2'] = $userV2Transormer;
}
}
++/code>++/pre>++pre class=" language-yaml">++code class=" language-yaml"># app/config/services.yml
services:
transformer_factory:
class: "AppBundle\Factory\TransformerFactory"
arguments:
- "%app_tranformer_configuration%"
- "@version_grabber"
- "@user_transformer_v1"
- "@user_transformer_v2"
++/code>++/pre>
The last thing we must do is to set the ++code>app_tranformer_configuration++/code>parameters with the content of our configuration. The first step is to parse the yaml file and valid it. It could be done in the ++code>Configuration++/code> class. To learn more about this class, see the dedicated cookbook article.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files.
*
* To learn more see {@link http://symfony.com/doc/current/components/config/definition.html}
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('app');
$rootNode
->children()
->arrayNode('transformer_configuration')
->requiresAtLeastOneElement()
->prototype('array')
->prototype('scalar')
->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}
++/code>++/pre>
To finish, we must create an extension class in the DependencyInjection folder. This class will call the Configuration class and set the parameter with the parsed configuration. The name of the class must be equal to the bundle name with the Bundle suffix replaced by Extension.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* This is the class that loads and manages your bundle configuration.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class AppExtension extends Extension
{
/**
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('app_tranformer_configuration', $config['transformer_configuration']);
}
}
++/code>++/pre>
That's it. The tests should pass again. To learn more about the custom bundle configuration, there is a nice cookbook article on this subject.
The last improvement that we can make is on the transfomer injections. With our code, we must add a parameter injected in the controller everytime we add a tranformer. There is two problem with that :
A solution to that is to create a tag for service which could be returned by the factory. Let's call it "representation_transformer".
++pre class=" language-yaml">++code class=" language-yaml"># app/config/services.yml
services:
user_transformer_v1:
class: "AppBundle\Transformer\V1\UserTransformer"
arguments: ["@user_repository"]
tags:
- { name: "representation_transformer" }
user_transformer_v2:
class: "AppBundle\Transformer\V2\UserTransformer"
arguments: ["@user_repository"]
tags:
- { name: "representation_transformer" }
++/code>++/pre>
The tagged services will be injected with a method call. This method will be named ++code>addTransformer++/code>.
++pre class=" language-php">++code class=" language-php">class TransformerFactory
{
public function __construct(array $config, VersionGrabber $versionGrabber) {
$this->config = $config;
$this->versionGrabber = $versionGrabber;
}
/**
* Add an available transformer to the factory.
*
* @param DataTransformerInterface $transformer
*/
public function addTransformer($name, DataTransformerInterface $transformer)
{
$this->transformers[$name] = $transformer;
}
}
++/code>++/pre>
To make it works, we must create a compilerPass class. This object must implement the ++code>process++/code> method of the ++code>Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface++/code>. This method will be called during the dependency injection initialization. It's job is to let it know that the ++code>TransfomerFactory++/code> need all the services tagged ++code>representation_transformer++/code> passed in the ++code>addTransformer++/code> method.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Inject the tagged transformer to the factory.
*/
class FactoryTransformerCompilerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->has('transformer_factory')) {
return;
}
$definition = $container->findDefinition('transformer_factory');
$taggedServices = $container->findTaggedServiceIds('representation_transformer');
foreach ($taggedServices as $id => $tags) {
$definition->addMethodCall(
'addTransformer',
array($id, new Reference($id))
);
}
}
}
++/code>++/pre>
We just have to register the compiler pass in the bundle class and everything should works fine.
++pre class=" language-php">++code class=" language-php"><?php
namespace AppBundle;
use AppBundle\DependencyInjection\FactoryTransformerCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new FactoryTransformerCompilerPass());
}
}
++/code>++/pre>
And this is the end. This sections allowed us to see how to create a bundle configuration and how to works with tagged services. As usual, you can see all the changes we made here in the repository.
Finally, we made a smart and scalable way to have a versionned api. With the price of some duplication, we made an api not coupled with the application model. If your model change, adapt your transformers only and everything will be fine.
To add a new version of an entity, we don't have to edit any class any more. The benefit is, you don't have to understand how the existing code works. The only thing you have to do is following this four step (which should be documented somewhere) :
In a future article, we will see how to manage the ++code>POST++/code> and the ++code>PUT++/code> routes with our versionned api.