Kaherecode

Créer une application web avec Symfony - Les contrôleurs

Mamadou Aliou Diallo
@alioukahere 1 sept. 2021
677

Hey salut, bienvenue dans cette deuxième partie de la série Créer une application web avec Symfony. Dans ce tutoriel, nous allons parler des contrôleurs et créer les contrôleurs de notre application.

Mais avant, il faut récupérer les sources de la dernière partie et te placer sur le tag introduction pour pouvoir pratiquer:

$ git clone https://github.com/kaherecode/symfony-ticketing-app.git
$ cd symfony-ticketing-app
$ git checkout introduction
$ composer install

Il faudra faire cela avant de commencer chaque tutoriel dans cette série, seul la troisième ligne va changer, cela va te permettre d'être au même niveau que moi sur les sources pour que tu puisses pratiquer.

Aller on commence tout de suite.

Symfony et MVC

Symfony repose sur un modèle MVC (Model - View - Controller ou Modèle - Vue - Contrôleur), et le contrôleur joue un rôle central dans cette architecture. Il est chargé de recevoir la requête du client, charger les modèles nécessaires depuis une source de données puis construire la vue et la retourner à l'utilisateur.

On dit qu'une image vaut mieux que milles mots, alors voici:

https://imgur.com/jiXXwXg.jpg Architecture MVC

Voici un peu une image de l'architecture MVC, le contrôleur reçoit une requête HTTP, récupère les données nécessaires et retourne une réponse soit en HTML, JSON ou autres.

C'est donc sur cet architecture que repose Symfony, avec une couche supérieur. Avec Symfony, toutes les requêtes passent par un premier contrôleur, le Front Controller (Contrôleur Frontal). Le Front Controller de Symfony agit un peu comme un aiguilleur, toutes les requêtes qui arrivent sur notre application passent par ce contrôleur, le contrôleur frontal va ensuite appeler le contrôleur qui va traiter la requête reçu.

Une image vaut toujours mieux que milles mots, alors regarde un peu l'image ci-dessous:

https://imgur.com/S09yPJk.jpg Le contrôleur frontal de Symfony

Concrètement, qu'est-ce qu'il se passe?

Prenons l'exemple de cette page sur laquelle tu te trouves, ici même, en train de lire ce tutoriel, lorsque tu as rentrer l'URL de cet article dans ton navigateur (Chrome, Firefox, ...), qui représente le client, une requête HTTP a été envoyer par ton navigateur, puis, par la magie d'internet et du DNS, cette requête a atteint le serveur de Kaherecode, de là, le contrôleur frontal vérifie la route qui compose l'URL, /tutorial dans ce cas, il va donc vérifier qu'il y a un contrôleur associer à cette route, si c'est le cas, il appelle donc ce contrôleur qui va charger le modèle nécessaire (le tutoriel que tu es en train de lire), puis construire la vue avec le contenu du tutoriel. Une fois la vue construite, le contrôleur va retourner la vue (une page HTML dans notre cas) au contrôleur frontal, et enfin le contrôleur frontal va retourner la page HTML à ton navigateur qui va te l'afficher pour que tu puisses lire ce tutoriel. Mais qu'est-ce qu'il se passe si aucun contrôleur n'est associé à la route? Tu as alors une erreur 404, que tu dois connaître qui indique que l'URL que tu cherches n'existe pas sur ce serveur.

Voici un peu comment fonctionne le contrôleur frontal de Symfony. Le contrôleur frontal c'est le fichier public/index.php , c'est donc ce fichier le point d'entrée de notre application Symfony.

Les contrôleurs

En Symfony, le contrôleur est une fonction PHP qui est relié à une route. Une route sert à faire le lien entre l'URL de la requête et le contrôleur. Les contrôleurs sont définis dans le dossier src/Controller, tu peux donc toi même créer une classe PHP dans ce dossier et y définir tes contrôleurs, ou tu peux me laisser te montrer une méthode paresseuse simple pour le faire. Pour cela, il faut simplement exécuter la commande:

$ symfony console make:controller CoreController

