Construire sa propre API avec Symfony

C

Avec les frameworks javascript on a de plus en plus besoin d’avoir accès aux données via une API, et ça sans forcement vouloir sortir l’artillerie lourde (je pense entre autre à l’excellent api-platform)

L’API que nous allons développer sera ultra simple (il n’y aura par exemple pas d’authentification, peu de gestion des erreurs, pas de versionning, …).

Elle contiendra 5 routes qui permettra de gérer des livres :

  • GET : /api/livre/{id] : Lecture d’un seul livre
  • GET : /api/livres : Lecture de tous les livres
  • POST : /api/livre : création d’un seul livre
  • PUT : /api/livre/{id} : modification d’un seul livre
  • DELETE : /api/livre/{id} : suppression d’un livre

Sans plus attendre nous allons créer notre projet (pour ma part PHP 8 et Symfony 6) :

symfony new custom-api
symfony server:start

Ce qui devrait nous donner comme page :

La base de données

On avoir besoin d’une base de données et donc des composants pour pouvoir y accéder.
On va aussi utiliser l’indispensable maker-bundle.

composer require orm
composer require --dev maker-bundle

Vous pourrez ensuite renseigner dans le fichier .env.local vos paramètres de connexion :

DATABASE_URL=”mysql://login:mdp@localhost:3306/customApi?serverVersion=5.7″

Et on va créer notre 1ere entité, on va partir sur quelque chose de simple, un livre !
Il y aura juste un titre, un auteur et le nombre de page (histoire d’avoir un entier).

bin/console make:entity Livre

bin/console doctrine:database:create
bin/console make:migration
bin/console doctrine:migrations:migrate

Les fixtures

On va se créer quelques fixtures :

composer require --dev orm-fixtures
composer require fzaninotto/faker --dev
bin/console make:fixtures
class LivreFixtures extends Fixture
{
    protected Generator $faker;

    public function load(ObjectManager $manager): void
    {
        $this->faker = Factory::create('fr_FR');

        for ($i = 0; $i < 50; $i++) {
            $livre = new Livre();
            $livre->setAuteur($this->faker->firstName.' '.$this->faker->lastName);
            $livre->setTitre($this->faker->realText($this->faker->numberBetween(10,100)));
            $livre->setNbPage(rand(50,999));
            $manager->persist($livre);
        }

        $manager->flush();
    }
}
bin/console doctrine:fixtures:load

Avec ce petit bout de code vous devriez vous retrouver avec 50 beaux livres :

GET

On va commencer par le plus simple, la méthode GET qui va nous permettre de lire une entité.
Pour ça on va créer un controller (ce dernier contiendra d’ailleurs toutes nos méthodes)
On aussi avoir d’un composant indispensable pour sérialiser nos entités :

composer req symfony/serializer
composer req symfony/property-access
bin/console make:controller LivreController
class LivreController extends AbstractController
{
    private EntityManagerInterface $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    #[Route('/api/livre/{livre}', name: 'get_livre', methods: ['GET'])]
    public function get($livre): Response
    {
        $livre = $this->entityManager->getRepository(Livre::class)->findOneBy(['id'=>$livre]);

        if($livre===null){
            return $this->json(null,Response::HTTP_NOT_FOUND);
        }

        return $this->json($livre, Response::HTTP_OK);
    }
}

Cette fonction est très simple, vous pouvez tester directement dans votre navigateur ou dans un logiciel pour faire des requêtes, type POSTMAN, personnellement j’utilise Insomnia :

on peut aussi ajouter une fonction pour lire tous les livres :

#[Route('/api/livres', name: 'get_livres', methods: ['GET'])]
public function getAll(): Response
{
        $livres = $this->entityManager->getRepository(Livre::class)->findAll();

        $data = [];
        foreach($livres as $livre){
            $data[] = $livre;
        }

        return $this->json($data,Response::HTTP_OK);
}

Création

La création n’est pas beaucoup plus compliqué, à la différence de la lecture, on reçoit les champs dans le body de la requête :

#[Route('/api/livre', name: 'add_livre', methods: ['POST'])]
public function add(Request $request): Response
{
         $data = json_decode($request->getContent(), true);

        if(empty($data['titre']) || empty($data['auteur']) || empty($data['nbPage'])){
            return $this->json(['message'=>'Tous les champs doivent être renseignés'],Response::HTTP_BAD_REQUEST);
        }
        $titre = $data['titre'];
        $auteur = $data['auteur'];
        $nbPage = $data['nbPage'];

        $newLivre = new Livre();
        $newLivre->setTitre($titre)->setAuteur($auteur)->setNbPage($nbPage);

        $this->entityManager->persist($newLivre);
        $this->entityManager->flush();

        return $this->json(['message'=>'Livre créé'],Response::HTTP_CREATED);
}

Mise à jour

Vous avez 2 méthodes de mise à jour : PUT et PATCH, PUT met à jour l’ensemble de l’objet, PATCH ne met à jour que les champs que vous souhaitez.

#[Route('/api/livre/{livre}', name: 'put_patch_livre', methods: ['PUT','PATCH'])]
public function putPatch(Request $request, $livre): Response
{
$data = json_decode($request->getContent(), true);

$livreObject = $this->entityManager->getRepository(Livre::class)->findOneBy(['id'=>$livre]);

if($livreObject===null){
throw $this->createNotFoundException(sprintf(
'Pas de livre trouvé "%s"',
$livre
));
}

if($request->getMethod()==='PUT' and (empty($data['titre']) || empty($data['auteur']) || empty($data['nbPage']))){
return $this->json(['message'=>'Tous les champs doivent être renseignés'],Response::HTTP_BAD_REQUEST);
}

if(!empty($data['titre'])) $livreObject->setTitre($data['titre']);
if(!empty($data['auteur'])) $livreObject->setAuteur($data['auteur']);
if(!empty($data['nbPage'])) $livreObject->setNbPage($data['nbPage']);

$this->entityManager->flush();

return $this->json($livreObject,Response::HTTP_OK);
}

Suppression

C’est la méthode DELETE qui se charge de ça, si un livre n’est pas trouvé vous pouvez retourner une erreur 404 ou faire comme si le delete s’est bien passé (code 204)

#[Route('/api/livre/{livre}', name: 'delete_livre', methods: ['DELETE'])]
public function delete(Request $request, $livre): Response
{
$livreObject = $this->entityManager->getRepository(Livre::class)->findOneBy(['id'=>$livre]);

if($livreObject===null){
throw $this->createNotFoundException(sprintf(
'Pas de livre trouvé "%s"',
$livre
));
}

$this->entityManager->remove($livreObject);
$this->entityManager->flush();

return $this->json(null,Response::HTTP_NO_CONTENT);
}

Voilà, j’espère que cet article vous aura bien aidé, vous pourrez trouver le source sur mon github :

https://github.com/gponty/custom-api

About the author

Guillaume

2 comments

Répondre à Guillaume Cancel reply

By Guillaume

Guillaume

Get in touch

Quickly communicate covalent niche markets for maintainable sources. Collaboratively harness resource sucking experiences whereas cost effective meta-services.