• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/06/2024

Réalisez une application configurable et extensible

Configurez vos objets à l’aide du container de services

Avez-vous déjà été dans la situation où vous avez eu besoin d'une classe, d'un objet qui avait de nombreuses dépendances, et qu'il était un peu "pénible" d'instancier ?

Ou encore, avez-vous eu besoin de cet objet un peu partout dans votre code et dû dupliquer partout l'instanciation de cet objet ?

Dans ce chapitre, vous allez découvrir le composant Dependency Injection de Symfony, et notamment comment construire nos objets et les récupérer à l'aide du container de services : suivez le guide. :ange:

Le container de services

Le composant Dependency Injection est fourni avec un container de services. Mais qu'est-ce qu'un service ?

Un service est tout simplement un objet qui est utilisé dans votre projet et auquel vous avez besoin d'accéder. Ce service est enregistré dans un container, ainsi que sa "recette de cuisine", ou plutôt, ainsi que les étapes nécessaires à sa construction : dépendances, méthodes et arguments à appeler.

Prenons un exemple avec un objet un peu complexe à construire :

<?php
// src/Services/ComplexObject.php
namespace App\Services;

class ComplexObject
{
    private $foo;
    private $bar;
    private $baz;
    private $other;
    
    public function __construct(
        Foo $foo,
        Bar $bar,
        Baz $baz,
        Other $other
    )
    {
        $this->foo = $foo;
        $this->bar = $bar;
        $this->baz = $baz;
        $this->other = $other;
    }

    public function doSomething() {
        // ...
    }
}

Vous venez pourtant ici de créer votre premier service, félicitations !

Pour vérifier qu'un service est bien disponible, la console de Symfony a une commande dédiée. Voici le retour de la commande  debug:container  pour le service nommé App\Services\ComplexObject :

Cette capture d'écran présente le retour console de la commande debug:container pour le service nouvellement créé.
Le service est immédiatement disponible dans le container !

Et ce n'est pas tout, puisque le service est présent dans le container de services, on peut l'injecter sans crainte dans nos classes ! Par exemple, imaginons que vous vouliez utiliser cet objet dans un contrôleur vu précédemment :

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

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

class HelloController extends AbstractController
{
    /**
     * Page d'accueil
     *
     * @Route("/", name="accueil")
     */
    public function home(ComplexObject $complexObject)
    {
        // $complexObject->doSomething();
    }
}

Mais comment est-ce possible ? Nous n'avons absolument rien fait d'autre que déclarer notre objet ! :-°

C'est grâce à l'une des fonctionnalités les plus utiles du container de services : l'autowiring (ou autrement dit, l'autochargement des classes) !

L'autowiring de services

Cette fonctionnalité est activée par défaut dans tout projet Symfony 4.

Dans le fichier services.yaml, vous retrouverez la déclaration suivante :

# config/services.yaml
services:
    _defaults:
        # Ajoute automatiquement les dépendances à vos objets
        autowire: true

 Vous ne connaissez pas le format YAML ? C'est un format très adapté à la configuration. Quelques minutes suffisent pour le maîtriser, jetez un œil à la documentation. :magicien:

Tous les arguments qui sont des objets typés de la fonction  __construct()  seront automatiquement passés à ce service s'ils sont disponibles dans le container de services.

Dans votre projet, le framework va parcourir l'ensemble des ressources (ou chemins) définies :

services:
    # Rend chaque classe disponible dans src/ disponible en tant que service
    App\:
        resource: '../src/*'
        exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'

Comment faire alors pour une classe qui n'est pas dans ce dossier ?

Vous pouvez déclarer une autre ressource ou encore la classe seule :

services:
    # Toutes les classes d'une dépendance Composer
    OtherLibrary\:
        resource: '../vendor/a/b/c'

    # Seulement une classe, tant qu'elle est trouvée par Composer.
    OtherLibrary\MyClass:

Mais parfois, j'ai besoin de passer autre chose que des objets en constructeur, des tableaux ou une simple valeur de configuration : comment faire ?