Je choisis d'appeler cette première classe CoreController, le choix du nom de la classe est personnel, mais il faut que le nom est le suffixe Controller. Et il faut essayer d'organiser les contrôleurs dans différents classes, ce que j'aime faire par exemple, c'est de définir une classe CoreController qui va contenir les contrôleurs de base de mon application comme la page d'accueil, une page contact, ... ensuite avoir une classe EventController par exemple, qui va contenir les contrôleurs sur la gestion des évènements et ainsi de suite. Ceci est une organisation personnel, j'ai commencé par organiser mes contrôleurs en bundles avec Symfony 3, puis à mettre tout mes contrôleurs dans une seule classe avant d'en venir à cette organisation, qui n'est pas forcément la meilleure, mais ça je le saurais avec la pratique et le temps. Tu peux donc utiliser ta propre logique pour organiser tes contrôleurs. Il faut juste avoir une organisation qui va te faciliter la maintenance de ton application plus tard.

Pour créer la classe CoreController, nous allons exécuter la commande:

$ symfony console make:controller CoreController

https://imgur.com/3ls2TBh.jpg

Et comme par magie (alors que c'est juste Symfony), la console nous indique que la commande a créer les fichiers src/Controller/CoreController.php et templates/core/index.html.twig.

Si tu ouvres le fichier src/Controller/CoreController.php:

<?php

namespace App\Controller;

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

class CoreController extends AbstractController
{
    /**
     * @Route("/core", name="core")
     */
    public function index(): Response
    {
        return $this->render('core/index.html.twig', [
            'controller_name' => 'CoreController',
        ]);
    }
}

La classe CoreController hérite de la classe AbstractController qui est défini dans le namespace Symfony\Bundle\FrameworkBundle\Controller, ensuite nous avons une méthode index() qui retourne un objet Response qui est aussi défini par Symfony. La fonction index() est décoré d'une annotation @Route("/core", name="core"), cette annotation permet de définir une route /core qui est rattaché au contrôleur index, si tu ouvres ton navigateur et que tu te rends sur l'URL https://127.0.0.1:8000/core, tu vas avoir une page comme ci-dessous:

https://imgur.com/ZbxPwQP.jpg

Ça commence à bien le faire la, on continue encore un peu et tu seras un développeur Symfony confirmé.

Ce qu'il s'est passé, c'est que le contrôleur frontal (tu t'en rappelles non?) à reçu la requête, il a sorti la route /core et à regarder le contrôleur rattaché à cette route, la méthode index() dans la classe CoreController, cette fonction a retourner un fichier index.html.twig dont nous allons parler dans la prochaine partie et le contrôleur frontal à renvoyer ce fichier au navigateur, simple et efficace.

Dans notre cas, nous voulons que quand l'utilisateur arrive à la racine de notre site, le contrôleur index soit appelé et que celui-ci soit la page d'accueil, il faut donc modifier la définition de la route comme ceci:

<?php
// src/Controller/CoreController.php
namespace App\Controller;

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

class CoreController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        return $this->render('core/index.html.twig', [
            'controller_name' => 'CoreController',
        ]);
    }
}

Si tu retournes sur le lien https://127.0.0.1:8000/core, tu vas avoir une erreur 404 qui dit que la route GET /core n'existe pas, et oui tu l'as supprimer, il faut maintenant aller sur le lien https://127.0.0.1:8000 pour voir la page.

Actuellement le contrôleur affiche une vue et c'est plutôt cool, mais nous parlerons des vues dans la prochaine partie, nous allons donc pour l'instant retourner un objet Response:

<?php
// src/Controller/CoreController.php
namespace App\Controller;

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

class CoreController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        return new Response("Hello toi dev Symfony, Bienvenue sur Kaherecode!");
    }
}

Maintenant le contrôleur homepage retourne un objet Response qui contient le texte "Hello toi dev Symfony, bienvenue sur Kaherecode!", on regarde ce qu'on a dans le navigateur:

https://imgur.com/n2CBf8n.jpg

Et c'est bien ça, on a notre premier contrôleur, yesssss!

Bon j'avoue qu'on a encore du chemin à faire, mais on peut aussi retourner du HTML pour que le texte soit un peu plus lisible:

<?php
// src/Controller/CoreController.php
namespace App\Controller;

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

class CoreController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        return new Response("
            <h1>Hello toi dev Symfony,</h1>
            <h2>Bienvenue sur Kaherecode!</h2>
        ");
    }
}

https://imgur.com/RXKBSHN.jpg

