design patterns avec php 5.3, symfony et pimple
DESCRIPTION
Cette conférence présente deux grands motifs de conception : l'observateur et l'injection de dépendance. Ce sujet allie à la fois théorie et pratique. Le composant autonome EventDispatcher de Symfony ainsi que le conteneur d'injection de dépendance Pimple sont mis à l'honneur avec des exemples pratiques d'usage. Ces cas pratiques combinent du code de l'ORM Propel ainsi que le composant autonome Zend\Search\Lucene du Zend Framework 2TRANSCRIPT
Simpli!ez-vous les Design Patterns avec PHP 5.3
Hugo Hamon – 12/07/12
Observateur
Dependency Injection
Inversion de Contrôle
Obse
rver
Un sujet, l’objet observable, émet un signal à des modules qui jouent le rôle d’observateurs.
Event Dispatcher
Le Dispatcheur est un objet qui gère les connexions entre le sujet observé et ses observateurs (écouteurs).
# composer.json { "require": { "php": ">=5.3.3", "symfony/event-dispatcher": "2.1.*" } }
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());
Mise
en Pr
atiqu
e
use AFUP\Model\Article; $article = new Article(); $article->setTitle('AFUP Design Patterns'); $article->setContent('Some **content**'); $article->save(); echo $article->getHtmlContent();
<p> Some <strong>content</strong> </p>
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; } }
Le Sujet Observé
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; } }
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; } }
Propager un Evénement
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; } }
Ajouter des écouteurs
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); } }
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(); // ... } }
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(); } }
Enregistrer les écouteurs
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);
$article = new Article(); $article->setDispatcher($dispatcher); $article->setTitle('AFUP Design Patterns'); $article->setMarkdownContent( 'Some **markdown** content' ); $article->save();
Dependency Injection
Mauvaise Conception Initiale
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; } } }
$message = new Message(); $message->setFrom('[email protected]'); $message->setTo('[email protected]'); $message->setSubject('Bonjour ...'); $message->setBody('Hello ...'); $mailer = new Mailer(); $mailer->send($message);
Ca fonctionne !
Oui mais ?!!!
$transport = new SMTPTransport( 'smtp.foo.com', 1234, 'mailer', 'p$wD^' );
Je veux utiliser un transport différent…
Je veux con!gurer le SMTP en dev et en prod…
$logger = Logger::getInstance(); $logger->log('Unable to...'); $logger->logException($e);
Si le logger n’existe pas ?
Si je veux changer la con!guration du logger?
Je veux tester mon code avec PHPUnit et je n’y arrive pas…
La Solution?
Injecter ses dépendances au Mailer
What ???
Injection par les propriétés
class Mailer { public $transport; public function send(Message $message) { try { $this->transport->send($message); } catch (TransportException $e) { // ... } } }
$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);
Injection par constructeur
class Mailer { private $transport; function __construct(Transport $t) { $this->transport = $t; } }
$message = Message(); $message->setFrom('[email protected]'); // ... $transport = new SMTPTransport('...'); $mailer = new Mailer($transport); $mailer->send($message);
Injection par un mutateur
class Mailer { private $logger; function setLogger(Logger $logger) { $this->logger = $logger; } }
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; } } } }
$message = Message(); // ... $logger = new FileLogger('/to/dev.log'); $transport = new SMTPTransport('...'); $mailer = new Mailer($transport); $mailer->setLogger($logger); $mailer->send($message);
Découplage avec les interfaces
class Mailer { function __construct(TransportInterface $t) { $this->transport = $t; } function setLogger(LoggerInterface $logger) { $this->logger = $logger; } }
class SMTPTransport implements TransportInterface { } class MailTransport implements TransportInterface { } class NullTransport implements TransportInterface { }
Béné!ces vs Pertes ?!
Con!gurabilité Modularité Testabilité
Construction un peu plus Complexe
Inversion de Contrôle
# composer.json { "require": { "pimple/pimple": "1.0.*" } }
Global Con!guration + Lazy Services
= Container
Paramètres Globaux de Con!guration
$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*';
Enregistrer des services
$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; });
$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'] ); });
$pimple['mailer'] = $pimple->share(function ($c) { $mailer = new Mailer($c['mailer.transport']); if (isset($c['logger'])) { $mailer->setLogger($c['logger']); } return $mailer; });
Initialisation des services à la demande
$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);