Consommer une API avec HttpClient et Symfony 5 : Les chiffres du COVID

Introduction

Aujourd’hui nous allons voir comment consommer une API avec le HttpClient de Symfony ! Vous allez le voir, pour l’usage simple que nous allons en faire, c’est un jeu d’enfant !

Comme d’habitude, pour que la démonstration soit parlante et pour que vous puissiez facilement vous projeter sur de vrais usages, nous allons mettre en place un petit projet complet.

Notre application permettra de consulter quelques chiffres sur la COVID-19.

  • À l’échelle du pays (pour notre exemple la France)
    • Nombre d’hospitalisations
    • Nombre de personnes réa
    • Nombre total de décès 😥
    • Nombre totale de guéris
    • Nombre de cas confirmés
    • Source des données
  • À l’échelle d’un département
    • Nombre d’hospitalisations
    • Nombre de personnes réa
    • Nombre de nouvelles hospitalisation, graphique sur 7 jours glissants
    • Nombre de nouvelles réa, graphique sur 7 jours glissants
    • Source des données

Pour obtenir les chiffres, nous utiliserons cette API: https://www.data.gouv.fr/fr/reuses/coronavirusapi-france/

Initialisation du projet Symfony

Installons un nouveau projet Symfony, et démarrons le serveur Symfony

symfony new covid --full
cd covid
symfony serve -d

Bootstrap (via CDN)

Mettons un peu de style en utilisant Bootstrap 5 (via CDN), et ajoutons-y un peu de mise en page. Pour cela éditons le fichier templates/base.html.twig.

<!DOCTYPE html>
<html lang="fr">
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<meta charset="UTF-8">
		<title>Covid Stats |
			{% block title %}{% endblock %}
		</title>
		{% block stylesheets %}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
		{% endblock %}
	</head>

	<body>
		<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
			<div class="container-fluid">
				<a class="navbar-brand" href="/">Covid Stats</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>
				<div class="collapse navbar-collapse" id="navbarsExampleDefault">
					<ul class="navbar-nav me-auto mb-2 mb-md-0">
						<li class="nav-item active">
							<a class="nav-link" aria-current="page" href="/#france">Home</a>
						</li>
						<li class="nav-item">
							<a class="nav-link" href="/#department">Par départements</a>
						</li>
					</ul>
				</div>
			</div>
		</nav>

        <main class="container">
            <div class="text-center py-5 px-3">
                {% block body %}{% endblock %}
            </div>
        </main>

		{% block javascripts %}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
		{% endblock %}
	</body>
</html>

Création de la Home

Créons-nous une page “Home”.

symfony console make:controller Home

Testons que tout fonctionne bien !

Création d’un service pour interroger l’API : Données pour la France entière

Pour notre application nous allons avoir besoin de faire pleins d’appels différents à l’API, pour éviter de dupliquer du code, nous allons mettre en place un service dédié pour récupérer les informations de notre API.

Créons un répertoire Service dans ./src. Puis créons un fichier CallApiService.php. Pour le moment nous faisons le minium pour tester que notre service fonctionne.

<?php

namespace App\Service;

class CallApiService
{
    public function getFranceData(): array
    {
        return ['test', 'test2'];
    }
}

Et utilisons ce service dans notre contrôleur HomeController.php.

 public function index(CallApiService $callApiService): Response
    {
        dd($callApiService->getFranceData());

        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }

Et actualisons la page d’accueil, nous devrions voir notre Die & Dump 😉

Modifions notre service pour réaliser notre première appel API et retourner le résultat sous forme de tableau grâce à la méthode toArray() fournit par HttpClient.

<?php

namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class CallApiService
{
    private $client;

    public function __construct(HttpClientInterface $client)
    {
        $this->client = $client;
    }

    public function getFranceData(): array
    {
        $response = $this->client->request(
            'GET',
            'https://coronavirusapi-france.now.sh/FranceLiveGlobalData'
        );

        return $response->toArray();
    }
}

Rafraichissons notre homepage ! Superbe, nous récupérons bien les données pour la France !

Mise en page des données de la France entière

Maintenant que nous somme capable de récupérer les données de la France entière, il ne nous reste plus qu’à mètre cela en forme.

Commençons par envoyer cela à notre vue Twig en modifiant notre contrôleur HomeController.php.

public function index(CallApiService $callApiService): Response
    {
        return $this->render('home/index.html.twig', [
            'data' => $callApiService->getFranceData(),
        ]);
    }

Puis faisons une (très) rapide mise en page avec Bootstrap.

{% extends 'base.html.twig' %}