Et on a maintenant des balises HTML. Sois fier de toi, on a une page web qui s'affiche, nous allons l'améliorer plus tard.

Les routes

Nous avons un peu parler des routes dans les paragraphes précédents, dans cette section, nous allons essayer d'aller un peu plus en profondeur.

Annotations ou fichiers YAML

Pour notre contrôleur homepage, nous avons défini la route en utilisant une annotation sur la méthode homepage(), c'est plus pratique de faire ainsi, comme cela nous avons la définition de nos routes et contrôleurs dans le même fichier, pas besoin d'aller chercher dans un autre fichier pour savoir la route associer à un contrôleur. Mais il est aussi possible de définir les routes de notre application dans un fichier YAML, XML ou aussi PHP, je vais te présenter le fichier YAML ici, tu peux regarder la documentation pour en savoir plus sur les autres formats.

Pour définir nos routes dans un fichier YAML, nous allons utiliser un fichier routes.yaml qui se trouve dans le dossier config/. Nous allons par exemple définir une route /about qui va afficher une page à propos. Cett route va être rattaché à un contrôleur about dans la classe CoreController:

# config/routes.yaml
about:
  path: /about
  controller: App\Controller\CoreController::about

Voilà, on commence par donner un nom about à la route, puis dans path, on spécifie l'URL à partir de laquelle cette route va être appeler, enfin on renseigne le contrôleur associé.

Il faut ensuite créer ce contrôleur dans la classe CoreController:

<?php
// src/Controller/CoreController.php
namespace App\Controller;

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

class CoreController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function homepage(): Response
    {
        return new Response("
            <h1>Hello toi dev Symfony,</h1>
            <h2>Bienvenue sur Kaherecode!</h2>
        ");
    }

    public function about()
    {
        return new Response("<h1>About Symfony Ticketing App</h1>");
    }
}

Et voilà, quand tu accèdes à la route https://127.0.0.1:8000/about, tu as la page avec le message "About Symfony Ticketing App" qui s'affiche.

Pour cette série de tutoriel nous allons utiliser les annotations pour définir nos routes, c'est un choix personnel encore une fois et je le trouve plus pratique.

Paramètres de routes

Jusque là, nous avons des routes statiques, c'est à dire une route dont l'URL ne change pas, comme la route /about par exemple. Si nous voulons avoir des routes dynamiques, une route pour afficher un tutoriel sur Kaherecode par exemple, il va falloir ajouter des paramètres à la définition de notre route.

Nous allons créer un contrôleur pour afficher un événement spécifique sur notre application. Je vais d'abord créer une classe EventController:

$ symfony console make:controller EventController

Je vais ensuite créer un contrôleur show() dans cette classe:

<?php
// src/Controller/EventController.php
namespace App\Controller;

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

class EventController extends AbstractController
{
    /**
     * @Route("/events/{id}", name="show_event")
     */
    public function show($id): Response
    {
        return new Response("<h1>Affichage de l'événement {$id}.</h1>");
    }
}

Je crée la route /event/{id}, et cette fois-ci la fonction show() prend un paramètre $id comme dans la route.

Pour ajouter une variable à la route, il faut donc mettre le nom de la variable dans les accolades {} et ensuite définir un paramètre dans la définition du contrôleur avec le même nom que la variable sur la route. Si tu accèdes maintenant à l'URL https://127.0.0.1:8000/events/4, tu vas avoir cette page:

https://imgur.com/mnEJHsb.jpg

Tu peux changer la valeur de id dans la route comme avec https://127.0.0.1:8000/events/100, https://127.0.0.1:8000/events/250 et tu verras le changement sur la page.

Mais qu'est-ce qui se passe si tu ne mets pas id dans la route? On essaie avec https://127.0.0.1:8000/events

https://imgur.com/GCIV5MN.jpg

On a une erreur qui dit que la route GET /events n'existe pas. On a pas défini de route /events, mais plutôt /events/{id}. Si tu veux avoir un paramètre facultatif, comme une catégorie par exemple, nous allons avoir une route comme ceci:

<?php
// src/Controller/EventController.php
namespace App\Controller;

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

class EventController extends AbstractController
{
    // ...