On y vient, il va falloir cette fois déclarer manuellement vos paramètres.

Déclaration manuelle de paramètres

Imaginons que l'on ait besoin d'un e-mail administrateur pour recevoir des e-mails en cas d'erreur :

<?php
// src/Services/MailLogger
namespace App\Services;

class MailLogger
{
    private $adminEmail;

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

    public function sendMail()
    {
        /* ... */    
    }
}

Dans ce cas, le framework Symfony n'est pas capable de retrouver la valeur de la variable $adminEmail, nous devons la déclarer manuellement, soit seulement pour ce service, soit globalement :

# config/services.yaml
services:
    # Disponible seulement pour ce service
    App\Services\MailLogger:
        arguments:
            $adminEmail: 'admin@openclassrooms.com'
    
    # Déclaration globale à tous les services
    # déclarés dans ce fichier
    _defaults:
        bind:
            $adminEmail: 'admin@openclassrooms.com'

Simple, n'est-ce pas ? Et encore mieux, le container de services est également capable de contenir des paramètres. Adaptons l'exemple précédent :

# config/services.yaml
parameters:
    admin_email: 'admin@openclassrooms.com'

# Suffisant pour que le MailLogger soit bien instancié.
services:
    _defaults:
        bind:
            $adminEmail: '%admin_email%'

Avec toutes ces informations, vous serez maintenant capables de construire vos objets proprement et d'y accéder n'importe où dans votre projet Symfony. :soleil:

Étendez votre application grâce aux événements natifs

Le framework Symfony pousse le développeur à développer des applications de bonne qualité et faciles à étendre. Maintenant que nous savons construire et retrouver nos objets, intéressons-nous à comment rendre notre application extensible.

Imaginons, par exemple, un processus d'achat de produits sur un site e-commerce. Lors de la mise en panier du produit, de multiples tâches doivent être vérifiées :

  • Le produit est-il en stock ?

  • L'utilisateur est-il connecté ? Dans ce cas, on peut lier le panier au compte utilisateur.

  • Le produit est-il souvent vendu avec d'autres ? Dans ce cas, nous pourrions rediriger vers une liste de produits complémentaires.

  • Etc.

Chacune de ces actions est complètement indépendante des autres et ne devrait pas être "liée" ou "couplée" dans votre code.

C'est justement pour vous permettre de gérer ce type de problématique que nous allons utiliser le composant EventDispatcher qui implémente deux patterns de programmation objet : Observateur et Mediateur.

Le composant EventDispatcher en bref

Une application Symfony dispose d'un répartiteur d'événements qui va envoyer une série d'événements natifs et métiers. Ensuite, des objets, qui peuvent être des écouteurs ou encore des souscripteurs d'événements, peuvent écouter ces événements et exécuter des fonctions à partir de données qui sont transmises par l'événement.

Schéma de fonctionnement du composant Event Dispatcher de Symfony
Vue d'ensemble du répartiteur d'événements

Expliquons le schéma ci-dessus :

  • Écouteur 1 écoute l'événement 1, écouteur 2 l'événement 2, et le souscripteur l'événement 3.

  • Les 3 "écouteurs" (2 écouteurs, 1 souscripteur) ont été ajoutés au répartiteur d'événements (ou encore "EventDispatcher").

  • Quand le répartiteur envoie les événements, il donne l'information aux écouteurs qui peuvent donc réaliser des actions au bon moment sans pour autant avoir connaissance des autres écouteurs.

Créez un écouteur d'événements

Un écouteur d'événements est une simple classe PHP disposant de fonctions publiques qui prennent en argument l'événement écouté.

Par exemple, cette classe est un écouteur d'événement valide :

<?php
// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Some\Events\FooEvent;

class BarListener
{
    public function doSomething(FooEvent $event)
    {
        /* ... */
    }
}

Pour qu'il soit considéré comme écouteur par le répartiteur d'événements, nous devons enregistrer le service avec un "tag" particulier :

# config/services.yaml
services:
    App\EventListener\BarListener:
        tags:
            - { name: kernel.event_listener, event: foo, method: doSomething }

