· 11 min read

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

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 !

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 !

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.

<?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.

<?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.

<?php

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.

<?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
Back to Blog