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 full-size 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 full-size slide

  3. Observateur
    Injection de Dépendance
    Service Locator

    View full-size slide

  4. 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 full-size slide

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

    View full-size slide

  6. Couplage
    fort!

    View full-size slide

  7. Evolutivité
    limitée!

    View full-size slide

  8. Maintenance
    peu aisée!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. Implémenter des
    Observateurs

    View full-size slide

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

    View full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. Notifier les
    Observateurs

    View full-size slide

  16. 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 full-size slide

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

    View full-size slide

  18. 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 full-size slide

  19. SplObserver
    SplSubject

    View full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. Limitations?

    View full-size slide

  23. EventDispatcher

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. 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 full-size slide

  27. Mise en Pratique

    View full-size slide

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

    View full-size slide


  29. Some content

    View full-size slide

  30. 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 full-size slide

  31. Le Sujet
    Observé

    View full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. Propager un
    Evénement

    View full-size slide

  35. 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 full-size slide

  36. Ajouter des
    écouteurs

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. Enregistrer les
    écouteurs

    View full-size slide

  41. 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 full-size slide

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

    View full-size slide

  43. Injection de dépendance

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. Mauvaise
    Conception
    Initiale

    View full-size slide

  47. 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 full-size slide

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

    View full-size slide

  49. ça
    fonctionne !

    View full-size slide

  50. Oui mais ?!!!

    View full-size slide

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

    View full-size slide

  52. Je veux
    utiliser un
    transport
    différent…

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  55. Si le logger
    n’existe
    pas ?

    View full-size slide

  56. Si je veux
    changer la
    configuration
    du logger?

    View full-size slide

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

    View full-size slide

  58. La Solution?

    View full-size slide

  59. Injecter ses
    dépendances
    au Mailer

    View full-size slide

  60. Injection
    par les
    propriétés

    View full-size slide

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

    View full-size slide

  62. $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 full-size slide

  63. Injection par
    constructeur

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  66. Injection
    par un
    mutateur

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

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

    View full-size slide

  70. Découplage
    avec les
    interfaces

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  73. Bénéfices
    vs
    Pertes ?!

    View full-size slide

  74. Configurabilité
    Modularité
    Testabilité

    View full-size slide

  75. Construction
    un peu plus
    Complexe

    View full-size slide

  76. Service Locator

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  79. Global Configuration
    + Lazy Services
    = Container

    View full-size slide

  80. Paramètres
    Globaux de
    Configuration

    View full-size slide

  81. $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 full-size slide

  82. Enregistrer
    des services

    View full-size slide

  83. $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 full-size slide

  84. $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 full-size slide

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

    View full-size slide

  86. Initialisation
    des services à
    la demande

    View full-size slide

  87. $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 full-size slide

  88. Pour aller
    plus loin!

    View full-size slide

  89. DependencyInjection

    View full-size slide