Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jprivet-dev/8ced5058f843074b88a0dbe90ec1112d to your computer and use it in GitHub Desktop.
Save jprivet-dev/8ced5058f843074b88a0dbe90ec1112d to your computer and use it in GitHub Desktop.
[Mémo] AFUP - PHP Tour 2012 - Symfony, Design Pattern Observer, injection de dépendance et Service Locator (Hugo Hamon)

[Mémo] AFUP - PHP Tour 2012 - Symfony, Design Pattern Observer, injection de dépendance et Service Locator (Hugo Hamon)

Sommaire

1. Ressources

Cette fiche mémo contient une retranscription des design patterns et des bonnes pratiques de développement exposés à la conférence de l’AFUP "Simplifiez-vous les design patterns avec Symfony", par Hugo Hamon :

📎
Retrouvez le listing de mes mémos, tips et autres sur https://github.com/jprivet-dev/memos

2. Remerciements

Merci Hugo Hamon pour cette conférence ! Certes elle date un peu, mais elle expose extrêmement bien ces patterns et principes appliqués à Symfony :)

3. Pourquoi un Design Pattern ?

Un Design Pattern est un modèle pour résoudre une problématique récurrente, que l’on pourra implémenter avec un langage de programmation (PHP, Java, .NET, …​).

Les Design Patterns ne sont pas à utiliser tel quel : ils sont là pour donner une inspiration, pour aider à solutionner un problème.

4. Le Design Pattern Observer

4.1. À quoi sert-il ?

Le Design Pattern Observer permet de découpler les dépendances et d’étendre facilement les capacités d’un objet.

4.2. Cas pratique : confirmation de commande et notifications

La classe suivante Order permet de confirmer une commande, de la sauvegarder en BDD et de faire un ensemble de traitements spécifiques, comme logger la commande et envoyer deux emails (un au client et un autre au département des ventes) :

class Order {
    public function confirm() {
        // On confirme et sauvegarde la commande.
        $this->status = 'confirmed';
        $this->save();

        // On logge la commande.
        if($this->logger) {
            $this->logger->log('New order...');
        }

        // On notifie par email le client.
        $mail = new Email();
        $mail->recipient = $this->customer->getEmail();
        $mail->subject = 'Your order !';
        $mail->message = 'Thanks for ordering...';
        $this->mailer->send($mail);

        // On notifie par email le département des ventes.
        $mail = new Email();
        $mail->recipient = '[email protected]';
        $mail->subject = 'New order to ship!';
        $mail->message = '...';
        $this->mailer->send($mail);
    }
}

4.2.1. Qu’est-ce qui pose problème ?

Trop de responsabilités :

Cette classe Order à trois responsabilités :

  1. Elle persiste de l’information en BDD.

  2. Elle logge un message dans le logger.

  3. Elle notifie en envoyant des mails.

Ces responsabilités augmenteront les possibilités de générer des bugs quand vous interviendrez sur cette classe.

Couplage fort :

L’objet Order dépend du $this→logger et de l’objet Email.

Evolutivité limitée :

Si l’on souhaite faire évoluer les missions de cette classe Order, par exemple changer la manière de notifier, il faudra modifier directement cette classe.

Ce couplage a une incidence sur la complexité des tests unitaires : plus il y a de dépendances, plus vous êtes obligé d’utiliser des mocks.

Maintenance peu aisée :

La maintenance du code sera difficile, car si nous avons plusieurs classes de ce type, nous serons face à un code spaghetti, qui sera difficile à déchiffrer.

4.2.2. Suivre les principes SOLID

Voici les cinq bonnes pratiques orientées objet à appliquer au code, afin d’en simplifier la maintenance, la testabilité et les évolutions futures :

S

Single Responsibility Principle

Principe de responsabilité unique : une classe, méthode ou fonction ne doit avoir qu’une seule responsabilité.

O

Open/Closed Principle

Principe ouvert / fermé : une classe doit être ouverte à l’extension, mais fermée à la modification.

L

Liskov Substitution Principle

