Live Coding : Créer une API avec Symfony 4

3 janvier 2020 - : API Tutoriel Symfony Live-Coding - : 3 commentaires - Tutoriel Controllers API Base de données MVC Symfony Live-Coding

Visualisez les fichiers de cette série sur GitHub

Lors de la création d'un site web, il peut être nécessaire d'accéder aux données au moyen d'une API pour lire ou écrire en base de données depuis du code Javascript, par exemple.

Dans cette session de Live Coding, nous avons vu comment créer cette API avec Symfony.

IMPORTANT

Le code traité dans cet article indique les concepts de programmation de cette API mais il est nécessaire de prendre en compte des techniques de sécurisation au moyen de tokens, par exemple.

API ?

Avant de définir ce qu'est une API REST, définissons API.

L'acronyme API signifie "Application Programming Interface" en Anglais, et peut être traduit simplement par "Interface de Programmation d'Application".

Bien sûr, ça ne vous aide pas à comprendre ce que c'est.

Pour rester simple, il s'agit de créer un accès à une application et permettre de venir y consommer des données ou des fonctionnalités. Ainsi, vous pouvez, avec une autre application ou un site, venir interroger l'application et utiliser ses données.

REST ?

L'acronyme REST, quant à lui, signifie "REpresentative State Transfer".

Le standard a été créé en 2000 par Roy Fielding, informaticien américain et cofondateur d'Apache, dans sa thèse.

Globalement, une API REST sera conforme au standard créé en 2000. Ce standard ayant créé un haut niveau de certification, il est très difficile d'être considéré comme "RESTful".

Mise en pratique

Nous allons traiter ici les bases de la création d'une API REST, en utilisant l'exemple de l'accès à une base de données d'articles de notre blog.

Lors de la création de notre API, nous devons garder en tête les critères qui en font une API REST. Une API REST se doit d'être :

  • Sans état : le serveur ne fait aucune relation entre les différents appels d'un même client. Il ne connaît pas l'état du client entre ces transactions

  • Cacheable : le client doit être capable de garder nos données en cache pour optimiser les transactions

  • Orienté client-serveur : Il nous faut une architecture client-serveur

  • Avec une interface uniforme : ceci permet à tout composant qui comprend le protocole HTTP de communiquer avec votre API

  • Avec un système de couches : avec des serveurs intermédiaires, le client final ne doit pas savoir si il est connecté au serveur principal ou à un serveur intermédiaire

Outils

Pour créer cette API, nous utiliserons Visual Studio Code et une extension appelée REST Client qui nous permettra de tester les requêtes et le fonctionnement de l'API.

Création du contrôleur

Pour gérer notre API, nous allons créer un contrôleur spécifique que nous allons appeler APIController. C'est dans ce contrôleur que nous allons créer les différentes méthodes permettant de lire, écrire et supprimer des données.

Pour créer ce contrôleur nous utiliserons la commande

php bin/console make:controller

Ensuite nous répondrons à la question en inscrivant "APIController"

Le fichier "APIController.php" a été créé. Nous allons le modifier comme suit

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/api", name="api_")
 */
class APIController extends AbstractController
{
    /**
     * @Route("/", name="api")
     */
    public function index()
    {

    }
}

Nous ajoutons une annotation avant la classe afin de donner une route par défaut pour toutes les méthodes de notre contrôleur. Nous avons également vidé temporairement la méthode "index"

Lecture des données

Pour commencer, nous allons regarder comment lire des données depuis la base de données en récupérant la liste complète des articles.

Dans une API REST, la lecture utilise la méthode HTTP GET et doit retourner une erreur 405 "Method Not Allowed" si une autre méthode HTTP est utilisée.

Nous allons donc modifier la méthode "index" de notre contrôleur que nous allons renommer "liste" afin qu'elle accepte uniquement la méthode GET.

/**
 * @Route("/articles/liste", name="liste", methods={"GET"})
 */
public function liste()
{

}

Cette méthode doit récupérer la liste complète des articles de la base de données, la convertir en json et la renvoyer.

Il y aura plusieurs façons de faire ceci, nous allons en traiter deux. L'une nous permettra de récupérer l'intégralité des informations d'un article, l'autre permettra de sélectionner les informations dont nous aurons besoin.

Obtenir l'intégralité des informations

Pour obtenir l'intégralité des informations des articles, nous utiliserons la méthode "findAll".

Cette méthode nous renvoyant une collection de données, nous allons devoir la convertir en json après l'avoir "normalisée", c'est à dire, convertie en tableau.

Pour ce faire, nous appellerons la classe "ObjectNormalizer", puis, pour la conversion en json, nous utiliserons la classe "Serializer".

