Create a Rest API in a Symfony application

Share Button

Lire la version française

As we needed a connection between a Symfony web application and an Android application, we had to learn and understand how to create a Rest API in a simple and secured way, based on our existing entities.

We chose WSSE for security access, FOSRestBundle for datas restitution and JMSSerializerBundle for serialization. 

We also needed to separate logs for the wsse authentification errors. See our article to know how to create separated log files in Monolog

Links library :

Bundles

Articles / Answers

First Step : Install FOSRestBundle and JMSSerializerBundle

You will need these two bundles, so add them to your composer.json file:

//composer.json

// ...
"friendsofsymfony/rest-bundle": "dev-master",
"jms/serializer-bundle": "dev-master"

Run the command php composer.phar update  to update your vendors and register the bundles in your AppKernel.php file:
//app/AppKernel.php

public function registerBundles()
    {
        $bundles = array(
            [...],
            new FOS\RestBundle\FOSRestBundle(),
            new JMS\SerializerBundle\JMSSerializerBundle(),

Finally, configure your new FOSRestBundle: 
#app/config/config.yml
fos_rest:
    param_fetcher_listener: true
    body_listener: true
    format_listener: true
    view:
        view_response_listener: 'force'
        formats:
            xml: true
            json : true
        templating_formats:
            html: true
        force_redirects:
            html: true
        failed_validation: HTTP_BAD_REQUEST
        default_engine: twig
    routing_loader:
        default_format: json

Our aim here is not to details how to choose the right configuration. Read the FOSRestBundle for more information.

Your bundles are now ready to be used.

Second step : Create your rest routing file(s) and call your API.

In order to handle Rest requests, you will need to declare your routes. FOSRestBundle allow you to use a new route type : “rest”

Routes will be automatically generated, follow the following steps to know how.

Import your routing_rest.yml file

In your app/routing.yml, declare a routing_rest.yml file which is loaded with the “rest” type. This type is needed for automatic route generation.

We also tell to Symfony that every route behind routing_rest.yml needs the prefix /api.

#app/routing_yml

# ...

#REST 
rest : 
  type : rest 
  resource : "routing_rest.yml"
  prefix : /api

In app/routing_rest.yml you must declare your Rest routing file(s) for each bundle:
#app/routing_rest.yml
Rest_User : 
  type : rest
  resource: "@UserBundle/Resources/config/routing_rest.yml"

In the UserBundle Rest routing file, you must define a Rest Controller. The configuration also tells that every route will be named with the prefix api_ in order to avoid conflicts:
#src/Obtao/UserBundle/Resources/config/routing_rest.yml
users : 
  type: rest
  resource:     "UserBundle:UserRest"
  name_prefix:  api_

Create your first Rest API function : getUser 


namespace Obtao\UserBundle\Controller;

use FOS\RestBundle\Controller\Annotations\View;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class UserRestController extends Controller
{
  public function getUserAction($username){
    $user = $this->getDoctrine()->getRepository('UserBundle:User')->findOneByUsername($username);
    if(!is_object($user)){
      throw $this->createNotFoundException();
    }
    return $user;
  }
}

Your routes has been generated, run the command router:debug to check if all is ok:
 $ app/console router:debug | grep api_

fr__RG__api_get_user                         GET      ANY    ANY  /api/users/{username}.{_format}

But the serializer does not know how to serialize a user yet. That’s we will see in the next section.

Configure entity serialization

For the moment we just need to send “id”, “firstname”,”name” and “usedname”. We can use the annotations of the JMSSerializerBundle directly in our entity:



namespace Obtao\UserBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;

/**
 * @ORM\Entity
 * @ORM\Entity(repositoryClass="Obtao\UserBundle\Repository\UserRepository")
 * 
 * @ExclusionPolicy("all") 
 */
class User extends BaseUser {
    
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     * @Expose
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     */
    protected $firstname;
 
    // ...

    /**
     * Get the formatted name to display (NAME Firstname or username)
     * 
     * @param $separator: the separator between name and firstname (default: ' ')
     * @return String
     * @VirtualProperty 
     */
    public function getUsedName($separator = ' '){
        if($this->getName()!=null && $this->getFirstName()!=null){
            return ucfirst(strtolower($this->getFirstName())).$separator.strtoupper($this->getName());
        }
        else{
            return $this->getUsername();
        }
    }   
  

Take a look at the previous example in more detail:

@ExclusionPolicy(“all”) : Every field on your entity will be ignore while serializing. 

@Expose : This field will be serialized

@VirtualProperty : This method will be called and serialized as a virtual property. (used_name in our example)

Here we are. Call api/users/userName and check the response.

For those who want to make an api and extend some third party entities (as we do with the FOSUserBundle ), read carefully the following part.

Third (and optionnal) step : Configure serialization on third-part extended entities

You can add some private/public policy on our entities, but you cannot modify third-party entities. Fortunately, it is possible to add some parameters in order to hide/show properties as we do with annotations.

Declare a new metadata to JMSSerializerBundle

Add this to your config.yml

//app/config.yml
jms_serializer:
    metadata:
        directories:
            FOSUB:
                namespace_prefix: "FOS\\UserBundle"
                path: "%kernel.root_dir%/serializer/FOSUserBundle"

It tells to JMS that FOSUserBundle entities metadata will be declared also under the app/serializer/FOSUserBundle folder.

Add user model parameters for serialization

In this example we want to override User metadatas so we add Model.User.yml in app/serializer/FOSUserBundle/

//app/serializer/FOSUserBundle/Model.User.yml
FOS\UserBundle\Model\User:
    exclusion_policy: ALL
    properties : 
      username : 
        expose : true

Every User property will be exclude while serializing and only the “username” will be exposed.

Last step : Create entity model for serialization (hide/show attributes according to the context)

In some cases, you will need 2 differents serializations for the same entity. “Groups” is the notion you need.

As an example :
You want to serve by your API a public service with users informations (Username and description), but you also want to serve a restricted service (with authentication) which provides register user informations about his account (firstname, lastname).
In this part, we call the two way to serialize User object as two different “context”.

Follow next steps to learn how to do this only with annotations.

For your User entity, you need to create 2 differents methods. One public method to get public information about the user : 

server.name/api/users/francois

and another one to get private information about the authenticated user :

server.name/api/me

You need to create these 2 methods in your UserRestController (no need to create routes).

For the context definition, you will use the annotations added by FOSRest based on JMSSerializer “Groups” functionnality.


namespace Obtao\UserBundle\Controller;
use Obtao\ObtaoBundle\Controller\ObtaoController;

use Obtao\TripBundle\Entity\Trip;
use FOS\RestBundle\Controller\Annotations\View;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class UserRestController extends ObtaoController
{
  /**
   * 
   * @param type $username
   * 
   * @View(serializerGroups={"Default","Details"})
   */
  public function getUserAction($username){
    $user = $this->getRepository('UserBundle:User')->findOneByUsername($username);
    
    if(!is_object($user)){
        throw $this->createNotFoundException();
    }
    
    return $user;
  }
  
  /**
   * 
   * @View(serializerGroups={"Default","Me","Details"})
   */
  public function getMeAction(){
    $this->forwardIfNotAuthenticated();
    return $this->getUser();
  }

  /**
   * Shortcut to throw a AccessDeniedException($message) if the user is not authenticated
   * @param String $message The message to display (default:'warn.user.notAuthenticated')
   */
  protected function forwardIfNotAuthenticated($message='warn.user.notAuthenticated'){
    if (!is_object($this->getUser()))
    {
        throw new AccessDeniedException($message);
    }
  }  
}

What we done here is to set a context to our methods.

  • For the public service(getUserActionapi/users/{username}), we set 2 groups as context : “Default” and “Details”. All the fields of User entity marked for “Default” group (@Expose annotation), or for “Details” group (@Expose + @Groups({“Details”}) ) will be serialized.
  • For the authenticated service (getMeapi/me), we set 3 groups as context : “Default”, “Details” (as in public service) but we added “Me”. All fields marked for these groups will be serialized

The controller is ready, you have now to explicit which attribute has to be serialized in each context.

In your entity, you will now use the annotation “Groups” from the JMSSerializer to define your attributes:


    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     * @Groups({"Me"})
     */    
    protected $name; // Name is visible for the context "Me" 

    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     */    
    protected $avatar; //avatar will be visible for the group "Default"
    
    /**
    * @ORM\Column(type="text", nullable=true)
    * @Expose
    * @Groups({"Details"})
    */    
    protected $description; // Description is visible for the context "Details"
    

$description will be serialized for method in controller with annotation @View(serializerGroups={“Details”}).
$name for @View(serializerGroups={“Me”})
$avatar for @View(serializerGroups={“Default”}) (As no group has been specified in entity)

Configure groups in third party extended entity(FOSUserBundle)

For those who extended a third party entity (in our case FOSUserBundle) you may need to configure groups also on inherited attributes.

Just modify your model config this way : 

//app/serializer/FOSUserBundle/Model.User.yml
FOS\UserBundle\Model\User:
    exclusion_policy: ALL
    properties : 
      salt : 
        expose : true
        groups : [Details]
      email :
        expose  : true
        groups  : [Me]
      enabled : 
        expose  : true


Your API is now ready to be used.

The result of your configuration will be :

For http://www.obtao.com/api/users/francois
-> getUser($username) in UserRestController, context = “Default” and “Details”) :

{"username":"francois", "salt":"mysalt", "enabled" : true, "avatar" : "myAvatar.png", "description" : "This is my description"}

For http://www.obtao.com/api/me (Registered as “francois”)
-> getMe() in UserRestController, context was {“Default”,”Details” and “Me”}

{"username":"francois", "salt":"mysalt", "enabled" : true, "avatar" : "myAvatar.png", "description" : "This is my description", "email" : "myemail@obtao.com", "name" : "MyName"}

Read our next post about WSSE to learn how to protect your resources with login/password in a RESTFul way.

Share Button

5 thoughts on “Create a Rest API in a Symfony application

  1. Hey,

    Thanks for this good tutorial, we need more like this one online!

    But I’m experiencing some troubles here:
    When doing a “app/console router:debug | grep api_” I get the following errors:
    [Symfony\Component\Config\Exception\FileLoaderLoadException]
    Cannot import resource “C:/wamp/www/Oscar/Symfony/app/config\routing_rest.yml’ from “C:/wamp/www/Oscar/Symfony/app/config\routing.yml”.

    [InvalidArgumentException]
    Bundle “UserBundle” does not exist or it is not enabled. Maybe you forgot to add it in the registerBundles() method of your AppKernel.php file?
    router:debug [name]

    Here is the content of /app/config/routing_rest.yml

    #app/routing_rest.yml
    Rest_User :
    type : rest
    resource: “@UserBundle/Resources/config/routing_rest.yml”

    And we added to app/routing.yml those lines:

    rest :
    type : rest
    resource : “routing_rest.yml”
    prefix : /api

    And in the AppKernel.php file we have:

    new FOS\UserBundle\FOSUserBundle(),
    new Sampleo\UserBundle\SampleoUserBundle(),

    Which seems the good way to declare them.
    Any leads?

  2. Hey,
    Me again!

    We used route annotations instead. Seems more efficient, can’t figure out why we had the problem in the first place though.

    PS: Bonjour de Paris :)

  3. Maybe your routing_rest.yml needs to have “@SampleoUserBundle” instead of “@UserBundle” ? (Even if the problem is solved)

    Bonjour à vous aussi ! ;)