Principe de substitution de Liskov : soit G, un sous-type de T, peut remplacer T sans modifier la cohérence du programme.

I

Interface Segregation Principle

Principe de ségrégation d’interfaces : utiliser plusieurs interfaces spécifiques pour chaque client qu’une seule interface générale.

D

Dependency Inversion Principle

Principe d’inversion de dépendance : dépendre des abstractions et non des implémentations.

💡
Dans le cas de notre classe Order, l’objectif est de suivre le principe du S, le Single Responsability Principle, c’est-à-dire qu’une méthode ne doit faire qu’une seule chose à la fois et la faire bien. Nous allons donc découpler la méthode confirm() en utilisant le Design Pattern Observer.

4.3. Présentation du Design Pattern Observer

Un sujet, l’objet observable, émet un signal à des modules qui jouent le rôle d’observateurs.

Dans la classe Order de notre cas pratique, l’objet observable serait notre commande. Cette commande émettra un signal à ses observateurs : le mailer et le logger.

À chaque fois que mon sujet change, à chaque fois que l’état de ma commande change, on va notifier ce changement à la série d’observateurs.

Chaque observateur sera un objet différent, avec une responsabilité unique.

4.4. 1ère implémentation (pur PHP)

4.4.1. Les interfaces ObserverInterface & ObservableInterface

On définie tout d’abord les deux interfaces suivantes :

interface ObserverInterface {       // | (1)
    function notify(ObservableInterface $subject);
}

interface ObservableInterface {     // | (2)
    function attach(ObservableInterface $observer);
    function notify();
}
  1. Un observateur devra répondre à l’interface ObserverInterface et implémenter la méthode notify() qui va recevoir en paramètre le sujet ($subject) que cet observateur observe.

  2. L’interface ObservableInterface définie l’objet observé, auquel on rattache ses observateurs avec la méthode attach(), et qui pourra notifier l’ensemble de ses observateurs avec la méthode notify().

4.4.2. Implémentation des observateurs (logger & notifications)

Notre classe Order permet de logger la commande et d’envoyer un mail au client et au département des ventes. On peut découpler ces actions en trois observateurs, en trois classes différentes.

La classe LoggerHandler :

class LoggerHandler implements ObserverInterface {
    public $logger;                                         // | (1)

    public function notify(ObservableInterface $subject) {  // | (2)
        $reference = $subject->getReference();
        $this->logger->log('New order #'. $reference);
    }
}
  1. On injecte le logger dans l’objet LoggerHandler, via le constructeur par exemple.

  2. Quand l’objet LoggerHandler sera notifié, la méthode notify() recevra la commande en entrée et loggera la référence de cette commande.

La classe CustomerNotifier :

class CustomerNotifier implements ObserverInterface {
    public $mailer;                                         // | (1)

    public function notify(ObservableInterface $subject) {  // | (2)
        $mail = new Email();
        $mail->recipient = $subject->customer->getEmail();
        $mail->subject = 'Your order!';
        $mail->message = 'Thanks for ordering...';
        $this->mailer->send($mail);
    }
}
  1. On injecte le mailer dans l’objet CustomerNotifier, via le constructeur par exemple.

  2. Quand l’objet CustomerNotifier sera notifié, la méthode notify() recevra la commande en entrée ($subject) et enverra le mail au client.

La classe SalesNotifier :

class SalesNotifier implements ObserverInterface {
    public $mailer;                                         // | (1)

    public function notify(ObservableInterface $subject) {  // | (2)
        $mail = new Email();
        $mail->recipient = '[email protected]';
        $mail->subject = 'New order to ship!';
        $mail->message = '...';
        $this->mailer->send($mail);
    }
}
  1. On injecte le mailer dans l’objet SalesNotifier, via le constructeur par exemple.

  2. Quand l’objet SalesNotifier sera notifié, la méthode notify() recevra la commande en entrée ($subject) et enverra le mail au département des ventes.

4.4.3. Implémentation de l’observable Order

Maintenant que nous avons nos trois observateurs (LoggerHandler, CustomerNotifier & SalesNotifier), nous devons pouvoir les rattacher à l’objet observé Order :

