Créer une API Rest dans une application Symfony

Share Button

Read the English version

Comme nous avions besoin d’une connexion entre une application web Symfony et une appli Android, nous avons dû apprendre et comprendre comment créer une API Rest de manière simple et sécurisée, en nous basant sur nos entités existantes.

Nous avons choisi WSSE pour l’accès sécurisé, FOSRestBundle pour la restitution de données et JMSSerializerBundle pour la sérialisation. 

Nous avions également besoin de séparer les logs pour les erreurs d’authentification WSSE. Lisez notre article pour savoir comment créer un fichier de logs séparé avec Monolog (en anglais).

Librairies

Bundles

Articles

Première étape : Installer FOSRestBundle et JMSSerializerBundle

Vous aurez besoin de ces deux bundles, donc ajoutez-les à votre fichier composer.json :

// composer.json

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

Lancez la commande php composer.phar update  pour mettre à jour vos vendors puis enregistrez les bundles dans votre fichier AppKernel.php :

//app/AppKernel.php

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

Enfin, configurez le 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

Notre but ici n’est pas de détailler comment choisir la meilleure configuration. Lisez la documentation du FOSRestBundle pour plus d’informations.

Vos bundles sont maintenant prêts à être utilisés.

Deuxième étape : Créer vos fichiers de routing Rest et appeler votre API.

Dans le but de gérer les requêtes Rest, vous devrez définir vos routes. FOSRestBundle vous permet d’utiliser un nouveau type de route : “rest”

Les routes seront générées automatiquement. Suivez les étapes suivantes pour savoir comment.

Importez le fichier routing_rest.yml

Dans votre app/routing.yml, déclarez un fichier routing_rest.yml qui est chargé avec le type “rest”. Ce type est nécessaire pour la génération automatique des routes.

Nous spécifions également à Symfony que chaque route de routing_rest.yml a besoin du préfixe /api.

#app/routing_yml

# ...

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

Dans app/routing_rest.yml, vous devez déclarer votre (ou vos) fichiers de routing Rest pour chaque bundle :

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

Dans le fichier de routing Rest du UserBundle, vous devez définir un Contrôleur Rest. La configuration spécifie également que chaque route sera nommée avec le préfixe api_ pour éviter les conflits :
#src/Obtao/UserBundle/Resources/config/routing_rest.yml
users : 
  type: rest
  resource:     "UserBundle:UserRest"
  name_prefix:  api_

Créez votre première fonction de l’API Rest : 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;
  }
}

Vos routes ont été générées, lancez la commande router:debug pour vérifier si tout est ok :

 $ app/console router:debug | grep api_

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

Mais le sérialiser ne sait pas encore comment sérialiser un utilisateur. C’est ce que nous allons voir dans la section suivante.

Configurez la sérialisation de l’entité

Pour le moment, nous avons juste besoin d’envoyer les propriétés “id”, “firstname”,”name” et “usedname”. Nous pouvons utiliser les annotation du JMSSerializerBundle directement dans nos entités :

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();
        }
    }   
  

Jetons un oeil aà l’exemple précédent plus en détails :

@ExclusionPolicy(“all”) : Chaque propriété de votre entité sera ignoré lors de la sérialisation. 

@Expose : Cette propriété sera sérialisée.

@VirtualProperty : Cette méthode sera appelée et sérialisée comme propriété virtuelle (used_name dans notre exemple).

Nous y voilà. Appelez api/users/userName et vérifiez la réponse.

Pour ceux qui veulent créer une api et étendre des entités de librairies tierces (comme nous le faisons avec le FOSUserBundle), lisez attentivement la section suivante.

Troisième étape (facultative) : Configurer la sérialisation sur les entités de librairies tierces

Vous pouvez ajouter des propriétés privées/publiques sur vos entités, mais vous ne pouvez pas modifier les entités des librairies tierces. Heureusement, il est possible de déclarer des paramètres afin de cacher/afficher les propriétés, comme nous le faisons avec les annotations.

