Mettre en place une API REST 2ème partie

A présent, récupérons une catégorie avec son id, en utilisant directement le type de l’entité (principe du ParamConverter pour directement binder son id avec le champ correspondant de la base et éviter de coder nous-mêmes le renvoi à une page 404)

/**
     * @Rest\Get(
     *    path = "/{id}",
     *    name="app_api_category",
     *    requirements={"id"="\d+"}
     * )
     *
     * @Doc\ApiDoc(
     *     section="Categories",
     *     resource=true,
     *     description="Get one category.",
     *     requirements={
     *          {
     *              "name"="id",
     *              "dataType"="integer",
     *              "requirement"="\d+",
     *              "description"="The category unique identifier."
     *          }
     *      },
     *      statusCodes={
     *          200="Returned when successful",
     *      }
     * )
     */
    public function getCategoryAction(Category $category)
    {
        return $category;
    }

postman_categorie

Le verbe POST

C’est parti pour le POST. Le code final va être celui-ci:

/**
     * @Rest\Post("/", name="app_api_new_category")
     *
     * @ParamConverter("category", converter="fos_rest.request_body")
     *
     * @Rest\View(statusCode=201)
     *
     * @Doc\ApiDoc(
     *      section="Categories",
     *      description="Creates a new category.",
     *      statusCodes={
     *          201="Returned if category has been successfully created",
     *          400="Returned if errors",
     *          500="Returned if server error"
     *      }
     * )
     */
    public function postCategoryAction(Category $category, ConstraintViolationListInterface $violations)
    {
        if (count($violations)) {
            return $this->view($violations, 400);
        }

        $this->get('app_core.category_manager')->save($category);

        return $this->view(null, 201,
                [
                'Location' => $this->generateUrl('app_api_category', [ 'id' => $category->getId()]),
        ]);
    }

Plusieurs choses:

  • j’utilise la classe Symfony\Component\Validator\ConstraintViolationListInterface. N’oubliez pas de faire un use en haut du fichier
  • Je fais appel à un manager d’entité qui va sauver la nouvelle catégorie.

Le manager de Category

<?php

namespace App\CoreBundle\Entity\Manager;

use App\CoreBundle\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;

class CategoryManager
{
    public function __construct(ObjectManager $em) {
        $this->em = $em;
    }

    public function save(Category $category)
    {
        $this->em->persist($category);
        $this->em->flush();
    }
}

Ok, vous pourriez me dire, si c’était pour faire ça, l’utilité d’un manager était vraiment superflue.

En effet, vous auriez raison. Je l’ai fait dans l’hypothèse que j’y injecterai des dépendances autres que l’ObjectManager (comme le dispatcher). Je préfère le faire dès le début que de perdre du temps à créer mon manager par la suite.

L’interface ConstraintViolationListInterface

Celle-là, je la trouve puissante, car elle va aller analyser notre validation d’entité (par annotation, en php, etc.) pour valider l’objet sans que l’on ait à le faire dans le contrôleur. Et ça, c’est vraiment puissant.

Dans PostMan, nous transmettons alors un array avec le titre:

Nous voyons que le code 201 est renvoyé, avec l’indication « created ». Dans les headers, la location indique l’url de la nouvelle catégorie créée.

Le verbe PUT

Il est temps de pouvoir mettre à jour notre entité. Attention, j’attire votre attention sur le fait que PUT ne prend que les valeurs transmises et met à null les autres. Si vous voulez ne mettre à jour que certains champs et laisser les autres tels qu’ils étaient, vous devez utiliser PATCH. C’est une différence extrêmement importante. Ici, comme j’ai juste le titre, et qu’il est obligatoire, j’utilise PUT.