    /**
     * @Route("/events/{category}", name="list_events")
     */
    public function list($category = null): Response
    {
        $htmlMessage = "<h1>Liste des événements";
        if ($category) {
            $htmlMessage .= " ave la catégorie: ${category}";
        }
        $htmlMessage .= "</h1>";

        return new Response($htmlMessage);
    }
}

Nous définissons un contrôleur list() qui va afficher la liste de tous les événements ou seulement les événements d'une catégorie si la catégorie est renseigné. La route ne change pas de ce qu'on avait tout à l'heure, mais le paramètre de la fonction list() est par défaut mis à null, et c'est tout ce qu'on a besoin.

Si tu accèdes à l'URL https://127.0.0.1:8000/events/php tu vas voir une page:

https://imgur.com/86cdKzi.jpg

Oopssss!!!! Je crois que nous venons de créer notre premier bug, BRAVO!!!

Qu'est-ce qui s'est passé au juste? Tu vois le problème? Pour rappel, voici les contrôleurs que nous avons dans la classe EventController:

<?php
// src/Controller/EventController.php
namespace App\Controller;

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

class EventController extends AbstractController
{
    /**
     * @Route("/events/{id}", name="show_event")
     */
    public function show($id): Response
    {
        return new Response("<h1>Affichage de l'événement {$id}.</h1>");
    }

    /**
     * @Route("/events/{category}", name="list_events")
     */
    public function list($category = null): Response
    {
        $htmlMessage = "<h1>Liste des événements";
        if ($category) {
            $htmlMessage .= " ave la catégorie: ${category}";
        }
        $htmlMessage .= "</h1>";

        return new Response($htmlMessage);
    }
}

Alors tu vois mieux? Nous avons deux routes: /events/{id} et /events/{category}, pour nous ces routes sont différentes, mais pas pour Symfony. Quand Symfony reçoit une URL /events/1 il le traite de la même manière qu'une URL /events/php. Il regarde la première route qui correspond à ce schéma /events/{param} et appel son contrôleur. Dans notre cas, le premier contrôleur est show($id), si tu mets la définition du contrôleur list($category = null) avant, il va être appeler en premier. Est-ce la solution est d'organiser nos contrôleurs du plus prioritaire au moins? Pas forcément.

Le contrôleur show($id) prend un id en paramètre, les ids sont des entiers, et le contrôleurs list($category = null) prend category en paramètre, qui est une chaine. Nous allons donc définir le contrôleur show pour ne passer que si le paramètre est un entier. Si ce n'est pas un entier, le contrôleur list va être appeler.

<?php
// src/Controller/EventController.php
namespace App\Controller;

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

class EventController extends AbstractController
{
    /**
     * @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
     */
    public function show($id): Response
    {
        return new Response("<h1>Affichage de l'événement {$id}.</h1>");
    }

    /**
     * @Route("/events/{category}", name="list_events")
     */
    public function list($category = null): Response
    {
        $htmlMessage = "<h1>Liste des événements";
        if ($category) {
            $htmlMessage .= " ave la catégorie: ${category}";
        }
        $htmlMessage .= "</h1>";

        return new Response($htmlMessage);
    }
}

Nous ajoutons un attribut requirements à la définition de la route show_event pour lui dire que la variable id est un entier \d+. Si tu appelles maintenant la route https://127.0.0.1:8000/events/100 tu as bien le contrôleur pour afficher un événement, et la route https://127.0.0.1:8000/events/php affiche la liste des événements en php

https://imgur.com/W8zBFOC.jpg

Et si tu renseignes pas la catégorie comme ici https://127.0.0.1:8000/events, tu as la liste des événements:

https://imgur.com/R7cO36G.jpg

Maintenant, dans ce cas ci, si tu définis le contrôleur list avant show, ton contrôleur show ne sera jamais appeler, parce que la route pour le contrôleur list va prendre le dessus, vu que cette route n'a aucun requirements, et donc que le paramètre soit une chaîne ou un entier, elle va passer.

Et voilà, tu peux avoir autant de variable pour une route que tu veux, il faut juste mettre chacune variable dans les accolades /events/{category}/{city}, définir un paramètre sur le contrôleur avec le même nom que dans la route list($category, $city) et ne pas avoir deux variables avec le même nom.

Il y a plein d'autres fonctionnalités avec les routes que je vais te laisser découvrir sur la documentation.

Tu peux voir la liste des routes de ton application avec la commande:

$ symfony console debug:router

https://imgur.com/GC1UeEu.jpg

Les routes que nous avons défini sont tout en bas, les autres sont définis par Symfony et ne sont accessible qu'en développement.

Maintenant que nous avons nos routes et nos contrôleurs, dans la prochaine partie, nous allons créer nos différentes vues et retourner des pages HTML comme une application web qui se respecte au lieu de retourner de simple balise h1.

D'ici là, essaie de pratiquer en créant un nouveau projet, des contrôleurs et de comprendre comment tout cela fonctionne. Si tu as des questions, n'hésite pas à les poser dans les commentaires ci-dessous ou nous retrouver dans la chaîne #symfony sur le chat discord pour discuter. A très bientôt.


Partage ce tutoriel


Merci à

Mamadou Aliou Diallo

Mamadou Aliou Diallo

@alioukahere

Développeur web fullstack avec une passion pour l’entrepreneuriat et les nouvelles technologies. Fondateur de Kaherecode.

Continue de lire

Discussion

Tu dois être connecté pour participer à la discussion. Me connecter.

Lamine Yade
@lamzo il y a 2 mois

Bonjour, est il possible de coupler l'application avec mini imprimante de caisse pour pour imprimer les tickets. Merci.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Bonjour, oui c'est bien possible, en utilisant des sockets par exemple. Regarde un peu la fonction fsockopen.

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Bonjour, je voudrais savoir si vous avez prévu une date de la sortie de la 3em partie. Merci

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Bonjour, je publie le prochain tutoriel ce samedi!

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Merci pour la réponse. J’ai hâte de voir la suite. J’ai déjà travailler sur les deux premières parties. J’attend la prochaine ……

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Je trouve ce tuto super intéressant. Le cahier de charge est très bien détaillé . J’ai hâte de travailler dessus.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Je ferais le max pour répondre aux attentes! A Samedi!

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Merci. Juste une question est ce que dans ce projet on va faire du design ou bien on va faire que du Symfony?

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Bon on ne parlera pas vraiment du design, mais j'essaierais de trouver un tutoriel pour parler de webpack encore.

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Merci pour la réponse. Au fait moi j’ai déjà fais du Symfony mais je cherche à avoir plus de compétence et je trouve que le tutoriel va traiter beaucoup de cas notamment les droit des utilisateurs ect….. je trouve que c’est très enrichissant ce tutoriel. C’est pourquoi que je voulais savoir si il y’a des dates de sortie de chaque partie comme ça je m’organise.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Je vais publier une partie chaque Samedi à partir de demain, et je ferais vraiment le max pour m'en tenir. Merci!

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Merci. C’est vraiment important pour moi. Une fois que j’aurais fini ce tutoriel je vais aussi suivre ta formation d Api-platform pour pouvoir l’appliquer sur le tutoriel.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

OK ça marche! Je vais essayer de faire quelques lives sur Symfony le mois prochain, ça pourrait peut être t'intéresser. Tu peux rejoindre le serveur Discord pour ne pas manquer l'info.

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Oui ça m’intéresse le livre surtout si ça traite un sujet de A-Z. Exemple si on peut développer une application en utilisant un Api ça serait très intéressant. Je suis déjà dans le discorde.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

OK top, nous allons commencer par traiter la série sur Symfony, mais avec des lives videos pour chaque partie, et nous allons parler beaucoup plus en détails de chaque concept.

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Si j’ai bien compris y’aura les tutoriel de gestion de tickets qui sortiront tous les samedis mais en parallèle y’aura aussi le livre qui va sortir mais en vidéo, c’est bien ça ?

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

Oui justement!

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

Top alors j’attend que ça et merci pour l’aide que tu nous apportes. Mon but est de devenir lead Dev sur Symfony et je pense que avec ton aide je peux atteindre mes objectifs.

Tu dois être connecté pour participer à la discussion. Me connecter.
Mamadou Aliou Diallo
@alioukahere auteur il y a 2 mois

On l'espère, merci!

Tu dois être connecté pour participer à la discussion. Me connecter.
Soidridine Ibrahima
@soidridine1 il y a 2 mois

C’est moi qui te remercie. J’ai hâte que tu mette le tutoriel en ligne demain

Tu dois être connecté pour participer à la discussion. Me connecter.