Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Simplifiez-vous les design patterns avec Symfony

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
November 30, 2012

Simplifiez-vous les design patterns avec Symfony

Au cours de cette conférence, nous aborderons l'implémentation pratique des patrons Observateur et Service Locator au travers de deux libraries : le composant EventDispatcher du framework Symfony et Pimple, le conteneur d'injection de dépendance du framework Silex.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

November 30, 2012
Tweet

Transcript

  1. http://brightsideof.com/wp-content/uploads/2012/11/m4-1024x679.jpg Simplifiez-vous les design patterns avec Symfony2

  2. Patron de Conception « En génie logiciel, un patron de

    conception est une solution générique d'implémentation répondant à un problème spécifique. » http://fr.wikibooks.org/wiki/Patrons_de_conception
  3. Disclaimer

  4. Observateur Injection de Dépendance Service Locator

  5. Observer

  6. 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 = 'sales@acme.com'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  7. Trop de responsabilités! => SOLID Principle

  8. Couplage fort!

  9. Evolutivité limitée!

  10. Maintenance peu aisée!

  11. None
  12. Un sujet, l’objet observable, émet un signal à des modules

    qui jouent le rôle d’observateurs.
  13. None
  14. interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface {

    function attach(ObserverInterface $observer); function notify(); }
  15. Implémenter des Observateurs

  16. class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface

    $subject) { $reference = $subject->getReference(); $this->logger->log('New order #'. $reference); } }
  17. class CustomerNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface

    $subject) { $mail = new Email(); $mail->recipient = $subject->customer->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); } }
  18. class SalesNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface

    $subject) { $mail = new Email(); $mail->recipient = 'sales@acme.com'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  19. Notifier les Observateurs

  20. class Order implements ObservableInterface { // ... private $observers; public

    function attach(ObserverInterface $observer) { $this->observers[] = $observer; } public function notifyObservers() { foreach ($this->observers as $observer) { $observer->notify($this); } } }
  21. class Order implements ObservableInterface { public function confirm() { $this->status

    = 'confirmed'; $this->save(); $this->notifyObservers(); } }
  22. class OrderController { public function finishOrderAction() { $order = new

    Order(); $order->attach(new LoggerNotifier($logger)); $order->attach(new CustomerNotifier($mailer)); $order->attach(new SalesNotifier($mailer)); $order->customer = $customer; $order->amount = 150; // Exécution des observateurs $order->confirm(); } }
  23. SplObserver SplSubject

  24. class SalesNotifier extends SplObserver { public $mailer; public function update(SplSubject

    $subject) { $mail = new Email(); $mail->recipient = 'sales@acme.com'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  25. class Order extends SplSubject { // ... private $observers; public

    function attach(SplObserver $observer) { $this->observers[] = $observer; } public function notify() { foreach ($this->observers as $observer) { $observer->notify($this); } } }
  26. Limitations?

  27. EventDispatcher

  28. Le Dispatcheur est un objet qui gère les connexions entre

    le sujet observé et ses observateurs (écouteurs).
  29. "symfony/event-dispatcher": "2.1.*"

  30. use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; use AFUP\ArticleListener; $dispatcher = new EventDispatcher();

    // Déclaration des écouteurs $listener1 = array(new ArticleListener(), 'onDelete'); $listener2 = array(new ArticleListener(), 'onSave'); // Enregistrement des écouteurs $dispatcher->addListener('article.delete', $listener1); $dispatcher->addListener('article.pre_save', $listener2); // Notification des écouteurs $dispatcher->dispatch('article.pre_save', new Event());
  31. Mise en Pratique

  32. use AFUP\Model\Article; $article = new Article(); $article->setTitle('AFUP Design Patterns'); $article->setContent('Some

    **content**'); $article->save(); echo $article->getHtmlContent();
  33. <p> Some <strong>content</strong> </p>

  34. namespace AFUP\Model; use Propel\Runtime\Connection\ConnectionInterface; use dflydev\markdown\MarkdownParser; class Article extends \AFUP\Model\Base\Article

    { public function save(ConnectionInterface $con = null) { $parser = new MarkdownParser(); $html = $parser->transformMarkdown($this->getContent()); $this->setHtmlContent($html); $ret = parent::save($con); $this->updateLuceneIndex(); return $ret; } }
  35. Le Sujet Observé

  36. namespace AFUP\Model; use Symfony\Component\EventDispatcher\EventDispatcher; use AFUP\Model\Base\Article as BaseArticle; class Article

    extends BaseArticle { private $dispatcher; public function setDispatcher(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } }
  37. namespace AFUP\Model; // ... use Propel\Runtime\Connection\ConnectionInterface; use AFUP\Event\ArticleEvent; class Article

    extends BaseArticle { // ... public function save(ConnectionInterface $con = null) { $event = new ArticleEvent($this); $this->dispatcher->dispatch('article.pre_save', $event); $ret = parent::save($con); $this->dispatcher->dispatch('article.post_save', $event); return $ret; } }
  38. Propager un Evénement

  39. namespace AFUP\Event; use Symfony\Component\EventDispatcher\Event; use AFUP\Model\Article; class ArticleEvent extends Event

    { private $article; public function __construct(Article $article) { $this->article = $article; } public function getArticle() { return $this->article; } }
  40. Ajouter des écouteurs

  41. namespace AFUP\Listener; use AFUP\Event\ArticleEvent; use dflydev\markdown\MarkdownParser; class ArticleListener { public

    function onPreSave(ArticleEvent $event) { $article = $event->getArticle(); $markdown = $article->getContent(); $parser = new MarkdownParser(); $html = $parser->transformMarkdown($markdown); $article->setHtmlContent($html); } }
  42. namespace AFUP\Listener; use Zend\Search\Lucene\Document; use Zend\Search\Lucene\Document\Field; use AFUP\Event\ArticleEvent; use AFUP\Model\ArticlePeer;

    class LuceneListener { public function onPostSave(ArticleEvent $event) { $article = $event->getArticle(); // ... } }
  43. namespace AFUP\Listener; // ... class LuceneListener { public function onPostSave(ArticleEvent

    $event) { $article = $event->getArticle(); $index = ArticlePeer::getLuceneIndex(); // remove existing entries foreach ($index->find('pk:'.$article->getId()) as $hit) { $index->delete($hit->id); } $doc = new Document(); $doc->addField(Field::Keyword('pk', $article->getId())); $doc->addField(Field::UnStored('title', $article->getTitle())); $doc->addField(Field::UnStored('content', $article->getContent())); $index->addDocument($doc); $index->commit(); } }
  44. Enregistrer les écouteurs

  45. use Symfony\Component\EventDispatcher\EventDispatcher; use AFUP\Listener\ArticleListener; use AFUP\Listener\LuceneListener; use AFUP\Model\Article; // Déclaration

    des écouteurs $listener1 = array(new ArticleListener(), 'onPreSave'); $listener2 = array(new LuceneListener(), 'onPostSave'); // Enregistrement des écouteurs $dispatcher = new EventDispatcher(); $dispatcher->addListener('article.pre_save', $listener1); $dispatcher->addListener('article.post_save', $listener2);
  46. $article = new Article(); $article->setDispatcher($dispatcher); $article->setTitle('AFUP Design Patterns'); $article->setMarkdownContent( 'Some

    **markdown** content' ); $article->save();
  47. Injection de dépendance

  48. Single Responsability Open / Close principle Liskov substitution Interface segregation

    Dependency Injection
  49. « Dependency Injection is where components are given their dependencies

    through their constructors, methods, or directly into fields. » http://picocontainer.codehaus.org/injection.html
  50. Mauvaise Conception Initiale

  51. class Mailer { public function send(Message $message) { try {

    $transport = new SMTPTransport( 'smtp.foo.com', 1234, 'mailer', 'p$wD^' ); return $transport->send($message); } catch (TransportException $e) { $logger = Logger::getInstance(); $logger->log('Unable to send message to...'); $logger->logException($e); throw $e; } } }
  52. $message = new Message(); $message->setFrom('me@example.com'); $message->setTo('you@example.com'); $message->setSubject('Bonjour ...'); $message->setBody('Hello ...');

    $mailer = new Mailer(); $mailer->send($message);
  53. ça fonctionne !

  54. Oui mais ?!!!

  55. $transport = new SMTPTransport( 'smtp.foo.com', 1234, 'mailer', 'p$wD^' );

  56. Je veux utiliser un transport différent…

  57. Je veux configurer le SMTP en dev et en prod…

  58. $logger = Logger::getInstance(); $logger->log('Unable to...'); $logger->logException($e);

  59. Si le logger n’existe pas ?

  60. Si je veux changer la configuration du logger?

  61. Je veux tester mon code et je n’y arrive pas…

  62. La Solution?

  63. Injecter ses dépendances au Mailer

  64. What ???

  65. Injection par les propriétés

  66. class Mailer { public $transport; public function send(Message $message) {

    try { $this->transport->send($message); } catch (TransportException $e) { // ... } } }
  67. $message = Message(); $message->setFrom('me@example.com'); $message->setTo('you@example.com'); $message->setSubject('Bonjour ...'); $message->setBody('Hello ...'); $transport

    = new SMTPTransport('...'); $mailer = new Mailer(); $mailer->transport = $transport; $mailer->send($message);
  68. Injection par constructeur

  69. class Mailer { private $transport; function __construct(Transport $t) { $this->transport

    = $t; } }
  70. $message = Message(); $message->setFrom('me@example.com'); // ... $transport = new SMTPTransport('...');

    $mailer = new Mailer($transport); $mailer->send($message);
  71. Injection par un mutateur

  72. class Mailer { private $logger; function setLogger(Logger $logger) { $this->logger

    = $logger; } }
  73. class Mailer { // ... public function send(Message $message) {

    try { $this->transport->send($message); } catch (TransportException $e) { if (null !== $this->logger) { $this->logger->log('...'); $this->logger->logException($e); throw $e; } } } }
  74. $message = Message(); // ... $logger = new FileLogger('/to/dev.log'); $transport

    = new SMTPTransport('...'); $mailer = new Mailer($transport); $mailer->setLogger($logger); $mailer->send($message);
  75. Découplage avec les interfaces

  76. class Mailer { function __construct(TransportInterface $t) { $this->transport = $t;

    } function setLogger(LoggerInterface $logger) { $this->logger = $logger; } }
  77. class SMTPTransport implements TransportInterface { } class MailTransport implements TransportInterface

    { } class NullTransport implements TransportInterface { }
  78. Bénéfices vs Pertes ?!

  79. Configurabilité Modularité Testabilité

  80. Construction un peu plus Complexe

  81. Service Locator

  82. « Le patron service locator permet d’encapsuler les mécanismes d’obtention

    d’un service. »
  83. # composer.json { "require": { "pimple/pimple": "1.0.*" } }

  84. Global Configuration + Lazy Services = Container

  85. Paramètres Globaux de Configuration

  86. $pimple = new Pimple(); $pimple['logger.file'] = '/path/to/dev.log'; $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*';
  87. Enregistrer des services

  88. $pimple['logger'] = $pimple->share(function ($c) { if (!is_writable($c['logger.file'])) { throw new

    Exception('...'); } $logger = new Logger($c['logger.file']); if (isset($c['logger.severity'])) { $logger->setSeverity($c['logger.severity']); } return $logger; });
  89. $pimple['mailer.transport'] = $pimple ->share(function ($c) { return new SMTPTransport( $c['transport.smtp.host'],

    $c['transport.smtp.port'], $c['transport.smtp.user'], $c['transport.smtp.passwd'] ); });
  90. $pimple['mailer'] = $pimple->share(function ($c) { $mailer = new Mailer($c['mailer.transport']); if

    (isset($c['logger'])) { $mailer->setLogger($c['logger']); } return $mailer; });
  91. Initialisation des services à la demande

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

    // ... $message = Message(); $message->setFrom('me@example.com'); // ... // Création à la demande du mailer $pimple['mailer']->send($message);
  93. Pour aller plus loin!

  94. DependencyInjection

  95. None