{% block title %}Les chiffres du COVID-19 en France
{% endblock %}

{% block body %}
	<div class="album py-5" id="france">
		<div class="container">
            <h1 class="mb-5">Les chiffres du COVID-19 en France</h1>

			<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">

				<div class="col">
					<div class="card shadow-sm h-100">
						<div class="card-body">
							<h5 class="card-title">Nombre d’hospitalisations</h5>
							<p class="card-text">Nombre de personnes actuellement hospitalisée</p>
						</div>
                        <h3 class="card-footer bg-warning text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.hospitalises }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre de Réa</h5>
							<p class="card-text">Nombre de personnes actuellement en réanimation</p>
						</div>
                        <h3 class="card-footer bg-warning text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.reanimation }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre total de décès 😥</h5>
							<p class="card-text">Nombre de personnes décédés depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-dark text-light mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.deces + data.FranceGlobalLiveData.0.decesEhpad }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre totale de guéris</h5>
							<p class="card-text">Nombre de personnes guéris depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-success text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.gueris }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre de cas confirmés</h5>
							<p class="card-text">Nombre de cas confirmés depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-info text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.casConfirmes + data.FranceGlobalLiveData.0.casConfirmesEhpad }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Source des données</h5>
							<p class="card-text">Source des données :<br>{{ data.FranceGlobalLiveData.0.sourceType }}</p>
						</div>
                        <h3 class="card-footer bg-secondary text-grey mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.date }}</h3>
					</div>
				</div>

			</div>
		</div>
	</div>
{% endblock %}

Rafraichissons notre page d’accueil, et apprécions le résultat 😉

Mise en page des données par département (Sur la homepage)

Toujours sur notre page d’accueil, nous allons afficher quelques chiffres par département (et plus tard nous ferons une page dédié par département).

Commençons par écrire l’appel API qui récupère toutes les données des départements dans notre Service/CallApiService.php. On en profite pour refactoriser un peu notre code.

<?php

namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class CallApiService
{
    private $client;

    public function __construct(HttpClientInterface $client)
    {
        $this->client = $client;
    }

    public function getFranceData(): array
    {
        return $this->getApi('FranceLiveGlobalData');
    }

    public function getAllData(): array
    {
        return $this->getApi('AllLiveData');
    }

    private function getApi(string $var)
    {
        $response = $this->client->request(
            'GET',
            'https://coronavirusapi-france.now.sh/' . $var
        );

        return $response->toArray();
    }
}

Envoyons les données à notre vue dans le fichier HomeController.php.

public function index(CallApiService $callApiService): Response
    {
        return $this->render('home/index.html.twig', [
            'data' => $callApiService->getFranceData(),
            'departments' => $callApiService->getAllData(),
        ]);
    }

Et mettons cela en page dans le fichier templates/home/index.html.twig.

{% extends 'base.html.twig' %}

{% block title %}Les chiffres du COVID-19 en France
{% endblock %}

{% block body %}
	<div class="album py-5" id="department">
		<div class="container">
            <h1 class="mb-5">Les chiffres du COVID-19 en France</h1>

			<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">

				<div class="col">
					<div class="card shadow-sm h-100">
						<div class="card-body">
							<h5 class="card-title">Nombre d’hospitalisations</h5>
							<p class="card-text">Nombre de personnes actuellement hospitalisée</p>
						</div>
                        <h3 class="card-footer bg-warning text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.hospitalises }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre de Réa</h5>
							<p class="card-text">Nombre de personnes actuellement en réanimation</p>
						</div>
                        <h3 class="card-footer bg-warning text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.reanimation }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre total de décès 😥</h5>
							<p class="card-text">Nombre de personnes décédés depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-dark text-light mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.deces + data.FranceGlobalLiveData.0.decesEhpad }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre totale de guéris</h5>
							<p class="card-text">Nombre de personnes guéris depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-success text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.gueris }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Nombre de cas confirmés</h5>
							<p class="card-text">Nombre de cas confirmés depuis le début de la pandémie</p>
						</div>
                        <h3 class="card-footer bg-info text-dark mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.casConfirmes + data.FranceGlobalLiveData.0.casConfirmesEhpad }}</h3>
					</div>
				</div>

                <div class="col">
					<div class="card shadow-sm">
						<div class="card-body">
							<h5 class="card-title">Source des données</h5>
							<p class="card-text">Source des données :<br>{{ data.FranceGlobalLiveData.0.sourceType }}</p>
						</div>
                        <h3 class="card-footer bg-secondary text-grey mx-2 rounded-pill">{{ data.FranceGlobalLiveData.0.date }}</h3>
					</div>
				</div>

			</div>
		</div>
	</div>

	<div class="album py-5">
		<div class="container">
            <h1 class="mb-5">Les chiffres du COVID-19 par département</h1>

			<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
				{% for department in departments.allLiveFranceData %}
					<div class="col">
						<div class="card shadow-lg h-100 m-2">
							<div class="card-body">
								<h5 class="card-title">{{ department.nom }}</h5>
								<p class="card-text">Actuellement hospitalisée<span class="badge bg-secondary">{{ department.hospitalises }}</span></p>
								<p class="card-text">Actuellement en réa<span class="badge bg-secondary">{{ department.reanimation }}</span></p>
								<a href="#" class="btn btn-secondary" role="button" aria-pressed="true">Plus de chiffres</a>
							</div>
						</div>
					</div>
				{% endfor %}
			</div>
		</div>
	</div>
	
{% endblock %}

Actualisons la page d’accueil, ça marche !

Mise en page des données par département (sur une page dédiée)

Modifions notre service CallApiService.php pour récupérer les données d’un seul département.

public function getDepartmentData($department): array
    {
        return $this->getApi('LiveDataByDepartement?Departement=' . $department);
    }

Créons-nous un contrôleur pour la gestion des détails d’un département.

symfony console make:controller Department

Éditons le fichier src/Controller/DepartmentController.php.

<?php

namespace App\Controller;

use App\Service\CallApiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DepartmentController extends AbstractController
{
    /**
     * @Route("/department/{department}", name="app_department")
     */
    public function index(string $department, CallApiService $callApiService): Response
    {
        return $this->render('department/index.html.twig', [
            'data' => $callApiService->getDepartmentData($department),
        ]);
    }
}

Et affichons les informations dans notre vue Twig, en éditant le fichier templates/department/index.html.twig.

{% extends 'base.html.twig' %}

{% block title %}Les chiffres du COVID-19|{{ data.LiveDataByDepartement.0.nom }}
{% endblock %}

{% block body %}
	<div class="px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
		<h1 class="display-4">Les chiffres du COVID-19 -
			{{ data.LiveDataByDepartement.0.nom }}</h1>
		<p class="lead">Source :
			{{ data.LiveDataByDepartement.0.sourceType }}</p>
		<p class="lead">Date d'actualisation :
			{{ data.LiveDataByDepartement.0.date }}</p>
	</div>

	<div class="row text-center">
		<div class="col">
			<div class="card mb-4 shadow-sm">
				<div class="card-header">
					<h4 class="my-0 fw-normal">Nombre d’hospitalisations</h4>
				</div>
				<div class="card-body">
					<h1 class="card-title pricing-card-title">{{ data.LiveDataByDepartement.0.hospitalises }}</h1>
				</div>
			</div>
		</div>
		<div class="col">
			<div class="card mb-4 shadow-sm">
				<div class="card-header">
					<h4 class="my-0 fw-normal">Nombre de personnes réa</h4>
				</div>
				<div class="card-body">
					<h1 class="card-title pricing-card-title">{{ data.LiveDataByDepartement.0.reanimation }}</h1>
				</div>
			</div>
		</div>
	</div>
{% endblock %}

Mettons à jour notre template templates/home/index.html.twig pour faire le lien vers les pages des départements.

{% for department in departments.allLiveFranceData %}
					<div class="col">
						<div class="card shadow-lg h-100 m-2">
							<div class="card-body">
								<h5 class="card-title">{{ department.nom }}</h5>
								<p class="card-text">Actuellement hospitalisée<span class="badge bg-secondary">{{ department.hospitalises }}</span></p>
								<p class="card-text">Actuellement en réa<span class="badge bg-secondary">{{ department.reanimation }}</span></p>
								<a href="{{ path('app_department', {'department': department.nom }) }}" class="btn btn-secondary" role="button" aria-pressed="true">Plus de chiffres</a>
							</div>
						</div>
					</div>
				{% endfor %}

Et testons de suivre le lien vers un département.

Les graphiques sur une semaine glissante

Il ne nous reste plus qu’à générer nos graphiques sur une semaine glissante pour en avoir terminé 😉 Pour cela nous allons utiliser les possibilités offerte par Symfony UX et Chart.js.

Commençons par installer Webpack Encore et Chat.js pour Symfony UX.

composer require symfony/webpack-encore-bundle
composer require symfony/ux-chartjs
npm install --force

Modifions le fichier assets/styles/app.css pour le vider complètement (le fichier doit être vide). Lançons un build.

npm run build

Puis éditons le fichier base.html.twig pour qu’il utilise les fichiers de webpack Encore.

		{% block stylesheets %}
			<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
			{{ encore_entry_link_tags('app') }}
		{% endblock %}

		{% block javascripts %}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
			{{ encore_entry_script_tags('app') }}
		{% endblock %}

Modifions (encore une fois) notre service CallApiService.php.

 public function getAllDataByDate($date): array
    {
        return $this->getApi('AllDataByDate?date=' . $date);
    }

Et créons les tableaux de données pour Chart.js dans DepartmentController.php envoyons-les à la vue twig.

<?php

namespace App\Controller;

use App\Service\CallApiService;
use DateTime;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
use Symfony\UX\Chartjs\Model\Chart;

class DepartmentController extends AbstractController
{
    /**
     * @Route("/department/{department}", name="app_department")
     */
    public function index(string $department, CallApiService $callApiService, ChartBuilderInterface $chartBuilder): Response
    {
        $label = [];
        $hospitalisation = [];
        $rea = [];

        for ($i=1; $i < 8; $i++) { 
            $date = New DateTime('- '. $i .' day');
            $datas = $callApiService->getAllDataByDate($date->format('Y-m-d'));

            foreach ($datas['allFranceDataByDate'] as $data) {
                if( $data['nom'] === $department) {
                    $label[] = $data['date'];
                    $hospitalisation[] = $data['nouvellesHospitalisations'];
                    $rea[] = $data['nouvellesReanimations'];
                    break;
                }
            }
        }

        $chart = $chartBuilder->createChart(Chart::TYPE_LINE);
        $chart->setData([
            'labels' => array_reverse($label),
            'datasets' => [
                [
                    'label' => 'Nouvelles Hospitalisations',
                    'borderColor' => 'rgb(255, 99, 132)',
                    'data' => array_reverse($hospitalisation),
                ],
                [
                    'label' => 'Nouvelles entrées en Réa',
                    'borderColor' => 'rgb(46, 41, 78)',
                    'data' => array_reverse($rea),
                ],
            ],
        ]);

        $chart->setOptions([/* ... */]);

        return $this->render('department/index.html.twig', [
            'data' => $callApiService->getDepartmentData($department),
            'chart' => $chart,
        ]);
    }
}

Et enfin modifions le fichier templates/department/index.html.twig pour y ajouter le graphique Chatjs.

{% extends 'base.html.twig' %}

{% block title %}Les chiffres du COVID-19|{{ data.LiveDataByDepartement.0.nom }}
{% endblock %}

{% block body %}
	<div class="px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
		<h1 class="display-4">Les chiffres du COVID-19 -
			{{ data.LiveDataByDepartement.0.nom }}</h1>
		<p class="lead">Source :
			{{ data.LiveDataByDepartement.0.sourceType }}</p>
		<p class="lead">Date d'actualisation :
			{{ data.LiveDataByDepartement.0.date }}</p>
	</div>

	<div class="card mb-5 shadow-lg">
		<div class="card-body">
			{{ render_chart(chart) }}
		</div>
	</div>


	<div class="row text-center">
		<div class="col">
			<div class="card mb-4 shadow-sm mt-4">
				<div class="card-header">
					<h4 class="my-0 fw-normal">Nombre d’hospitalisations</h4>
				</div>
				<div class="card-body">
					<h1 class="card-title pricing-card-title">{{ data.LiveDataByDepartement.0.hospitalises }}</h1>
				</div>
			</div>
		</div>
		<div class="col">
			<div class="card mb-4 shadow-sm mt-4">
				<div class="card-header">
					<h4 class="my-0 fw-normal">Nombre de personnes réa</h4>
				</div>
				<div class="card-body">
					<h1 class="card-title pricing-card-title">{{ data.LiveDataByDepartement.0.reanimation }}</h1>
				</div>
			</div>
		</div>
	</div>

{% endblock %}

Et chargeons la page d’un département ! Tou Doum !

Publier notre application

Nous l’avions détaillé dans une vidéo dédiée, alors sans trop de détails, nous allons publier notre application avec Heroku.

heroku create
echo 'web: heroku-php-apache2 public/' > Procfile
heroku config:set APP_ENV=prod
composer require symfony/apache-pack
heroku buildpacks:add --index 2 heroku/nodejs

Dans le fichier package.json, ajouter les instructions pour faire le build.

(...)

"scripts": {
        (...)
        "heroku-postbuild" : "node_modules/.bin/encore production"
    }

git push heroku master

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.