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

Simplifiez-vous les design patterns avec Symfony

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.

Hugo Hamon

November 30, 2012
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

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

    View Slide

  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

    View Slide

  3. Disclaimer

    View Slide

  4. Observateur
    Injection de Dépendance
    Service Locator

    View Slide

  5. Observer

    View Slide

  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 = '[email protected]';
    $mail->subject = 'New order to ship!';
    $mail->message = '...';
    $this->mailer->send($mail);
    }
    }

    View Slide

  7. Trop de
    responsabilités!
    => SOLID Principle

    View Slide

  8. Couplage
    fort!

    View Slide

  9. Evolutivité
    limitée!

    View Slide

  10. Maintenance
    peu aisée!

    View Slide

  11. View Slide

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

    View Slide

  13. View Slide

  14. interface ObserverInterface
    {
    function notify(ObservableInterface $subject);
    }
    interface ObservableInterface
    {
    function attach(ObserverInterface $observer);
    function notify();
    }

    View Slide

  15. Implémenter des
    Observateurs

    View Slide

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

    View Slide

  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);
    }
    }

    View Slide

  18. class SalesNotifier implements ObserverInterface
    {
    public $mailer;
    public function notify(ObservableInterface $subject)
    {
    $mail = new Email();
    $mail->recipient = '[email protected]';
    $mail->subject = 'New order to ship!';
    $mail->message = '...';
    $this->mailer->send($mail);
    }
    }

    View Slide

  19. Notifier les
    Observateurs

    View Slide

  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);
    }
    }
    }

    View Slide

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

    View Slide

  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();
    }
    }

    View Slide

  23. SplObserver
    SplSubject

    View Slide

  24. class SalesNotifier extends SplObserver
    {
    public $mailer;
    public function update(SplSubject $subject)
    {
    $mail = new Email();
    $mail->recipient = '[email protected]';
    $mail->subject = 'New order to ship!';
    $mail->message = '...';
    $this->mailer->send($mail);
    }
    }

    View Slide

  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);
    }
    }
    }

    View Slide

  26. Limitations?

    View Slide

  27. EventDispatcher

    View Slide

  28. Le Dispatcheur est un
    objet qui gère les
    connexions entre le
    sujet observé et ses
    observateurs
    (écouteurs).

    View Slide

  29. "symfony/event-dispatcher": "2.1.*"

    View Slide

  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());

    View Slide

  31. Mise en Pratique

    View Slide

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

    View Slide


  33. Some content

    View Slide

  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;
    }
    }

    View Slide

  35. Le Sujet
    Observé

    View Slide

  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;
    }
    }

    View Slide

  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;
    }
    }

    View Slide

  38. Propager un
    Evénement

    View Slide

  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;
    }
    }

    View Slide

  40. Ajouter des
    écouteurs

    View Slide

  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);
    }
    }

    View Slide

  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();
    // ...
    }
    }

    View Slide

  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();
    }
    }

    View Slide

  44. Enregistrer les
    écouteurs

    View Slide

  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);

    View Slide

  46. $article = new Article();
    $article->setDispatcher($dispatcher);
    $article->setTitle('AFUP Design Patterns');
    $article->setMarkdownContent(
    'Some **markdown** content'
    );
    $article->save();

    View Slide

  47. Injection de dépendance

    View Slide

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

    View Slide

  49. « Dependency Injection is
    where components are
    given their dependencies
    through their
    constructors, methods, or
    directly into fields. »
    http://picocontainer.codehaus.org/injection.html

    View Slide

  50. Mauvaise
    Conception
    Initiale

    View Slide

  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;
    }
    }
    }

    View Slide

  52. $message = new Message();
    $message->setFrom('[email protected]');
    $message->setTo('[email protected]');
    $message->setSubject('Bonjour ...');
    $message->setBody('Hello ...');
    $mailer = new Mailer();
    $mailer->send($message);

    View Slide

  53. ça
    fonctionne !

    View Slide

  54. Oui mais ?!!!

    View Slide

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

    View Slide

  56. Je veux
    utiliser un
    transport
    différent…

    View Slide

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

    View Slide

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

    View Slide

  59. Si le logger
    n’existe
    pas ?

    View Slide

  60. Si je veux
    changer la
    configuration
    du logger?

    View Slide

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

    View Slide

  62. La Solution?

    View Slide

  63. Injecter ses
    dépendances
    au Mailer

    View Slide

  64. What ???

    View Slide

  65. Injection
    par les
    propriétés

    View Slide

  66. class Mailer
    {
    public $transport;
    public function send(Message $message)
    {
    try {
    $this->transport->send($message);
    } catch (TransportException $e) {
    // ...
    }
    }
    }

    View Slide

  67. $message = Message();
    $message->setFrom('[email protected]');
    $message->setTo('[email protected]');
    $message->setSubject('Bonjour ...');
    $message->setBody('Hello ...');
    $transport = new SMTPTransport('...');
    $mailer = new Mailer();
    $mailer->transport = $transport;
    $mailer->send($message);

    View Slide

  68. Injection par
    constructeur

    View Slide

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

    View Slide

  70. $message = Message();
    $message->setFrom('[email protected]');
    // ...
    $transport = new SMTPTransport('...');
    $mailer = new Mailer($transport);
    $mailer->send($message);

    View Slide

  71. Injection
    par un
    mutateur

    View Slide

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

    View Slide

  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;
    }
    }
    }
    }

    View Slide

  74. $message = Message();
    // ...
    $logger = new FileLogger('/to/dev.log');
    $transport = new SMTPTransport('...');
    $mailer = new Mailer($transport);
    $mailer->setLogger($logger);
    $mailer->send($message);

    View Slide

  75. Découplage
    avec les
    interfaces

    View Slide

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

    View Slide

  77. class SMTPTransport implements TransportInterface
    {
    }
    class MailTransport implements TransportInterface
    {
    }
    class NullTransport implements TransportInterface
    {
    }

    View Slide

  78. Bénéfices
    vs
    Pertes ?!

    View Slide

  79. Configurabilité
    Modularité
    Testabilité

    View Slide

  80. Construction
    un peu plus
    Complexe

    View Slide

  81. Service Locator

    View Slide

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

    View Slide

  83. # composer.json
    {
    "require": {
    "pimple/pimple": "1.0.*"
    }
    }

    View Slide

  84. Global Configuration
    + Lazy Services
    = Container

    View Slide

  85. Paramètres
    Globaux de
    Configuration

    View Slide

  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*';

    View Slide

  87. Enregistrer
    des services

    View Slide

  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;
    });

    View Slide

  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']
    );
    });

    View Slide

  90. $pimple['mailer'] = $pimple->share(function ($c) {
    $mailer = new Mailer($c['mailer.transport']);
    if (isset($c['logger'])) {
    $mailer->setLogger($c['logger']);
    }
    return $mailer;
    });

    View Slide

  91. Initialisation
    des services à
    la demande

    View Slide

  92. $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);

    View Slide

  93. Pour aller
    plus loin!

    View Slide

  94. DependencyInjection

    View Slide

  95. View Slide