Déclarer une nouvelle méta données pour le JMSSerializerBundle

Ajoutez ceci à votre fichier config.yml

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

Cela indique à JMS que les méta données des entités du FOSUserBundle seront également déclarées dans le dossier app/serializer/FOSUserBundle.

Ajouter les paramètres pour la sérialisation de l’utilisateur

Dans cet exemple, nous voulonssurcharger les méta données de l’entité User. Nous créons donc un nouveau fichier Model.User.yml dans le dossier app/serializer/FOSUserBundle/

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

Chaque propriété de l’entité User sera exclue lors de la sérialisation et seule la propriété “username” sera exposée.

Dernière étape : Créer un modèle d’entité pour la sérialisation (afficher/cacher des attributs selon le contexte)

Dans certains cas, vous aurez besoin de 2 sérialisations différentes pour la même entité. Vous avez donc besoin de la notion de “Groups”.

Voici un exemple :
Vous voulez fournir un service public via votre API qui contient des informations sur l’utilisateur (username et description), mais vous voulez aussi fournir un service sécurisé (avec authentification) qui donne des informations sur le compte des utilisateurs (nom et prénom).
Dans cette partie, nous appelerons “contexte” le fait de sérialiser différemment notre objet User.

Suivez les étapes suivantes pour apprendre comment le faire en utilisant seulement les annotations.

Pour votre entité User, vous avez besoin de 2 méthodes différentes. Une méthode publique pour retrouver les informations publiques de l’utilisateur :

server.name/api/users/francois

et une autre pour retrouver les informations privées sur l’utilisateur authentifié :

server.name/api/me

Vous devrez créer ces 2 méthodes dans votre UserRestController (pas besoin de créer de route).

Pour la définition du contexte, vous utiliserez les annotations fournies par FOSRest et basées sur la fonctionnalité “Groups” de JMSSerializer.


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);
    }
  }  
}

Ce que nous faisons ici est de définir un contexte pour nos méthodes.

  • Pour le service public(getUserActionapi/users/{username}), nous définissons 2 groupes comme contexte : “Default” et “Details”. Tous les champs de l’entité User marqués comme faisant partie du groupe “Default” (via l’annotation @Expose), ou du groupe “Details” (annotations @Expose + @Groups({“Details”}) ) seront sérialisés.
  • Pour le service sécurisé (getMeapi/me), nous définissons 3 groupes comme contexte : “Default”, “Details” (comme pour le service public) mais nous ajoutons également “Me”. Tous les champs marqués par l’un de ces 3 groupes seront sérialisés

Le contrôleur est prêt, vous devez maintenant spécifier quels attributs doivent être sérialisés pour chaque contexte.

Dans votre entité, vous pouvez maintenant utiliser l’annotation “Groups” de JMSSerializer pour définir vos attributs :


    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     * @Groups({"Me"})
     */    
    protected $name; // Le nom est visible pour le contexte "Me" 

    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     * @Expose
     */    
    protected $avatar; // l'avatar est visible pour le contexte "Default"
    
    /**
    * @ORM\Column(type="text", nullable=true)
    * @Expose
    * @Groups({"Details"})
    */    
    protected $description; // La description est visible pour le contexte "Details"
    

$description sera sérialisé pour les méthodes du contrôleur qui possèdent l’annotation @View(serializerGroups={“Details”}).
$name for @View(serializerGroups={“Me”})
$avatar pour @View(serializerGroups={“Default”}) (puisqu’aucun groupe n’a été défini)

Configurer les groupes d’une entité d’une librairie tierce (FOSUserBundle)

Pour ceux qui étendent une entité d’une librairie tierce (dans notre cas nous étendons le User du FOSUserBundle), vous devrez également configurer les groupes sur les attributs hérités.

Modifiez simplement votre configuration de modèle de cette manière : 

//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

