Mise en place d'une API REST avec FOSRestBundle dans Symfony 2

Ce tutorial vise à mettre en place une application Symfony 2 avec une table utilisateur et un controller prenant disposant deux méthodes getUsers et getUser. Ces méthodes vont nous retourner un réponse en JSON comportant les données de notre table utilisateur.

Publié le 23/06/2015

1 - Création de l'application et de son environnement

Je pars du principe qu'on est dans un environnement LAMP Debian, nous allons commencer par créer un vhost pour notre application :

nano /etc/apache2/sites-available/api_test


<VirtualHost *:80>
        ServerName api-test.local
        ServerAdmin admin@domain.local

        DocumentRoot /var/www/api_test/web
        <Directory />
                Options FollowSymLinks
                AllowOverride All
        </Directory>
        <Directory /var/www/api_test/web/>
                DirectoryIndex app.php
                Options Indexes FollowSymLinks MultiViews
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>

        ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
        <Directory "/usr/lib/cgi-bin">
                AllowOverride None
                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                Order allow,deny
                Allow from all
        </Directory>

        ErrorLog ${APACHE_LOG_DIR}/error.log

        # Possible values include: debug, info, notice, warn, error, crit,
        # alert, emerg.
        LogLevel warn

        CustomLog ${APACHE_LOG_DIR}/access.log combined

<VirtualHost>

 

Activation du vhost et reload d'Apache :

sudo a2ensite api_test sudo service apache2 reload

Création du projet Symfony 2 :

sudo php composer.phar create-project symfony/framework-standard-edition /var/www/api_test/ 2.6.0 cd /var/www/api_test/ sudo curl -s https://getcomposer.org/installer | php

A la configuration du projet on choisira les options par défaut et on choisira api_test comme nom de base de donnée.

Ajout du Bundle JMSSerializerBundle :

sudo php composer.phar require jms/serializer-bundle

Ajout du Bundle FOSRestBundle :

sudo php composer.phar require friendsofsymfony/rest-bundle @stable

Ajout du Bundle DoctrineFixturesBundle :

sudo php composer.phar require doctrine/doctrine-fixtures-bundle dev-master

Enregistrement des Bundles dans le kernel :

nano app/AppKernel.php

Ajouter les lignes suivantes dans le tableau $bundles de la méthode registerBundle :

 


new JMS\SerializerBundle\JMSSerializerBundle(),
new FOS\RestBundle\FOSRestBundle(),
new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),

 

Création du Bundle ApiTest :

 


chmod -R 777 app/cache/ app/logs
php app/console generate:bundle

 

Configuration du Bundle :

Bundle name : Fresh/ApiTest

Configuration format : annotation

2 - Configuration de FOSRestBundle

nano app/config/config.yml

Ajouter les directives suivantes :

 


sensio_framework_extra:
    view:   { annotations: false }
    router: { annotations: true }

# FOSRestBundle

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

 

3 - Création d'une entité User

php app/console doctrine:database:create php app/console doctrine:generate:entity

Configuration du Bundle :

name : FreshApiTestBundle:User

fields :

name : username

type : string

lenght : 255

name : password

type : string

lenght : 255

name : email

type : string

lenght : 255

Laisser les autres choix par défaut à la génération de l'entité.

4 - Création de fixtures pour l'entité User nano src/Fresh/ApiTestBundle/DataFixtures/ORM/LoadUserData.php


<?php

namespace Fresh\ApiTestBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Fresh\ApiTestBundle\Entity\User;

class LoadUserData implements FixtureInterface
{
    /**
     * Load data fixtures with the passed EntityManager
     *
     *
     */
    public function load(ObjectManager $manager)
    {
        $toto = new User();
        $toto->setUsername('toto');
        $toto->setPassword('toto');
        $toto->setEmail('toto@toto.org');

        $titi  = new User();
        $titi->setUsername('titi');
        $titi->setPassword('titi');
        $titi->setEmail('titi@titi.org');

        $manager->persist($toto);
        $manager->persist($titi);

        $manager->flush();
    }

}

5 - Mise à jour de la base de donnée


php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load

6 - Création des routes et du controller

Création des routes :

nano app/config.routing.yml


Fresh_demo:
    resource: "@FreshApiTestBundle/Controller/"
    type:     annotation
    prefix:   /

users:
    type:   rest
    resource:   Fresh\ApiTestBundle\Controller\UsersController

 

Création du Controller UsersController :

nano src/Fresh/ApiTestBundle/Controller/UsersController.php


<?php

namespace Fresh\ApiTestBundle\Controller;

use Fresh\ApiTestBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations\View;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

class UsersController extends Controller
{
    /**
     * @return array
     * @View()
     */
    public function getUsersAction()
    {
        $em = $this->getDoctrine()->getManager();

        $users = $em->getRepository('FreshApiTestBundle:User')->findAll();

        return array('users' => $users);
    }

    /**
     * @param User $user
     * @return array
     * @View()
     * @ParamConverter("user", class="FreshApiTestBundle:User")
     */
    public function getUserAction(User $user)
    {
        return array('user' => $user);
    }
}

 

7 - Exclusion du champs password

Afin d'exclure le champs mot de passe de la réponse JSON, on va configurer le serializer de façon à ce qu'il procède de la sorte.

Configuration du serializer :

