Symfony3 et webservice Rest JSON/XML avec FOSRestBundle

Hello,

Un rapide article sur symfony 3 et les possibilités de webservices proposé par le FOSRestBundle ainsi que JMSSerializer pour gérer un service REST en JSON.

L’idée est de créer un webservice en json pour la récupération de divers objets depuis ma base de données, ici rien de complexe, aucun retraitement des données, ce sera un simple controller avec un objet issus de la base de données.

Installation des composants

composer require FOSRestBundle
composer require JMSSerializerBundle

Configuration

La configuration contiendra plus de lignes que notre développement ;).

Modification de AppKernel ajout des bundles suivants dans $bundles.

            new JMS\SerializerBundle\JMSSerializerBundle(),
            new FOS\UserBundle\FOSUserBundle(),

Ajout de la configuration propre au module fosrest :
app/config/config.yml:

fos_rest:
    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
    disable_csrf_role: null
    access_denied_listener:
        enabled: false
        service: null
        formats:
    unauthorized_challenge: null
    param_fetcher_listener:
        enabled: true

Ne pas oublier d’activer le serializer pour le framework :
app/config/config.yml:

framework:
    serializer:
        enabled: true
        enable_annotations: true

Ajout des routes automatiques du Bundle :
app/config/routing.yml:

task_api:
    type:     rest
    resource: TaskBundle\Controller\ApiController
    prefix:     /api

Ici, j’aimerais que mon service renvoi des éléments de type rest sur des urls commençant par /api/.

Une fois la configuration terminé on peut passer à la programmation.

Création d’une classe Métier

Ici mon but est d’afficher des Catégories (avec un nom et un id).

On peut utiliser ici plusieurs annotations bien sympatique, qui permettent :

  • Dégager toutes les propriétés de bases (ExclusionPolicy)
  • Choisir les éléments à afficher (Expose)
  • Choisir leurs groupes d’affichages cf. le controlleur (Groups)

Le gros interêt de JMS Serializer ici est de pouvoir choisir les éléments que l’on veut afficher en fonction de leur contexte (default, category).

namespace TaskBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;

/**
 * Category
 *
 * @Serializer\ExclusionPolicy("all")
 * @ORM\Table(name="category")
 * @ORM\Entity(repositoryClass="TaskBundle\Repository\CategoryRepository")
 */
class Category
{
    /**
     * @var int
     *
     * @Serializer\Expose
     * @Serializer\Groups({"default"})
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @Serializer\Expose
     * @Serializer\Groups({"default"})
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;
}

Création d’un controller REST

use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\View;

class ApiController extends FOSRestController
{
    /**
     * @return Category
     * @View(serializerGroups={"default", "category"})
     */
    public function getCategoryAction(Request $req,$id){
        return $this->getDoctrine()->getRepository('TaskBundle:Category')->find($id);
    }
}

Dans mon cas ce controller me crée une nouvelle route :
ce que l’on peut vérifier avec la commande « console debug:router »

get_category                        GET        ANY      ANY    /api/categories/{id}.{_format}

Les routes sont crée automatiquement par le controller.

On peut désormais accéder à notre url de service : /api/categories/1
Ce qui nous renverra dans mon cas :

{"id":1,"name":"D\u00e9veloppement"}

A noter que l’on peut rajouter un format dérrière notre url :

/api/categories/1.json
/api/categories/1.xml
/api/categories/1.html

Gestion des posts

Il est possible de modifier nos objets directement depuis le controller rest, en respectant le format demandé (si l’on est en XML, il faut faire un post d’un objet xml, en json, il faut post un objet json, …).

On ajoute donc une action a notre Controller :

/**
     * @return Category
     * @View(serializerGroups={"default", "category"})
     */
    public function postCategoryAction(Request $req){
        return $this->processFromType($req,CategoryType::class,new Category());
    }

    private function processFromType($req,$type,$elem){
        if($elem==null)return null;
        $form = $this->createForm($type, $elem);
        $form->handleRequest($req);
        if ($form->isValid()) {
            $elem = $form->getData();
            $em = $this->getDoctrine()->getManager();
            $em->persist($elem);
            $em->flush();
            return $elem;
        }
        return $this->handleView($this->view($form, 400));
    }

Un petit coup de « console debug:router » pour vérifier la création de notre route d’édition :

get_category                        GET        ANY      ANY    /api/categories/{id}.{_format}
post_category                       POST       ANY      ANY    /api/categories.{_format}

Il vous faudra bien entendu un objet pour gérer le type du formulaire, ici un CategoryType :


namespace TaskBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CategoryType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('id')
        ;
    }
    
    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'TaskBundle\Entity\Category',
            'csrf_protection' => false
        ));
    }
    
    /**
     * @return string
     */
    public function getName()
    {
        return 'category';
    }
}

Le CategoryType est important, il permet de choisir le nom de notre container ici : « category », utile pour notre requête de création/modification :

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"category":{"name":"foo"}}' http://monurl/api/categories

Et voilà, vous savez tout … vous pouvez tenter de jouer sur les Groups et serializerGroups={« default », « category »} pour cacher des éléments en fonction des actions de votre controller (utile pour afficher des objets enfants ou non).