/**
     * @Rest\Put(
     *     path = "/{id}",
     *     name = " app_api_edit_category",
     *     requirements = {"id"="\d+"}
     * )
     *
     * @ParamConverter("apiCategory", converter="fos_rest.request_body")
     * 
     * @Doc\ApiDoc(
     *      section="Categories",
     *      description="Edit an existing category.",
     *      statusCodes={
     *          201="Returned if category has been successfully edited",
     *          400="Returned if errors",
     *          500="Returned if server error"
     *      },
     *      requirements={
     *          {
     *              "name"="id",
     *              "dataType"="integer",
     *              "requirement"="\d+",
     *              "description"="The category unique identifier."
     *          }
     *      },
     * )
     */
    public function putCategoryAction(Category $category, Category $apiCategory,
                                      ConstraintViolationListInterface $violations)
    {
        if (count($violations)) {
            return $this->view($violations, 400);
        }
        
        $this->get('app_core.category_manager')->update($category, $apiCategory);

        $em = $this->getDoctrine()->getManager();
        $em->flush($category);
        
        return $this->view('', Response::HTTP_NO_CONTENT);
    }

Ici, je récupère la category existante par l’id, et je récupère les informations passées dans la request par apiCategory.

Puis je fais appel au manager qui va mettre à jour les champs de Category avec ceux de ApiCategory.

<?php

namespace App\CoreBundle\Entity\Manager;

use App\CoreBundle\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;

class CategoryManager
{
    public function __construct(ObjectManager $em) {
        $this->em = $em;
    }

    public function update(Category $category, Category $apiCategory)
    {
        $category->update($apiCategory);

        $this->em->flush();
    }

    public function save(Category $category)
    {
        $this->em->persist($category);
        $this->em->flush();
    }
}

Et enfin, dans mon entité Category, je rajoute la fonction update:

public function update(Category $category)
    {
        $this->title = $category->title;
    }

postman_categorie_put

Nous avons un statut 204 no content, mais si nous regardons en base de données, nous avons bien le changement de titre et de slug.

Le verbe DELETE

Rien d’extraordinaire ici, nous continuons sur notre lancée:

/**
     * @Rest\Delete(
     *     path = "/{id}",
     *     name = "app_api_delete_category",
     *     requirements = {"id"="\d+"}
     * )
     * 
     * @Rest\View(statusCode=204)
     *
     * @Doc\ApiDoc(
     *      section="Categories",
     *      description="Delete an existing category.",
     *      statusCodes={
     *          201="Returned if category has been successfully deleted",
     *          400="Returned if category does not exist",
     *          500="Returned if server error"
     *      },
     *      requirements={
     *          {
     *              "name"="id",
     *              "dataType"="integer",
     *              "requirement"="\d+",
     *              "description"="The category unique identifier."
     *          }
     *      },
     * )
     */
    public function deleteCategoryAction(Category $category)
    {
        $this->get('app_core.category_manager')->remove($category);

        return $this->view('', Response::HTTP_NO_CONTENT);
    }

Et nous rajoutons cette fonction dans notre manager:

public function remove(Category $category)
    {
        $this->em->remove($category);
        $this->em->flush();
    }

postman_categorie_delete

Dans le prochain article, nous verrons comment customiser le rendu xml, faire des requêtes avec les paramsFetcher, paginer les résultats…

Le code source se trouve ici: https://github.com/jpsymfony/REST-BEHAT

Rédigé par

