[Mémo] AFUP - PHP Tour 2012 - Symfony, Design Pattern Observer, injection de dépendance et Service Locator (Hugo Hamon)
- 1. Ressources
- 2. Remerciements
- 3. Pourquoi un
Design Pattern
? - 4. Le
Design Pattern Observer
- 5. Le composant
EventDispatcher
(une variation duDesign Pattern Observer
) - 6. L’injection de dépendance
- 7. Le
Pattern Service Locator
- 8. Phase de questions dans la conférence
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 |
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 :)
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.
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=237 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=5 |
Le Design Pattern Observer
permet de découpler les dépendances et d’étendre facilement les capacités d’un objet.
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);
}
}
Trop de responsabilités :
Cette classe Order
à trois responsabilités :
-
Elle persiste de l’information en BDD.
-
Elle logge un message dans le logger.
-
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.
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 .
|
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=495 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=11 |
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. |
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=572 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=14 |
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();
}
-
Un observateur devra répondre à l’interface
ObserverInterface
et implémenter la méthodenotify()
qui va recevoir en paramètre le sujet ($subject
) que cet observateur observe. -
L’interface
ObservableInterface
définie l’objet observé, auquel on rattache ses observateurs avec la méthodeattach()
, et qui pourra notifier l’ensemble de ses observateurs avec la méthodenotify()
.
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);
}
}
-
On injecte le
logger
dans l’objetLoggerHandler
, via le constructeur par exemple. -
Quand l’objet
LoggerHandler
sera notifié, la méthodenotify()
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);
}
}
-
On injecte le
mailer
dans l’objetCustomerNotifier
, via le constructeur par exemple. -
Quand l’objet
CustomerNotifier
sera notifié, la méthodenotify()
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);
}
}
-
On injecte le
mailer
dans l’objetSalesNotifier
, via le constructeur par exemple. -
Quand l’objet
SalesNotifier
sera notifié, la méthodenotify()
recevra la commande en entrée ($subject
) et enverra le mail au département des ventes.
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);
}
}
}
-
La méthode
attach()
permet de rajouter un observateur à la liste des observateurs de cette classeOrder
. -
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();
}
}
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)
}
}
-
On rattache les observateurs à l’objet
Order
. -
À la confirmation de la commande, la méthode
notifyObservers()
de la classeOrder
sera exécutée, et tous les observateurs rattachés à la commande avec la méthodeattach()
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 .
|
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=897 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=23 |
La Standard PHP Library (SPL)
propose, pour le Design Pattern Observer
, deux classes abstraites :
-
SplObserver
: http://php.net/manual/fr/class.splobserver.php -
SplSubject
: http://php.net/manual/fr/class.splsubject.php
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
}
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);
}
}
}
-
Vous devez étendre les classes abstraites
SplObserver
etSplSubject
, ce qui nous limite car nous ne pouvons pas étendre plusieurs classes à la fois en même temps. -
L’objet
Order
doit être initialisé et nous devons lui donner l’ensemble de ses observateurs. À chaque implémentation duDesign 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 destraits
pour éviter de dupliquer du code). -
Avec la méthode
attach()
, on rend directement dépendant l’objet observé avec ses observateurs.
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=1055 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=27 |
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.
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)
-
On définie des écouteurs (= observateurs). Un écouteur peut être n’importe quel
callable
PHP. -
On connecte les écouteurs aux événements.
-
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.
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;
}
}
-
On transforme le contenu
markdown
enhtml
. -
On sauve en BDD les informations.
-
On met à jour l’index
Lucene
du moteur de recherche.
|
On peut noter que la classe
|
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;
}
}
-
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;
}
}
-
On dispatche l’événement
article.pre_save
en transmettant un objetArticleEvent
. -
On dispatche l’événement
article.post_save
en transmettant un objetArticleEvent
.
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;
}
}
-
On étend la classe
Event
. -
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. |
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); // |
}
}
-
On récupère l’article rattaché à l’événement.
-
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)
// ...
}
}
-
On récupère l’article rattaché à l’événement.
-
On fait ici la mise à jour des index du moteur de recherche.
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); // |
-
On définie les écouteurs (= observateurs)
ArticleListener
&LuceneListener
. -
On connecte les écouteurs aux événements.
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();
-
À l’exécution de la méthode
save()
, les événementsarticle.pre_save
&article.post_save
seront dispatchés.
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=1635 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=47 |
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.
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;
}
}
}
-
Nous avons une méthode
send()
qui permet d’envoyer un message. -
Pour envoyer ce message, la classe fait appel à un objet
SMTPTransport
que nous allons configurer. -
L’objet
SMTPTransport
peut lever une exception. -
Si nous avons une exception, nous récupérons un objet
Logger
unique (singleton
), qui va permettre de logger un message.
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 classeNullTransport
par exemple. -
Nous ne pourrons pas configurer spécifiquement notre serveur
SMTP
pour chacun des environnements deprod
(serveur en ligne, authentification avecuser
etpassword
) et dedev
(serveur local).
L’objet Logger
va être contraignant :
-
Que se passe-t-il dans le cas où nous désactivons le
Logger
enprod
? -
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…
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.
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;
}
}
}
-
Nous avons une propriété publique à laquelle nous passons directement l’objet de transport créé à l’extérieur de l’objet
Mailer
. -
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);
-
Nous créons l’objet
$transport
depuis l’extérieur. -
Nous passons directement l’objet
$transport
à la propriété publique$transport
de l’objetMailer
.
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;
}
}
}
-
Nous avons une propriété privée
$transport
. -
Nous passons l’objet de transport créé à l’extérieur de l’objet
Mailer
, via la méthode__construct()
. -
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);
-
Nous créons l’objet
$transport
depuis l’extérieur. -
Nous passons l’objet
$transport
à la propriété privée$transport
de l’objetMailer
, via la méthode__construct()
.
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;
}
}
}
-
Nous avons une propriété privée
$logger
. -
Nous passons l’objet créé à l’extérieur de l’objet
Mailer
, via la méthodesetLogger()
. -
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);
-
Nous créons l’objet
$transport
depuis l’extérieur. -
Nous créons l’objet
$logger
depuis l’extérieur aussi. -
Nous passons l’objet
$transport
à la propriété privée$transport
de l’objetMailer
, via la méthode__construct()
. -
Nous passons l’objet
$logger
à la propriété privée$logger
de l’objetMailer
, via le mutateursetLogger()
.
📎
|
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 .
|
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 .
|
-
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
.
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=1938 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=81 |
📎
|
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.
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 :
-
Global Configuration
: des paramètres globaux de configuration, pour pouvoir configurer les services. -
Lazy Services
: un ensemble de services qui sont chargés à la demande.
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*'; // |
// ...
-
On instancie
Pimple
(qui a une interfacearray access
). -
Il suffit ensuite de "pousser" de nouveaux paramètres globaux dans
$pimple
.
// 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;
});
-
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 servicelogger
: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 .
|
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;
});
-
On définit l’objet
transport
comme un service. -
On récupère depuis le
container
les paramètres de configuration globale. -
On définit le service
mailer
. -
On définit l’objet
mailer
qui dépend de l’objettransport
. -
On injecte le
logger
(si celui-ci est activé) dans lemailer
.
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);
-
https://framework.zend.com/manual/2.4/en/tutorials/quickstart.di.html
-
https://symfony.com/doc/current/components/dependency_injection.html
-
Configuration en xml, yml, php.
-
Faire de la compilation du conteneur en une pure classe PHP.
💡
|
Repartir au bon endroit dans la vidéo et les slides : https://youtu.be/uzH-Rcj793A?t=2277 https://speakerdeck.com/hhamon/simplifiez-vous-les-design-patterns-avec-symfony?slide=95 |