Sunday, April 8, 2012

Setting up a Symfony2 REST service with FOSRestBundle

Installation

First thing is to download and setup the FOSRestBundle. If you are running Symfony 2.0.x, get the FOSRestBundle version 0.6, otherwise, download the code in the master branch. The FOSRestBundle depends on the JMSSerializerBundle so the following instructions are going to include some extra information for setting up this bundle to complete our REST service.

First thing is to install our dependencies. Add the following to your deps file:

[metadata]
    git=http://github.com/schmittjoh/metadata.git
    version=1.1.0

[JMSSerializerBundle]
    git=git://github.com/schmittjoh/JMSSerializerBundle.git
    target=bundles/JMS/SerializerBundle

[FOSRest]
    git=git://github.com/FriendsOfSymfony/FOSRest.git
    target=fos/FOS/Rest

[FOSRestBundle]
    git=git://github.com/FriendsOfSymfony/FOSRestBundle.git
    target=bundles/FOS/RestBundle
    version=origin/0.6

Run the vendors script to install these bundles:

php bin/vendors install

Next, update your app/autoload.php file with the following lines:

$loader-&ght;registerNamespaces(array(

    'FOS\\Rest' => __DIR__.'/../vendor/fos',
    'FOS'       => __DIR__.'/../vendor/bundles',
    'JMS'              => __DIR__.'/../vendor/bundles',
    'Metadata'         => __DIR__.'/../vendor/metadata/src',

));

Register the new bundle in your app/AppKernel.php file:

public function registerBundles()
{
    $bundles = array(

        new JMS\SerializerBundle\JMSSerializerBundle($this),
        new FOS\RestBundle\FOSRestBundle(),

    );
}

Next, add the following configuration to your app/config.yml file:

fos_rest:
    view:
        view_response_listener: false
        failed_validation: HTTP_BAD_REQUEST
        default_engine: php
        formats:
            json: true
    format_listener:
        prefer_extension: true
    body_listener:
        decoders:
            json: fos_rest.decoder.json

A couple of words about the configuration section above. First of all, there are two formatting options called formats and templating_formats. Any format listed under the templating_formats option, requires a template for rendering. I am not going to utilize this method. Instead, I will use the JMSSerializerBundle by simply utilizing the formats option as illustrated above.

In regards to the view_response_listener option, this configuration option allows you to return a FOSRest View object directly from your controller like below:

$view = View::create();
$view->setData($data);

return $view;

This is bit of an invasive approach so I will set it to false and stick to using the handler method as illustrated below:

$view = View::create()
$view->setData($data);

return $this->get('fos_rest.view_handler')->handle($view);

However, if you decide to use the view listener approach, you will have to disable template annotations, or you will get the following error message:

RuntimeException: You need to disable the view annotations in SensioFrameworkExtraBundle when using the FOSRestBundle View Response listener.

To address this problem, you need to set the view annotations flag under sensio_framework_extra section in your app/config.yml to false.

sensio_framework_extra:
    view:
        annotations: false

If you have the AsseticBundle installed, setting prefer_extension to false will cause your css and js files to be returned from the server with application/json headers effectively breaking your UI. To prevent this you can either set prefer_extension to false and use .json or .xml URL prefixes when making REST calls or simply update the default_priorities flag with additional formats. See this link to see the solution for this issue. In this case, I will stick with the extension approach.

A full configuration example can be found here.

Finally, if you ever get a "Fatal error: Interface 'Doctrine\Common\Persistence\Proxy' not found" error, you need to upgrade your Doctrine packages from 2.1 to 2.2. Add the following to your deps file:

[doctrine-common]
    git=http://github.com/doctrine/common.git
    version=2.2.1

[doctrine-dbal]
    git=http://github.com/doctrine/dbal.git
    version=2.2.1

[doctrine]
    git=http://github.com/doctrine/doctrine2.git
    version=2.2.1

REST Service Setup

The first step is to create our REST controller. I will be exposing a User class that defines a user profile in application. Here is my REST controller with a single action that exposes a user profile:

namespace Acme\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\View\View;

class UserRestController extends Controller
{
    /**
     * GET /users/{userId}
     * 
     * @param string $userId
     * @return Response
     */
    public function getUserAction($userId)
    {
        $user = $this->get('security.context')->getToken()->getUser();
        
        if ($userId == $user->getId()) {
            
        }
        
        $view = View::create()
          ->setStatusCode(200)
          ->setData($user);

        return $this->get('fos_rest.view_handler')->handle($view);
    }
}

Next, register your controller in your app/config/routing.yml file. Do not forget to set the type option to rest as illustrated below:

acme_user_rest:
    resource: Acme\UserBundle\Controller\UserRestController
    prefix: /api
    type: rest

Once the controller setup is done, the next step is to expose our models/documents/entities. The User class that I will be exposing is actually a child class that inherits from FOSUserBundle/Document/User. This a very important point because when you are using JMSSerializerBundle, you have to separately configure each parent class for serialization. In order to accomplish this goal, add the following section to your configuration app/config/config.yml file:

jms_serializer:
    metadata:
        auto_detection: true
        directories:
            FOSUserBundle:
                namespace_prefix: FOS\UserBundle
                path: "@AcmeUserBundle/Resources/config/serializer/fos"

The directories section signals the JMSSerializerBundle to look for configuration files for any class that has a FOS\UserBundle prefix under the path specified. This is the mechanism that you should use when you are dealing with classes from other bundles. It allow you to expose class properties that you are interested in without actually touching any other bundle code.

Simply add the JMSSerializerBundle configuration files to your project:

Acme/Userbundle/Resources/config/serializer/fos/Model.User.xml

<serializer>
    <class name="FOS\UserBundle\Model\User" exclusion-policy="all">
        <property name="username" type="string" expose="true" access-type="public_method"></property>
        <property name="usernameCanonical" type="string" expose="true" access-type="public_method"></property>
        <property name="email" type="string" expose="true" access-type="public_method"></property>
        <property name="emailCanonical" type="string" expose="true" access-type="public_method"></property>
    </class>
</serializer>

Acme/Userbundle/Resources/config/serializer/Document.User.xml

<serializer>
    <class name="Acme\UserBundle\Document\User" exclusion-policy="all">
        <property name="id" type="string" expose="true" read-only="true"></property>
        <property name="dob" type="date" expose="true" access-type="public_method"></property>
        <property name="nameFirst" type="string" expose="true" access-type="public_method"></property>
        <property name="nameMiddle" type="string" expose="true" access-type="public_method"></property>
        <property name="nameLast" type="string" expose="true" access-type="public_method"></property>
        <property name="createdAt" type="DateTime" expose="true" access-type="public_method"></property>
        <property name="modifiedAt" type="DateTime" expose="true" access-type="public_method"></property>
        <property name="blocked" type="boolean" expose="true" access-type="public_method"></property>
        <property name="deleted" type="boolean" expose="true" access-type="public_method"></property>
        <property name="avatarPath" type="string" expose="true" access-type="public_method"></property>
        <property name="lastLogin" type="DateTime" expose="true" access-type="public_method"></property>
    </class>
</serializer>

The prefixes - "Model." and "Document." in this case - that are used for naming these configuration files correspond to the original directories where each class resides. Therefore, using an incorrect prefix would break the setup.

If you look at the Document.User.xml file above, you can see that there is no access-type type attribute for the id property update and it is replaced by a read-only attribute. In fact, failure to remove the access-type attribute causes an exception. This is because you can not update OjbectIds in MongoDB and auto-generated classes do not even contain a setId() method.

Another important point is to clear the Symfony cache when you add new files or move configuration files around otherwise the changes are not picked up. This has caused me to lose many hours while trying to figure out the JMSSerializerBundle.

Finally, if your /api path is behind a firewall, first login to your application, and then access your newly created REST service by calling /api/users/{userId}.json. You should see an output like the following:

{
    "username": "burak",
    "username_canonical": "burak",
    "email": "burak@localhost.localdomain",
    "email_canonical": "burak@localhost.localdomain",
    "dob": "1934-12-31T16:00:00-0800",
    "name_first": "burak",
    "name_last": "s",
    "created_at": "2012-04-06T16:18:04-0700",
    "modified_at": "2012-04-06T18:19:10-0700",
    "id": "4f7f79ac7f8b9a000f000001"
}

In my next article, I will write about securing your REST service with OAuth using FOSOAuthServerBundle.

12 comments:

  1. We are getting the following exception after updating the config.yml file

    Fatal error: Class 'Metadata\Driver\LazyLoadingDriver' not found in /Applications/MAMP/htdocs/Symfony/app/cache/dev/appDevDebugProjectContainer.php on line 1023

    ReplyDelete
    Replies
    1. you should use metadata v1.1.0, not v.1.0.0

      Delete
    2. Yeah, you will need to update deps to have:
      [metadata]
      git=http://github.com/schmittjoh/metadata.git
      version=1.1.0

      Then delete the line from deps.lock.

      Then run "bin/vendor install" to update to 1.1.

      Delete
  2. hi,
    i got the following error:
    Fatal error: Class 'JMS\SerializerBundle\JMSSerializerBundle' not found in ...\app\AppKernel.php on line 23

    ReplyDelete
  3. "Next, register your controller in your app/config/config.yml file."
    I guess you meant routing.yml and not config.yml, didn't you? ;)

    ReplyDelete
  4. How are routes autogenerated and assigned to actions?
    How can it connect them:
    /users -> getUsersAction()
    /users/{slug} -> getUserAction($slug)

    I thought it takes a look at the prefix(get..) and the parameters ($slug), and the middle of the action name (..User..) can be anything (like getAsdasdAction() VS getQwewerAction($slug)).
    But if I renamed them I got a nice message that it didn't find a route

    routing section and examples in the documentation:
    https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/5-automatic-route-generation_single-restful-controller.md

    ReplyDelete
  5. I did a router:debug and saw a not-so-obvious thing: FOS REST Bundle appends an 's' to the singular ones (getUserAction -> /userS/{slug})

    I thought it but didn't think it was real.
    It can lead to weird words with no (or weong) meaning. What about irregular plurals or singular words already ending with 's'?

    The documentation doesn't mention this one, it says almost nothing about these things...

    I hope it will be useful to anyone else spending a quarter of an hour trying to figure out this behaviour.

    ReplyDelete
  6. How can I change the structure of the XML? Will I have to change anything in the following line?
    return $this->get('fos_rest.view_handler')->handle($view);

    ReplyDelete
  7. I wonder what this file is for: Acme/Userbundle/Resources/config/serializer/fos/Model.User.xml.
    When is this used?

    ReplyDelete
    Replies
    1. Hans, that file is the configuration file that determines how your objects get serialized/de-serialized. Please see the documentation at http://jmsyst.com/bundles/JMSSerializerBundle.

      Delete