5 comments

  1. Bonjour,
    Comment passer en post une relation manytoone ou meme onetomany ?
    Dans le tuto si on passe une entité en objet json tout se passe bien. Si par contre on rajoute à cette entite une autre {‘name’:’entiteparente’, ‘manytone’:{‘id’:1,’name’:’relation’}} j’ai une erreur de type :
    A new entity was found through the relationship ‘Bunde\Entite#relation’ that was not configured to cascade persist operations for entity ‘Bunde\Entite’ etc…

    Pour enregistrer mon entité, je suis obligé de recuperer l’id de la relation et de faire un setRelation ().

    1. Bonjour,

      Désolé pour la très longue attente, j’ai été surchargé de travail et au CNAM, ça a été un peu difficile dernièrement. Dans ma mission actuelle, on a décidé que tout ce qui était sous ressource devait déjà exister, et donc qu’un ManyToOne était automatiquement persisté en passant uniquement un id existant. Par contre, si tu veux qu’en postant un parent, son enfant soit aussi persisté, en effet, il faut rajouter l’annotation cascade au niveau de l’entité parente, sinon, l’enregistrement ne se fait pas.

      Par exemple, ici, je crée une image en plus de créer un film, je ne sauve pas une image existante pour la rattacher au film.

      Par contre, , je ne fais pas de persist cascade car j’utilise une catégorie déjà existante dont l’id est passé dans le formulaire.

  2. Bonjour,
    J’essaye depuis quelque jours de comprendre mon erreur sur un post qui ne marche pas mais je n’arrive pas à comprendre après quelque recherche sur internet.
    Lorsque je post avec PostMan je tombe sur l’erreur suivante : «  »Converter ‘fos_rest.request_body’ does not support conversion of parameter ‘publications’. »
    Est-ce que je doit créer un ParamsConverters pour mon entity?
    J’ai bien activer la configuration :
    sensio_framework_extra:
    request: { converters: true }
    et dans ma config de rest :
    #FOSRestBundle
    fos_rest:
    body_listener:
    # Convert underscore case properties to camel case
    # ie: { « the_date »: « 2014-09-30 » } => { « theDate »: « 2014-09-30 » }
    array_normalizer: fos_rest.normalizer.camel_keys
    body_converter:
    enabled: true
    validate: true
    validation_errors_argument: violations
    view:
    view_response_listener: ‘force’
    mime_types:
    json:
    – application/json
    – application/x-json
    – application/vnd.app.categories+json
    – application/vnd.app.categories+json;v=1.0
    – application/vnd.app.categories+json;v=2.0
    xml:
    – text/xml
    – application/vnd.app.categories+xml
    – application/vnd.app.categories+xml;v=1.0
    – application/vnd.app.categories+xml;v=2.0
    formats: { json: true, xml: true, rss: false }
    param_fetcher_listener: true
    serializer:
    serialize_null: true
    format_listener:
    rules:
    – { path: ‘^/api’, priorities: [‘json’, ‘xml’], fallback_format: json, prefer_extension: false }
    – { path: ‘^/’, priorities: [ ‘html’], fallback_format: html }

    1. Bonjour,

      Je pense que dans postman, vous devez envoyer un payload avec une clef « publication » alors qu’il faudrait directement envoyer le payload avec les accolades sans mettre de clef.

      Attention pour la configuration, il faut remplacer categories par publications.

      Dites-moi si ça marche en faisant cela.

      1. Bonjour,

        Merci pour votre aide. En testant avec les modifications cela fonctionne toujours pas. voici les donnée envoyer par Postman :
        url :
        – « domaine »/app_dev.php/api/publications
        header:
        – Authorization : Bearer « le token valide »
        – Content-Type : application/json
        – Accept : application/json
        body :
        {
        « title »: »test avec Postman »,
        « content »: « super publication »
        }
        Dans Mon Controller:
        /*
        * @Rest\Post(« /publications »)
        * @Rest\View(statusCode=201)
        * @ParamConverter(« publications », converter= »fos_rest.request_body »)
        */
        public function postPublicationsAction(Publications $pubications, ConstraintViolationListInterface $violations){….}

        Les routes existantes avec debug:router :
        get_publications GET ANY ANY /api/publications.{_format}
        post_publications POST ANY ANY /api/publications.{_format}

        Mon entity Publication possède également une relation avec Medias le problème peut -il venir de la également?
        /**
        * @ORM\ManyToMany(targetEntity= »MediasBundle\Entity\Medias », fetch= »EAGER »)
        * @ORM\JoinTable(name= »publications_medias »,
        * joinColumns={@ORM\JoinColumn(name= »publications_id », referencedColumnName= »id »)},
        * inverseJoinColumns={@ORM\JoinColumn(name= »medias_id », referencedColumnName= »id »)}
        * )
        */
        private $medias;

        Pour la configuration j’ai remplacer par publications mais je me demande si le problème viens pas de la également.

        Voici le chemin de Publications : BusinessSocialsBundle\Entity\Publications
        Voici le chemin du controller : ApiBundle\Controller\PublicationsRestController

        Dans le mime_types de FOSREST :
        – application/vnd.app.publications+json

        Je me demande si app. devrais pas être remplacé par api. car il est dans Apibundle et pas dans AppBundle?

        Merci encore pour votre aide.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.