La réponse envoyée sera accompagnée d'une entête HTTP "Content-type".

Nous allons donc avoir besoin de toutes ces classes, il va falloir les déclarer (automatique dans certains IDE)

use App\Repository\ArticlesRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

Puis nous allons écrire notre méthode comme suit

/**
 * @Route("/articles/liste", name="liste", methods={"GET"})
 */
public function liste(ArticlesRepository $articlesRepo)
{
    // On récupère la liste des articles
    $articles = $articlesRepo->findAll();

    // On spécifie qu'on utilise l'encodeur JSON
    $encoders = [new JsonEncoder()];

    // On instancie le "normaliseur" pour convertir la collection en tableau
    $normalizers = [new ObjectNormalizer()];

    // On instancie le convertisseur
    $serializer = new Serializer($normalizers, $encoders);

    // On convertit en json
    $jsonContent = $serializer->serialize($articles, 'json', [
        'circular_reference_handler' => function ($object) {
            return $object->getId();
        }
    ]);

    // On instancie la réponse
    $response = new Response($jsonContent);

    // On ajoute l'entête HTTP
    $response->headers->set('Content-Type', 'application/json');

    // On envoie la réponse
    return $response;
}

Vous aurez certainement remarqué une mention "circular_reference_handler". Ce paramètre passé dans la méthode "serialize" permet d'indiquer ce que l'encodeur doit faire en cas de référence circulaire, en d'autres termes, ce qu'il doit faire si un élément se référence lui même dans la collection, ce qui arrive fréquemment dans une structure relationnelle.

Sélectionner les informations souhaitées

Dans la partie précédente, nous avons sélectionné toutes les informations. Mais comment faire si, par exemple, nous souhaitons avoir uniquement l'id, le titre, le contenu, l'image et la date de création de notre article. Inutile dans ce cas de transmettre toutes les informations.

Pour faire ceci, nous allons aller dans le fichier "Repository" de l'entité concernée, pour nous "ArticlesRepository", et nous allons créer une méthode personnalisée qui retournera uniquement les informations souhaitées.

La méthode en question, nous l'appellerons, par exemple, "apiFindAll".

Cette méthode contiendra ceci

/**
 * @return Articles[] Returns an array of Articles objects
*/
public function apiFindAll() : array
{
    $qb = $this->createQueryBuilder('a')
        ->select('a.id', 'a.titre', 'a.contenu', 'a.featured_image', 'a.created_at')
        ->orderBy('a.created_at', 'DESC');

    $query = $qb->getQuery();

    return $query->execute();
}

Il nous suffira maintenant de modifier le "findAll" de la partie précédente comme ceci

/**
 * @Route("/articles/liste", name="liste", methods={"GET"})
 */
public function liste(ArticlesRepository $articlesRepo)
{
    // On récupère la liste des articles
    $articles = $articlesRepo->apiFindAll();

    // On spécifie qu'on utilise l'encodeur JSON
    $encoders = [new JsonEncoder()];

    // On instancie le "normaliseur" pour convertir la collection en tableau
    $normalizers = [new ObjectNormalizer()];

    // On instancie le convertisseur
    $serializer = new Serializer($normalizers, $encoders);

    // On convertit en json
    $jsonContent = $serializer->serialize($articles, 'json', [
        'circular_reference_handler' => function ($object) {
            return $object->getId();
        }
    ]);

    // On instancie la réponse
    $response = new Response($jsonContent);

    // On ajoute l'entête HTTP
    $response->headers->set('Content-Type', 'application/json');

    // On envoie la réponse
    return $response;
}

Et le tour est joué.

Sélectionner 1 seul article

Si nous souhaitons accéder aux informations d'un seul article, nous utiliserons globalement la même procédure, à la différence que nous passerons l'id de l'article dans l'url et que nous récupérerons donc l'information dans notre méthode.

La méthode de notre contrôleur, que nous appellerons "getArticle" par exemple sera la suivante

/**
 * @Route("/article/lire/{id}", name="article", methods={"GET"})
 */
public function getArticle(Articles $article)
{
    $encoders = [new JsonEncoder()];
    $normalizers = [new ObjectNormalizer()];
    $serializer = new Serializer($normalizers, $encoders);
    $jsonContent = $serializer->serialize($article, 'json', [
        'circular_reference_handler' => function ($object) {
            return $object->getId();
        }
    ]);
    $response = new Response($jsonContent);
    $response->headers->set('Content-Type', 'application/json');
    return $response;
}

Comme vous pouvez le voir, aucune différence notable.

Ajouter des données