Votre API est maintenant prête à être utilisée.

Le résultat de toute cette configuration sera quelque chose comme :

Pour http://www.obtao.com/api/users/francois
-> getUser($username) dans UserRestController, contextes “Default” et “Details”) :

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

Pour http://www.obtao.com/api/me (authentifié en tant que “francois”)
-> getMe() dans UserRestController, contextes {“Default”,”Details” et “Me”}

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

Lisez notre post sur WSSE pour savoir comment protéger vos ressources avec un login/mot de passe de façon RESTful

Share Button

18 thoughts on “Créer une API Rest dans une application Symfony

  1. j’ai une réponse en json : {“hotels “:[{"file":null}]} au lieu d’avoir la liste de l’entité hôtel :’( vous pouvez me dire c’est quoi le problème ?

  2. Bonjour, dans le tuto, nous excluons par défaut toutes les propriétés lors de la sérialisation grâce à l’annotation @ExclusionPolicy(“all”). En conséquence, vous devez spécifiez quelles propriétés doivent être retournées lors de la sérialisation en utilisant l’annotation @Expose (comme nous le faisons pour les propriétés “name” et “firstname” par exemple).
    Si ce n’est pas ça, contactez-nous par email via le site pour nous donner plus de code afin que l’on puisse identifier ce qui ne va pas.
    Bon courage

  3. Merciii pour votre réponse mais c la même chose :( jvais vous contacter par mail pour plus de détail
    et merci d’avance :)

  4. Merciiiiii ça marche très bien :) (y)

    EDIT Gregquat : le problème était que le contrôleur doit retourner directement un objet ou une liste d’objets, et non un objet Response.

  5. bjr il m’affiche toujours cette erreur “Fatal error: Class ‘FOS\RestBundle\FOSRestBundle’ not found in C:\xampp\htdocs\Tunisie-business\app\AppKernel.php on line 21
    Done.” c’est quoi le problem exactement?
    merci de me repondre :)

  6. Bonjour,

    Il semble que le bundle FOSRest ne soit pas correctement installé (et donc que la classe ne soit pas trouvée) :
    - Vérifiez le composer.json (et la présence du "friendsofsymfony/rest-bundle": "dev-master"php composer.phar update et assurez vous que tout se termine sans erreur

    François

  7. c’est resolu !! :D j’ai installé git du site “msysgit.github.io” puis j’ai ajouté le chemin C:\Program Files\Git\cmd dans la variable d’environnement et ca marche tres bien
    merci a tous ;)

  8. Bonjour, merci pour ce super article qui reprend de bonnes bases et des bonnes pratiques pour la création d’une API sur une application Symfony2.
    Ayant rencontrée une problématique il y a peu, liée à la manipulation des attributs d’une entité lors de la serialisation (l’idée en deux mots : transformer un File::$filename en File::$resourceUrl ).
    Je pense que la notion abordée dans cet article (les Event Subscriber avec JMSSerializerBundle) est pertinente dans la continuité du votre, n’hésitez pas à me contacter par email pour me demander le lien, je vous inviterai à le renseigner sur cette page si vous le voulez bien, bien sûr.
    Bien à vous.

  9. Bonjour,
    Je vous remerci de ce super tuto. J’ai une petitte question: comment faire pour avoir plusieurs controlleurs qui gère le web service SVP ? J’ai tester de le faire mais en vain. Exemple: Deux controlleurs ArticleRestController et CommentaireRestController, ayant chacun une action : getArticleAction() et getCommentaireAction(). Puisque le bundle crée les route lui même les route, il écrase toujours la première (ws_get).

  10. C’est bon, j’ai trouvé la solution. Il suffit de définir un name_prefix différent pour chacun d’eux.

  11. bjr j’ai tjr cette erreur [RuntimeException] You need to disable the view annotations in SensioFrameworkExtraBundle when using the FOSRestBundle View Response listener.
    HELP PLZ :(