Design Patterns avec PHP 5.3, Symfony et Pimple

Post on 15-Jan-2015

4.799 views 1 download

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 2

Transcript of Design Patterns avec PHP 5.3, Symfony et Pimple

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('me@example.com'); $message->setTo('you@example.com'); $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('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);

Injection par constructeur

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

$message = Message(); $message->setFrom('me@example.com'); // ... $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('me@example.com'); // ... // Création à la demande du mailer $pimple['mailer']->send($message);