Dans une API REST, l'ajout de données doit passer par la méthode HTTP POST et retourner un code de réponse HTTP 201 "Created" lorsque l'ajout a fonctionné.

La méthode, que nous appellerons "addArticle" par exemple, contiendra le code suivant

/**
 * @Route("/article/ajout", name="ajout", methods={"POST"})
 */
public function addArticle(Request $request)
{
    // On vérifie si la requête est une requête Ajax
    if($request->isXmlHttpRequest()) {
        // On instancie un nouvel article
        $article = new Articles();

        // On décode les données envoyées
        $donnees = json_decode($request->getContent());

        // On hydrate l'objet
        $article->setTitre($donnees->titre);
        $article->setContenu($donnees->contenu);
        $article->setFeaturedImage($donnees->image);
        $user = $this->getDoctrine()->getRepository(Users::class)->findOneBy(["id" => 1]);
        $article->setUsers($user);

        // On sauvegarde en base
        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($article);
        $entityManager->flush();

        // On retourne la confirmation
        return new Response('ok', 201);
    }
    return new Response('Failed', 404);
}

ATTENTION : ceci est le code minimal, il conviendra d'ajouter des contrôles de données avant d'hydrater notre objet.

Modifier des données

Dans une API REST, la modification de données doit passer par la méthode HTTP PUT.

Cette méthode PUT doit modifier un article si il existe et retourner un code de réponse HTTP 200 Ok, et créer un article si il n'existe pas et retourner un code de réponse HTTP 201 "Created" lorsque l'ajout a fonctionné.

La méthode, que nous appellerons "editArticle" par exemple, contiendra le code suivant

/**
 * @Route("/article/editer/{id}", name="edit", methods={"PUT"})
 */
public function editArticle(?Articles $article, Request $request)
{
    // On vérifie si la requête est une requête Ajax
    if($request->isXmlHttpRequest()) {

        // On décode les données envoyées
        $donnees = json_decode($request->getContent());

        // On initialise le code de réponse
        $code = 200;

        // Si l'article n'est pas trouvé
        if(!$article){
            // On instancie un nouvel article
            $article = new Articles();
            // On change le code de réponse
            $code = 201;
        }

        // On hydrate l'objet
        $article->setTitre($donnees->titre);
        $article->setContenu($donnees->contenu);
        $article->setFeaturedImage($donnees->image);
        $user = $this->getDoctrine()->getRepository(Users::class)->find(1);
        $article->setUsers($user);

        // On sauvegarde en base
        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($article);
        $entityManager->flush();

        // On retourne la confirmation
        return new Response('ok', $code);
    }
    return new Response('Failed', 404);
}

ATTENTION : ceci est le code minimal, il conviendra d'ajouter des contrôles de données avant d'hydrater notre objet.

Supprimer des données

Enfin, dans une API REST, la suppression de données doit passer par la méthode HTTP DELETE et retourner un code de réponse HTTP 200 Ok lorsque la suppression a fonctionné.

La méthode, que nous appellerons "removeArticle" par exemple, contiendra le code suivant

/**
 * @Route("/article/supprimer/{id}", name="supprime", methods={"DELETE"})
 */
public function removeArticle(Articles $article)
{
    $entityManager = $this->getDoctrine()->getManager();
    $entityManager->remove($article);
    $entityManager->flush();
    return new Response('ok');
}

ATTENTION : ceci est le code minimal, il conviendra d'ajouter des contrôles de données avant d'hydrater notre objet.

Obtenir de l'aide

Pour obtenir de l'aide, vous pouvez accéder au serveur Discord pour une entraide par chat

Visualisez les fichiers de cette série sur GitHub

Partager

Partager sur Facebook Partager sur Twitter Partager sur LinkedIn

Commentaires

Ecrire un commentaire

Bao a écrit le 27 mai 2020 à 13:15

Bonjour à vous et merci pour ce tuto. Dans ce cas lors de l'ajout d'un article vous avez définis la variable de clé étrangère $user à 1 par exemple, que faire si j'ai deux tables article et commentaire et que je veuilles automatiquement ajouter des commentaires selon un artice précis, c'est à dire comment définir le setter de l'antité article qui se trouverait dans l'antité commentaire. Merci

Répondre

mastou a écrit le 8 mai 2020 à 02:48

Bonjour, j'aimerais savoir concernant la route GET permettant la lecture de données, il n'y a pas besoin de retourner un code 200 en cas de succès et un autre en cas d'echec ?

Répondre

Nouvelle-Techno.fr a répondu le 8 mai 2020 à 09:11

Bonjour,

Si, c'est obligatoire. C'est le code de réponse par défaut si il n'est pas précisé.

Répondre

Ecrire un commentaire