Détaillons cette configuration :

  • Tout d'abord, le name du tag "kernel.event_listener" est propre au framework. Tagger ce service permet de le déclarer en tant qu'écouteur.

  • Ensuite, l'event est le nom de l'événement qui sera envoyé par le répartiteur.

  • Enfin, la method permet de spécifier quelle méthode de la classe sera exécutée. Il est possible également de ne pas la déclarer si l'on utilise la syntaxe suivante : "on" + "nom de l'événement en CamelCase" (ex. : onFoo(FooEvent $event)).

Les souscripteurs d'événements

Une autre façon de réagir à un événement est d'utiliser un souscripteur d'événements.

Le souscripteur d'événements est mieux adapté pour écouter de multiples événements, et il contient la liste des événements à écouter. La classe doit impérativement implémenter l'interface Symfony\Component\EventDispatcher\EventSubscriberInterface :

<?php
// src/EventSubscriber/ExceptionSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use App\Events\FooEvent;
use App\Events\BarEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // Liste des évènements, méthodes et priorités
        return [
           'foo' => [
               ['doSomething', 10],
               ['doOtherThing', 0],
           ],
           'bar' => [
               ['doBarThing', -10]
            ],
        ];
    }

    public function doSomething(FooEvent $event) {}

    public function doOtherThing(FooEvent $event) {}

    public function doBarThing(BarEvent $event) {}
}

Et... c'est tout ! Parce que la classe implémente une interface particulière du framework Symfony, le container de services a automatiquement ajouté le tag "kernel.event_subscriber" et déclaré la classe en tant que souscripteur d'événements. Super, non ?

C'est ce que l'on appelle l'autoconfiguration de services, qui est activée par défaut en Symfony 4 : vérifiez la valeur de la propriété autoconfigure dans votre configuration.

Euh, mais pourquoi a-t-on des valeurs numériques comme 10, 0 ou -10 dans le souscripteur d'événement ? :-°

C'est une propriété facultative d'un écouteur. Imaginons que 3 écouteurs écoutent le même événement, comment savoir lequel écoutera le premier ? Et si l'ordre importe pour vous ?

Il suffira alors de déclarer une priorité, sachant que la plus forte valeur l'emporte et que la valeur par défaut est 0.

Cette propriété est aussi disponible pour les écouteurs d'événements. Reprenons l'exemple précédent avec une priorité de 999 :

# config/services.yaml
services:
    App\EventListener\BarListener:
        tags:
            - { name: kernel.event_listener, event: foo, method: doSomething, priority: 999 }

Cycle de vie d'une application Symfony

Nous l'avons vu dans le chapitre précédent : le framework Symfony est construit autour du principe requête/réponse. Pour parvenir à retourner une réponse, l'application traite la requête, recherche le contrôleur et l'action à partir de l’URL, va construire une page à partir d'un template, etc.

Dans le cycle de vie d'une application Symfony, de nombreux événements sont disponibles pour vous permettre d'altérer le comportement de l'application. Parmi les plus utiles :

  • kernel.request : envoyé avant que le contrôleur ne soit déterminé, au plus tôt dans le cycle de vie.

  • kernel.controller : envoyé après détermination du contrôleur, mais avant son exécution.

  • kernel.response : envoyé après que le contrôleur retourne un objet Response.

  • kernel.terminate : envoyé après que la réponse est envoyée à l'utilisateur.

  • kernel.exception : envoyé si une exception est lancée par l'application.

Chacun de ces événements va retourner une instance particulière de KernelEvent. Par exemple écouter kernel.request permet d'accéder à la requête à partir de l'événement, kernel.controller au contrôleur déterminé, kernel.exception à l'exception...

Pour visualiser le cycle de vie, voici un schéma qui provient de la documentation officielle :

Schéma présentant le cycle de vie d'une application Symfony, et comment on peut l'altérer avec les événements natifs
Le cycle de vie d'une application Symfony

Comme on peut le voir, il existe un événement capable d'agir à chaque étape du cycle de vie de l'application pour en changer le comportement originel. :magicien:

Voici un exemple d'application très simple : l'envoi d'un e-mail à un utilisateur qui aurait acheté un produit sur un site e-commerce.

<?php
// src/EventSubscriber/SummaryMailSubscriber
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SummaryMailSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            // 'kernel.terminate'
            KernelEvents::TERMINATE => [
                ['sendProductPaidMail', 0],
            ];
        ];
    }

    public function sendProductPaidMail(PostResponseEvent $event)
    {
        // envoi de l'email
    }
}

Un envoi d'e-mail, qui peut être long et bloquer l'envoi de la réponse, a donc été "repoussé" après que l'application a retourné une réponse à l'utilisateur.

Créez et déployez des événements spécifiques

Jusque là, nous avons vu comment écouter les événements, mais qu'est-ce qu'un événement et comment en créer un spécifique à notre métier ?

C'est très simple : un événement est un objet quelconque, vous êtes libre d'utiliser n'importe quel objet tant qu'il étend la classe Event du composant EventDispatcher.

Prenons l'exemple d'un événement à envoyer quand un utilisateur ajoute un produit à son panier sur un site e-commerce : que devrait contenir l'événement ? Eh bien, tout dépend de votre besoin.

Pour ma part, j'imagine ce type d'objet :

<?php
// src/Events/BasketProductAdded.php
namespace App\Events;

use Symfony\Component\EventDispatcher\Event;
use App\Entity\Product;
use App\Entity\Customer;

class BasketProductAdded extends Event
{
    const NAME = 'basket.product_added';

    private $product;
    private $customer;

    public function __construct(Product $product, Customer $customer)
    {
        $this->product = $product;
        $this->customer = $customer;
    }

    public function getProduct()
    {
        return $this->product;
    }

    public function getCustomer()
    {
        return $this->customer;
    }
}

Et dans le contrôleur qui est responsable de cette action :

<?php
// src/Controller/BasketController.php

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Events\BasketProductAdded;

class BasketController extends AbstractController
{
    public function add(EventDispatcherInterface $eventDispatcher)
    {
        /* ... */
        $event = new BasketProductAdded($product, $this->getUser());
        $eventDispatcher->dispatch($event, BasketProductAdded::NAME);
    }
}

En créant un écouteur ou un souscripteur d'événement, vous pourrez maintenant ajouter du comportement à votre application sans faire grossir vos classes, ou coupler vos différentes fonctionnalités entre elles.

En résumé

Le framework Symfony intègre les meilleures pratiques de développement pour des applications puissantes, extensibles et faciles à maintenir.

Nous avons d'abord vu comment déléguer la construction et la récupération de nos objets au container de services.

  • Grâce à l'autowiring, l'essentiel du temps, nous n'avons rien de spécial à faire pour que nos objets soient automatiquement retrouvés par le container et accessibles dans nos services et nos contrôleurs.

  • L'autoconfiguration permet d'ajouter des tags à nos services s'ils implémentent une interface spécifique et les services tagués sont traités différemment par le framework.

Ensuite, nous avons vu comment utiliser la programmation événementielle à l'aide du composant EventDispatcher.

Le principal intérêt est que l'on peut changer le comportement d'une application sans en changer le code, et ajouter de nombreux comportements sur une même action, sans pour autant que ces comportements soient liés entre eux.

Nous avons vu que Symfony dispose de nombreux événements natifs qui sont envoyés aux écouteurs durant le cycle de vie de l'application

Enfin, il est également possible de créer et de "dispatcher" ses propres événements métiers

Il faut créer l'événement qui doit "implémenter" la classe Event de Symfony, à laquelle on peut passer des informations au besoin. Ensuite, il suffit de faire appel à l'EventDispatcher pour envoyer l'information à tous les écouteurs concernés.

Dans le prochain chapitre, nous allons découvrir un outil essentiel du développeur Symfony : le Profileur Web ! 

Exemple de certificat de réussite
Exemple de certificat de réussite