nano src/Fresh/ApiTestBundle/Resources/config/serializer/Entity.User.yml


SfWebApp\ApiBundle\Entity\User:
    exclusion_policy: ALL
    properties:
        id:
            expose: true
        username:
            expose: true
        email:
            expose: true

Ajout des exclusions dans l'entité User :

nano src/SfWebApp/ApiBundle/Entity/User.php


<?php

namespace ApiTest\MainBundle\Entity;

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

/**
 * Class User
 * @package ApiTest\MainBundle\Entity
 * @ORM\Table(name="fos_user")
 * @ORM\Entity
 *
 * @ExclusionPolicy("all")
 */
class User extends BaseUser
{
	const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
	const ROLE_ADMIN = 'ROLE_ADMIN';
	const ROLE_USER = 'ROLE_USER';

	/**
	 * @var integer
	 *
	 * @ORM\Column(name="id", type="integer")
	 * @ORM\Id
	 * @ORM\GeneratedValue(strategy="AUTO")
	 */
	protected $id;

	/**
	 * @var
	 *
	 * @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
	 * @ORM\JoinTable(name="users_groups",
	 *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
	 *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
	 * )
	 */
	protected $groups;

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

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

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

	/**
	 * @var
	 *
	 * @ORM\Column(name="address", type="text", nullable=true, nullable=true)
	 */
	protected $address;

	/**
	 * @var
	 *
	 * @ORM\Column(name="zip_code", type="string", length=255, nullable=true)
	 */
	protected $zipCode;

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

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

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

	/**
	 * Constructor
	 */
	public function __construct()
	{
		parent::__construct();
		$this->groups = new ArrayCollection();
	}

	/**
	 * @param mixed $gender
	 */
	public function setGender($gender)
	{
		$this->gender = $gender;
	}

	/**
	 * @return mixed
	 */
	public function getGender()
	{
		return $this->gender;
	}

	/**
	 * @param mixed $firstname
	 */
	public function setFirstname($firstname)
	{
		$this->firstname = $firstname;
	}

	/**
	 * @return mixed
	 */
	public function getFirstname()
	{
		return $this->firstname;
	}

	/**
	 * @param mixed $lastname
	 */
	public function setLastname($lastname)
	{
		$this->lastname = $lastname;
	}

	/**
	 * @return mixed
	 */
	public function getLastname()
	{
		return $this->lastname;
	}



	/**
	 * @param mixed $address
	 */
	public function setAddress($address)
	{
		$this->address = $address;
	}

	/**
	 * @return mixed
	 */
	public function getAddress()
	{
		return $this->address;
	}

	/**
	 * @param mixed $city
	 */
	public function setCity($city)
	{
		$this->city = $city;
	}

	/**
	 * @return mixed
	 */
	public function getCity()
	{
		return $this->city;
	}

	/**
	 * @param mixed $country
	 */
	public function setCountry($country)
	{
		$this->country = $country;
	}

	/**
	 * @return mixed
	 */
	public function getCountry()
	{
		return $this->country;
	}

	/**
	 * @param int $id
	 */
	public function setId($id)
	{
		$this->id = $id;
	}

	/**
	 * @return int
	 */
	public function getId()
	{
		return $this->id;
	}

	/**
	 * @param mixed $zipCode
	 */
	public function setZipCode($zipCode)
	{
		$this->zipCode = $zipCode;
	}

	/**
	 * @return mixed
	 */
	public function getZipCode()
	{
		return $this->zipCode;
	}

	/**
	 * @param mixed $phone
	 */
	public function setPhone($phone)
	{
		$this->phone = $phone;
	}

	/**
	 * @return mixed
	 */
	public function getPhone()
	{
		return $this->phone;
	}

	/**
	 * @param $group
	 * @return $this
	 */
	public function addGoup($group)
	{
		$this->groups[] = $group;
		$group->setUser($this);

		return $this;
	}

	/**
	 * @param $groups
	 */
	public function setGroups($groups)
	{
		$this->groups->clear();
		foreach ($groups as $group) {
			$this->addGroup($group);
		}
	}

	/**
	 * @return ArrayCollection
	 */
	public function getGroups()
	{
		return $this->groups;
	}

	/**
	 * 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->getLastname() != null && $this->getFirstName() != null){
			return ucfirst(strtolower($this->getFirstName())).$separator.strtoupper($this->getLastName());
		}
		else{
			return $this->getUsername();
		}
	}
}

On a ajouté une méthode "getUsedName()" en tant que propriété virtuelle, afin de pouvoir récupérer le nom et prénom.

8 - Test de notre API

Appel retournant tous les utilisateur :

curl -X GET -H "Accept:application/json" http://api-test.local/users | python -mjson.tool

On doit obtenir un réponse JSON avec une collection de users :

 


{
    "users": [
        {
            "email": "toto@toto.org",
            "id": 1,
            "username": "toto"
        },
        {
            "email": "titi@titi.org",
            "id": 2,
            "username": "titi"
        }
    ]
}

 

Appel retournant un seul utilisateur :

curl -X GET -H "Accept:application/json" http://api-test.local/users/1 | python -mjson.tool

On doit obtenir un réponse JSON avec une collection de users :

 


{
    "user": {
        "email": "toto@toto.org",
        "id": 1,
        "username": "toto"
    }
}

 

Ressources :