class Order implements ObservableInterface {
    private $observers;

    public function attach(ObserverInterface $observer) {   // | (1)
        $this->observers[] = $observer;
    }

    public function notifyObservers() {                     // | (2)
        foreach($this->observers as $observer) {
            $observer->notify($this);
        }
    }
}
  1. La méthode attach() permet de rajouter un observateur à la liste des observateurs de cette classe Order.

  2. La méthode notifyObservers() va itérer sur cette liste d’observateurs et tous les notifier.

Nous passons de l’ancienne version de la méthode confirm() :

class Order {
    public function confirm() {
        $this->status = 'confirmed';
        $this->save();

        if($this->logger) {
            $this->logger->log('New order...');
        }

        $mail = new Email();
        $mail->recipient = $this->customer->getEmail();
        $mail->subject = 'Your order !';
        $mail->message = 'Thanks for ordering...';
        $this->mailer->send($mail);

        $mail = new Email();
        $mail->recipient = '[email protected]';
        $mail->subject = 'New order to ship!';
        $mail->message = '...';
        $this->mailer->send($mail);
    }
}

À cette nouvelle version de confirm(), qui exploite la méthode notifyObservers():

class Order implements ObservableInterface {
    public function confirm() {
        $this->status = 'confirmed';
        $this->save();

        $this->notifyObservers();
    }
}

4.4.4. Etape finale : exploitation de la classe Order dans le contrôleur

class OrderController {
    public function finishOrderAction() {
        // ...
        $order = new Order();
        $order->attach(new LoggerNotifier($logger));    // |
        $order->attach(new CustomerNotifier($mailer));  // | (1)
        $order->attach(new SalesNotifier($mailer));     // |

        $order->customer = $customer;
        $order->amout = 150;

        $order->confirm();                              // | (2)
    }
}
  1. On rattache les observateurs à l’objet Order.

  2. À la confirmation de la commande, la méthode notifyObservers() de la classe Order sera exécutée, et tous les observateurs rattachés à la commande avec la méthode attach() seront notifiés.

📎
À partir de maintenant, il est possible de faire évoluer les actions de l’objet Order sans modifier la classe Order directement. Il est possible de rattacher ou de détacher n’importe quel objet qui implémente l’interface ObserverInterface.

4.5. 2ème implémentation (pur PHP avec SplObserver & SplSubject)

4.5.1. Des classes abstraites proposées par défaut par PHP

La Standard PHP Library (SPL) propose, pour le Design Pattern Observer, deux classes abstraites :

SplObserver {
    abstract public update ( SplSubject $subject ) : void
}
SplSubject {
    abstract public attach ( SplObserver $observer ) : void
    abstract public detach ( SplObserver $observer ) : void
    abstract public notify ( void ) : void
}

4.5.2. Exemple d’implémentations avec SplObserver & SplSubject

Evolution de SalesNotifier :

class SalesNotifier extends SplObserver { // au lieu de `implements ObserverInterface`
    public $mailer;

    public function update(ObservableInterface $subject) { // au lieu de `notify()`
        $mail = new Email();
        $mail->recipient = '[email protected]';
        $mail->subject = 'New order to ship!';
        $mail->message = '...';
        $this->mailer->send($mail);
    }
}

Evolutions de Order:

💡
Dans l’exemple d’origine, Hugo Hamon utilise un tableau $observers. Il est possible dans notre cas d’utiliser les méthodes attach() et detach() de l’objet SplObjectStorage (https://www.php.net/manual/fr/class.splobjectstorage.php).
class Order extends SplSubject { // au lieu de `implements ObservableInterface`
    private $observers;

    public function __construct() {
        $this->observers = new SplObjectStorage();
    }

    public function attach(SplObserver $observer) { // au lieu de `ObserverInterface`
        $this->observers->attach($observer);
    }

    public function detach(SplObserver $observer) {
        $this->observers->detach($observer);
    }

    public function notify() { // au lieu de `notifyObservers()`
        foreach($this->observers as $observer) {
            $observer->notify($this);
        }
    }
}

4.5.3. Des limitations ? Oui !

  1. Vous devez étendre les classes abstraites SplObserver et SplSubject, ce qui nous limite car nous ne pouvons pas étendre plusieurs classes à la fois en même temps.

  2. L’objet Order doit être initialisé et nous devons lui donner l’ensemble de ses observateurs. À chaque implémentation du Design Pattern Observer, il faut écrire la logique métier qui notifie l’ensemble des observateurs du sujet implémenté (à partir de PHP 5.4, il est possible d’utiliser des traits pour éviter de dupliquer du code).

  3. Avec la méthode attach(), on rend directement dépendant l’objet observé avec ses observateurs.

5. Le composant EventDispatcher (une variation du Design Pattern Observer)

5.1. Présentation

Symfony propose le composant EventDispatcher, qui est une variation du Design Pattern Observer.

Le Dispatcheur est un objet qui gère les connexions entre le sujet observé et ses observateurs (appelés des écouteurs dans ce composant).

Ainsi au lieu de directement connecter vos écouteurs (= observateurs) à votre sujet observable, vous allez connecter votre sujet observable au dispatcher, et le dispatcher sera connecté aux différents écouteurs.

5.2. Exemple basique

use AFUP\Listener\ArticleListener;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcher;

$listener1 = [new ArticleListener(), 'onDelete'];           // | (1)
$listener2 = [new ArticleListener(), 'onSave'];             // |

$dispatcher = new EventDispatcher();                        // |
$dispatcher->addListener('article.delete', $listener1);     // | (2)
$dispatcher->addListener('article.pre_save', $listener2);   // |

$dispatcher->dispatch('article.pre_save', new Event());     // | (3)
  1. On définie des écouteurs (= observateurs). Un écouteur peut être n’importe quel callable PHP.

  2. On connecte les écouteurs aux événements.

  3. On notifie les écouteurs en dispatchant les événements (tous les écouteurs de l’événement dispatché vont être déclenchés). On transmet un objet Event qui va être passé aux différents écouteurs.

5.3. Cas pratique : enregistrement et affichage d’un article

Dans le cas pratique suivant, nous avons un article avec un contenu en markdown. À la sauvegarde de l’article, le contenu markdown est transformé en html :

use AFUP\Model\Article;

$article = new Article();
$article->setTitle('AFUP Design Patterns');
$article->setContent('Some **content**');
$article->save();

echo $article->getHtmlContent();

Ci-après la classe Article utilisée :

namespace AFUP\Model;

use AFUP\Model\Base\Article as BaseArticle;
use dflydev\markdown\MarkdownParser;
use Propel\Runtime\Connection\ConnectionInterface;

class Article extends BaseArticle {
    public function save(ConnectionInterface $connection = null) {
        $parser = new MarkdownParser();                             // |
        $html = $parser->transformMarkdown($this->getContent());    // | (1)
        $this->setHtmlContent($html);                               // |

        $result = parent::save($connection);                        // | (2)

        $this->updateLuceneIndex();                                 // | (3)

        return $result;
    }
}
  1. On transforme le contenu markdown en html.

  2. On sauve en BDD les informations.

  3. On met à jour l’index Lucene du moteur de recherche.

⚠️

On peut noter que la classe Article a trop de responsabilités :

  1. transformation du markdown.

  2. enregistrement de l’article.

  3. mise à jour du moteur de recherche.

5.3.1. Découpler la classe Article (mon sujet observé)

Au lieu de lui injecter directement les écouteurs (= observateurs), on va lui injecter un EventDispatcher. Ainsi la classe Article n’aura qu’une seule dépendance :

namespace AFUP\Model;

use AFUP\Model\Base\Article as BaseArticle;
use Symfony\Component\EventDispatcher\EventDispatcher;

class Article extends BaseArticle {
    private $dispatcher;

    public function setDispatcher(EventDispatcher $dispatcher) { // | (1)
        $this->dispatcher = $dispatcher;
    }
}
  1. On injecte un EventDispatcher.

On modifie la méthode save() de la classe Article :

namespace AFUP\Model;

use AFUP\Model\Base\Article as BaseArticle;
use AFUP\Event\ArticleEvent;
use Propel\Runtime\Connection\ConnectionInterface;

class Article extends BaseArticle {
    public function save(ConnectionInterface $connection = null) {
        $event = new ArticleEvent($this);
        $this->dispatcher->dispatch('article.pre_save', $event);    // | (1)

        $result = parent::save($connection);
        $this->dispatcher->dispatch('article.post_save', $event);   // | (2)

        return $result;
    }
}
  1. On dispatche l’événement article.pre_save en transmettant un objet ArticleEvent.

  2. On dispatche l’événement article.post_save en transmettant un objet ArticleEvent.

5.3.2. Propager l’événement ArticleEvent

L’événement ArticleEvent qu’on dispatche est un objet dans lequel nous pouvons injecter l’article en cours qui pourra ensuite être récupéré et traité dans les différents écouteurs :

namespace AFUP\Event;

use AFUP\Model\Article;
use Symfony\Component\EventDispatcher\Event;

class ArticleEvent extends Event { // | (1)
    private $article;

    public function __construct(Article $article) {
        $this->article = $article; // | (2)
    }

    public function getArticle() {
        return $this->article;
    }
}
  1. On étend la classe Event.

  2. On stocke ici l’article que l’on souhaite récupérer dans les écouteurs (= observateurs).

💡
Lorsque l’on définie des écouteurs (= observateurs), il est possible de spécifier une priorité. Sinon les écouteurs se déclenchent dans l’ordre dans lequel ils ont été rattachés.

5.3.3. Ajouter les écouteurs ArticleListener & LuceneListener

Nous allons définir un premier écouteur ArticleListener :

namespace AFUP\Listener;

use AFUP\Event\ArticleEvent;
use dflydev\markdown\MarkdownParser;

class ArticleListener {
    public function onPreSave(ArticleEvent $event) {
        $article = $event->getArticle();                            // | (1)

        $parser = new MarkdownParser();                             // |
        $html = $parser->transformMarkdown($article->getContent()); // | (2)
        $article->setHtmlContent($html);                            // |
    }
}
  1. On récupère l’article rattaché à l’événement.

  2. On transforme le contenu markdown de l’article en html.

Nous allons définir un deuxième écouteur LuceneListener :

namespace AFUP\Listener;

use AFUP\Event\ArticleEvent;
// ...

class LuceneListener {
    public function onPostSave(ArticleEvent $event) {
        $article = $event->getArticle(); // | (1)
        // ...
        // ... (2)
        // ...
    }
}
  1. On récupère l’article rattaché à l’événement.

  2. On fait ici la mise à jour des index du moteur de recherche.

5.3.4. Enregistrer les écouteurs ArticleListener & LuceneListener

On enregistre les écouteurs avec $dispatcher→addListener() :

use AFUP\Listener\ArticleListener;
use AFUP\Listener\LuceneListener;
use Symfony\Component\EventDispatcher\EventDispatcher;

$articleListener = [new ArticleListener(), 'onPreSave'];            // | (1)
$luceneListener = [new LuceneListener(), 'onPostSave'];             // |

$dispatcher = new EventDispatcher();                                // |
$dispatcher->addListener('article.pre_save', $articleListener);     // | (2)
$dispatcher->addListener('article.post_save', $luceneListener);     // |
  1. On définie les écouteurs (= observateurs) ArticleListener & LuceneListener.

  2. On connecte les écouteurs aux événements.

5.3.5. Exploiter les événements liés à mon Article

On reprend notre cas pratique :

// Enregistrer les écouteurs `ArticleListener` & `LuceneListener` ...

$article = new Article();
$article->setDispatcher($dispatcher);

$article->setTitle('AFUP Design Patterns');
$article->setContent('Some **content**');
$article->save(); // | (1)

echo $article->getHtmlContent();
  1. À l’exécution de la méthode save(), les événements article.pre_save & article.post_save seront dispatchés.

6. L’injection de dépendance

6.1. Présentation

L’injection de dépendance n’est pas un design pattern, mais plutôt une bonne pratique de développement objet. Cette injection de dépendance prend en référence le D des principes SOLID : Principe d’inversion de dépendance (dépendre des abstractions et non des implémentations).

📎
L’injection de dépendance, c’est quand vous injectez les composants, les objets qui dépendent des objets, via des constructeurs, des méthodes ou des propriétés publiques.

Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields.

6.2. Mauvaise conception initiale

6.2.1. Présentation de l’implémentation

Nous partons sur la mauvaise conception initiale suivante :

class Mailer {
    public function send(Message $message) {                // | (1)
        try {
            $transport = new SMTPTransport(                 // | (2)
                'smtp.foo.com', 1234, 'mailer', 'p$wD^'
            );
            return $transport->send($message);
        } catch(TransportException $e) {                    // | (3)
            $logger = Logger::getInstance();                // | (4)
            $logger->log('Unable to send message to ...');  // |
            $logger->logException($e);                      // |
            throw $e;
        }
    }
}
  1. Nous avons une méthode send() qui permet d’envoyer un message.

  2. Pour envoyer ce message, la classe fait appel à un objet SMTPTransport que nous allons configurer.

  3. L’objet SMTPTransport peut lever une exception.

  4. Si nous avons une exception, nous récupérons un objet Logger unique (singleton), qui va permettre de logger un message.

6.2.2. Analyse de l’implémentation

Ce code fonctionne, c’est pragmatique, mais nous sommes face à une mauvaise conception car nous avons un fort couplage.

L’objet SMTPTransport a des paramêtres hardcodés :

  • Dans un environnement de test, nous ne pourrons pas configurer et utiliser un transport différent pour simuler un envoi de mail, avec une classe NullTransport par exemple.

  • Nous ne pourrons pas configurer spécifiquement notre serveur SMTP pour chacun des environnements de prod (serveur en ligne, authentification avec user et password) et de dev (serveur local).

L’objet Logger va être contraignant :

  • Que se passe-t-il dans le cas où nous désactivons le Logger en prod ?

  • Comment pouvons-nous enregistrer les logs dans MongoDB à la place d’un fichier ?

Pour réaliser des tests, comment pourrons-nous émuler facilement les dépendances ?
Vu l’implémentation du code, ce sera très difficile…​

6.3. La solution ? Injecter les dépendances au Mailer

L’idée est d’injecter les dépendances au Mailer, de donner au Mailer les objets dont il a besoin pour fonctionner, via l’extérieur, au lieu de les crééer à l’intérieur de celui-ci.

6.3.1. Injection par les propriétés

Nous utilisons la propriété publique $transport :

class Mailer {
    public $transport;                                  // | (1)

    public function send(Message $message) {
        try {
            return $this->transport->send($message);    // | (2)
        } catch(TransportException $e) {
            // ...
            throw $e;
        }
    }
}
  1. Nous avons une propriété publique à laquelle nous passons directement l’objet de transport créé à l’extérieur de l’objet Mailer.

  2. Nous pouvons ensuite utiliser l’objet avec la propriété $transport.

Nous pouvons ensuite utiliser l’objet Mailer :

$message = new Message();
// ...

$transport = new SMTPTransport('...');  // | (1)

$mailer = new Mailer();
$mailer->transport = $transport;        // | (2)
$mailer->send($message);
  1. Nous créons l’objet $transport depuis l’extérieur.

  2. Nous passons directement l’objet $transport à la propriété publique $transport de l’objet Mailer.

6.3.2. Injection par le constructeur

Nous utilisons la méthode __construct() :

class Mailer {
    private $transport;                                 // | (1)

    public function __construct(Transport $transport) { // | (2)
        $this->transport = $transport;
    }

    public function send(Message $message) {
        try {
            return $this->transport->send($message);    // | (3)
        } catch(TransportException $e) {
            // ...
            throw $e;
        }
    }
}
  1. Nous avons une propriété privée $transport.

  2. Nous passons l’objet de transport créé à l’extérieur de l’objet Mailer, via la méthode __construct().

  3. Nous pouvons ensuite utiliser l’objet avec la propriété $transport.

Nous utilisons ensuite l’objet Mailer :

$message = new Message();
// ...

$transport = new SMTPTransport('...');  // | (1)

$mailer = new Mailer($transport);       // | (2)
$mailer->send($message);
  1. Nous créons l’objet $transport depuis l’extérieur.

  2. Nous passons l’objet $transport à la propriété privée $transport de l’objet Mailer, via la méthode __construct().

6.3.3. Injection par un mutateur

Pour le logger, nous utilisons un mutateur setLogger() :

class Mailer {
    private $logger = null;                             // | (1)

    public function setLogger(Logger $logger) {         // | (2)
        $this->logger = $logger;
    }

    public function send(Message $message) {
        try {
            return $this->transport->send($message);
        } catch(TransportException $e) {
            if(null !== $this->logger) {                // | (3)
                $this->logger->log('...');
                $this->logger->logException($e);
            }
            throw $e;
        }
    }
}
  1. Nous avons une propriété privée $logger.

  2. Nous passons l’objet créé à l’extérieur de l’objet Mailer, via la méthode setLogger().

  3. Nous pouvons ensuite utiliser l’objet $this→logger, seulement si celui-ci est souhaité et existe.

Nous utilisons ensuite l’objet Mailer :

$message = new Message();
// ...

$transport = new SMTPTransport('...');  // | (1)
$logger = new FileLogger('/to/dev.log') // | (2)

$mailer = new Mailer($transport);       // | (3)
$mailer->setLogger($logger);            // | (4)
$mailer->send($message);
  1. Nous créons l’objet $transport depuis l’extérieur.

  2. Nous créons l’objet $logger depuis l’extérieur aussi.

  3. Nous passons l’objet $transport à la propriété privée $transport de l’objet Mailer, via la méthode __construct().

  4. Nous passons l’objet $logger à la propriété privée $logger de l’objet Mailer, via le mutateur setLogger().

📎
Nous nous retrouvons avec une classe Mailer sans paramètres hardcodés : pour les tests unitaires, nous pouvons simuler beaucoup plus facilement les objets transport et logger, sans modifier la classe Mailer.

6.4. Découplage avec les interfaces

Pour d’avantage découpler, il vaut mieux éviter les types concrets ou abstraits, et privilégier le typage avec les interfaces TransportInterface et LoggerInterface :

class Mailer {
    function __construct(TransportInterface $transport) {
        $this->transport = $transport;
    }

    function setLogger(LoggerInterface $logger) {
        $this->logger = $logger
    }
}

Ce qui nous permet, par la suite, de passer n’importe quel objet qui répond à ces interfaces :

class SMTPTransport implements TransportInterface {
    // Pour un usage spécifique...
}

class MailTransport implements TransportInterface {
    // Pour utiliser la fonction mail de PHP...
}

class NullTransport implements TransportInterface {
    // Pour les tests...
}
💡
Avec l’interface TransportInterface, nous pouvons suivre le principe de substitution de Liskov (soit G, un sous-type de T, peut remplacer T sans modifier la cohérence du programme). Nous pouvons interchanger les trois objets SMTPTransport, MailTransport & NullTransport, sans toucher au code de l’objet Mailer.

6.5. Bénéfices VS Pertes

  • Bénéfices : nous avons un code beaucoup plus configurable, modulaire et testable.

  • Pertes : la création d’objet deviens plus complexe. Nous devons créer les dépendances des dépendances des dépendances,…​ avant de pouvoir utiliser l’objet. C’est là qu’intervient le Pattern Service Locator.

7. Le Pattern Service Locator

7.1. Présentation

📎
Le Pattern Service Locator permet d’encapsuler les mécanismes de création, d’initialisation et d’obtention d’un service.

Vous pouvez ainsi demander au Service Locator de vous fournir des services.

Les services sont des objets globaux (ex: connection à la BDD, un logger, un mailer, …​) qui ont une tâche globale dans l’application, et qui ne contiennent pas d’état particulier.

7.2. Exemple avec le conteneur d’injection de dépendance Pimple

Pimple est très simple à utiliser et permet une première approche du principe d’injection de dépendance : https://pimple.symfony.com/

Qu’est-ce qu’un conteneur ? C’est un objet qui stocke 2 choses :

  1. Global Configuration : des paramètres globaux de configuration, pour pouvoir configurer les services.

  2. Lazy Services : un ensemble de services qui sont chargés à la demande.

7.2.1. Définir les paramètres globaux de configuration

Pour définir des paramètres globaux de configuration, il suffit d’accéder à l’objet Pimple, qui est un objet qui se comporte comme un tableau :

$pimple = new Pimple();                             // | (1)

$pimple['logger.file'] = '/path/to/dev.log';        // | (2)
$pimple['logger.severity'] = 200;                   // |

$pimple['transport.smtp.host'] = 'smtp.foo.com';    // |
$pimple['transport.smtp.port'] = 1234;              // |
$pimple['transport.smtp.user'] = 'mailer';          // |
$pimple['transport.smtp.passwd'] = '^p4$$W0rD*';    // |

// ...
  1. On instancie Pimple (qui a une interface array access).

  2. Il suffit ensuite de "pousser" de nouveaux paramètres globaux dans $pimple.

7.2.2. Enregistrer le service logger

// Pimple global configuration
// ...

$pimple['logger'] = $pimple->share(function ($container) {   // | (1)
    if (!is_writable($container['logger.file'])) {
        throw new Exception('...');
    }

    $logger = new Logger($container('logger.file'));

    if (isset($container['logger.severity'])) {             // | (2)
        $logger->setSeverity($container['logger.severity']);
    }

    return $logger;
});
  1. Au lieu d’être une valeur scalaire, nous passons une fonction anonyme, qui reçoit en paramêtre le conteneur lui-même ($container). Cette fonction anonyme sera appelée lorsque l’on demandera le service logger : Pimple créera, initialisera et transmettra l’objet $logger.

Pimple stockera le logger créé. Lorsque le logger sera demandé à nouveau, il ne sera pas créé une nouvelle fois : Pimple renverra la même instance du logger.

7.2.3. Enregister le service mailer

L’objet mailer dépend de l’objet SMTPTransport. Si je veux utiliser l’objet mailer, je vais devoir aussi créer l’objet SMTPTransport :

$pimple['mailer.transport'] = $pimple->share(function ($container) {    // | (1)
    return new SMTPTransport(
        $container['transport.smtp.host'],                              // | (2)
        $container['transport.smtp.port'],                              // |
        $container['transport.smtp.user'],                              // |
        $container['transport.smtp.passwd']                             // |
    );
});

$pimple['mailer'] = $pimple->share(function ($container) {              // | (3)
    $mailer = new Mailer($container['mailer.transport']);               // | (4)

    if (isset($container['logger'])) {                                  // | (5)
        $mailer->setLogger($container['logger']);                       // |
    }

    return $mailer;
});
  1. On définit l’objet transport comme un service.

  2. On récupère depuis le container les paramètres de configuration globale.

  3. On définit le service mailer.

  4. On définit l’objet mailer qui dépend de l’objet transport.

  5. On injecte le logger (si celui-ci est activé) dans le mailer.

7.2.4. Utiliser les services

Si dans mon application j’ai besoin du mailer, j’ai simplement besoin de le récupérer. Il récupère ensuite en cascade le transport et le logger. Je ne me soucie pas de la façon dont-il faut initialiser l’objet :

$pimple = new Pimple();
$pimple['logger.file'] = '/path/to/dev.log';
$pimple['logger.severity'] = 200;
// ...

$message = Message();
$message->setFrom('[email protected]');
// ...

// Création à la demande du mailer
$pimple['mailer']->send($message);

7.3. Pour aller plus loin

8. Phase de questions dans la conférence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment