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

Practical Design PHPatterns

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
January 26, 2013

Practical Design PHPatterns

Observer, adapter, decorator, facade, composite… All these weird words refer to design patterns. Design patterns are general reusable solutions for solving common redundant problems in software architecture. Getting inspiration on some well known design patterns and implementing them in PHP will make your application code easier to understand, maintain and evolve. Based on very practical code samples, this talk will focus on the most usefull patterns in order to help you design better PHP applications.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

January 26, 2013
Tweet

Transcript

  1. Practical Design PHPatterns Hugo Hamon – PHPBenelux 2013 - Antwerp

    http://wwp.greenwichmeantime.com
  2. None
  3. What are design patterns?

  4. Definition « In software design, a design pattern is an

    abstract generic solution to solve a particular redundant problem. »
  5. Patterns categories Creation Structure Behavior

  6. Want to learn?

  7. Disclaimer Patterns are not the holly grail!

  8. Today’s patterns o  Facade o  Adapter o  Template Method o 

    Strategy o  Decorator o  Composite o  Factory Method o  Observer
  9. Facade http://www.flickr.com/photos/br1dotcom/5930994659

  10. Wrapping a complicated system in order to provide a simplier

    interface to use it. Goal  
  11. Parsing an Atom XML feed to extract some information. Practical

    Example  
  12. $content = file_get_contents(__DIR__.'/feed.xml'); $xml = new SimpleXMLElement($content); $title = (string)

    $xml->title; $entries = array(); $entry = array(); foreach ($this->xml->entry as $entry) { $entry['title'] = (string) $entry->title; $entry['summary'] = (string) $entry->summary; $entry['id'] = (string) $entry->id; if (isset($entry->author)) { // parse <author> node $author = ...; $entry['author'] = $author; } $entries[] = $entry; $entry = array(); }
  13. Encapsulating the complex xml parsing logic into a facade object.

    Solution  
  14. class FeedReader { public function getTitle() { [complex xml parsing

    logic here] } public function getEntries() { [complex xml parsing logic here] } }
  15. public function getEntries() { if (null === $this->xml) { $this->loadXml();

    } $entries = array(); $entry = array(); foreach ($this->xml->entry as $entry) { $entry['title'] = (string) $entry->title; $entry['summary'] = (string) $entry->summary; $entry['id'] = (string) $entry->id; if ($author = $this->getAuthor($entry)) { $entry['author'] = $author; } $entries[] = $entry; $entry = array(); } return $entries; }
  16. $facade = new FeedReader('feed.xml'); $title = $facade->getTitle(); $entries = $facade->getEntries();

    foreach ($entries as $entry) { echo $entry['title'],"\n"; if (isset($entry['author'])) { echo $entry['author']['name']; } }
  17. Adapter

  18. Providing a unique API to objects that don’t share the

    same API. Goal  
  19. Fetching weather forecasts with two different weather providers, which don’t

    have the same API. Practical Example  
  20. A weather client object relies on the famous WahooWeather web

    service to collect daily forecasts for a city.
  21. class WeatherService { private $api; public function __construct(WahooWeatherApi $api) {

    $this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } }
  22. class WeatherService { private $api; public function __construct(WahooWeatherApi $api) {

    $this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } } Concrete dependency Current API
  23. But a new competitor, BoogleWeather, now comes with a much

    more powerful API to get daily forecasts for a city.
  24. We want the weather service Client supports both weather providers

    although they don’t share the same API at all…
  25. $api = new WahooWeatherApi(); $result = $api->getCityForecasts('Paris', '2013-02-25'); $result =

    array( 'city' => 'Paris', 'country' => 'France', 'date' => '2013-02-25', 'conditions' => 'snowy', 'temperature' => '-6', 'unit' => 'C', );
  26. $weather = new BoogleWeather(); $weather->city = 'Paris'; $weather->date = '02/25/2013';

    $json = $weather->getForecasts(); $json = '{ "location": "Paris, France", "temp": "21.2", "conditions": "snowy", "date": "02\/05\/2013” }';
  27. WahooWeatherApi BoogleWeather Date in YYYY-mm-dd format Date in mm/dd/YYYY format

    City is passed to the method City is set in a public property Method is getCityForecasts() Method is getForecasts() Returns an array Returns a JSON string APIs differences
  28. Designing an Adapter helps to provide a single public API

    to support the two heterogeneous Adaptees.
  29. None
  30. abstract class WeatherProviderAdapter { protected $date; public function setDate($date) {

    $this->date = new DateTime($date); } abstract public function getForecasts($city); }
  31. class WahooProviderAdapter extends WeatherProviderAdapter { private $api; public function __construct(WahooWeatherApi

    $api) { $this->api = $api; } public function getForecasts($city) { $date = $this->date->format('Y-m-d'); return $this->api->getCityForecasts($city, $date); } }
  32. class BoogleProviderAdapter extends WeatherProviderAdapter { private $api; public function __construct(BoogleWeather

    $api) { $this->api = $api; } public function getForecasts($city) { $this->api->date = $this->date->format('m/d/Y'); $this->api->city = $city; $json = $this->api->getWeather(); // Convert the JSON output to an array expected by the client return $this->normalize($json); } }
  33. class WeatherService { private $provider; public function __construct(WeatherProviderAdapter $provider) {

    $this->provider = $provider; } public function getForecasts($city, $date) { $this->provider->setDate($date); return $this->provider->getForecasts($city); } }
  34. class WeatherService { private $provider; public function __construct(WeatherProviderAdapter $provider) {

    $this->provider = $provider; } public function getForecasts($city, $date) { $this->provider->setDate($date); return $this->provider->getForecasts($city); } } Abstraction Shared public API
  35. // Adapter for the Wahoo weather service $adaptee = new

    WahooWeatherApi(); $adapter = new WahooProviderAdapter($adaptee); // Adapter for the Boogle weather service $adaptee = new BoogleWeather(); $adapter = new BoogleProviderAdapter($adaptee); // Client code that deals with the adapters $client = new WeatherService($adapter); $client->getForecasts('Paris', '2013-02-25');
  36. Template Method http://www.flickr.com/photos/jmgeorges/4849023808

  37. Let subclasses redefine certain steps of an algorithm without changing

    the algorithm’s structure. Goal  
  38. Inserting or updating a record in a datastore like a

    relational or nosql database. Practical Example  
  39. Mapper! RelationalMapper! NoSQLMapper! + save($data) [final]! # getPrimaryKey(array $data)! #

    insert(array $data)! # update($pk, array $data)! # getPrimaryKey(array $data)! # insert(array $data)! # update($pk, array $data)! # getPrimaryKey(array $data)! # insert(array $data)! # update($pk, array $data)!
  40. abstract class Mapper { /** * Inserts or updates a

    record in a datastore. * * @param array $data The data to insert or update * @return Boolean */ final public function save(array $data) { if ($pk = $this->getPrimaryKey($data)) { return $this->update($pk, $data); } return $this->insert($data); } abstract protected function getPrimaryKey(array $data); abstract protected function update($pk, array $data); abstract protected function insert(array $data); }
  41. class ArticleMapper extends Mapper { private $dbh; public function __construct(PDO

    $dbh) { $this->dbh = $dbh; } protected function getPrimaryKey(array $data) { return isset($data['id']) ? $data['id'] : null; } }
  42. class ArticleMapper extends Mapper { protected function insert(array $data) {

    $stmt = $this->dbh->prepare('INSERT INTO ... '); $stmt->bindValue(':title', $data['title'], PDO::PARAM_STR); $stmt->bindValue(':body', $data['body'], PDO::PARAM_STR); $stmt->execute(); return 1 === (int) $stmt->rowCount(); } protected function update($pk, array $data) { $stmt = $this->dbh->prepare('UPDATE ... WHERE id = :id '); $stmt->bindValue(':title', $data['title'], PDO::PARAM_STR); $stmt->bindValue(':body', $data['body'], PDO::PARAM_STR); $stmt->bindValue(':id', $pk, PDO::PARAM_INT); $stmt->execute(); return 1 === (int) $stmt->rowCount(); } }
  43. class LogMapper extends Mapper { private $mongo; public function __construct(MongoClient

    $mongo) { $this->mongo = $mongo; } protected function getPrimaryKey(array $data) { return isset($data['_id']) ? $data['_id'] : null; } }
  44. class LogMapper extends Mapper { protected function insert(array $data) {

    $data = $this->mongo->log->insert($data); return !empty($data['_id']); } protected function update($pk, array $data) { return $this->mongo->log->update($data); } }
  45. Strategy http://redmtncom.com/uploads/chess_pieces_photo.jpg

  46. The Strategy pattern encapsulates algorithms of the same nature into

    dedicated classes to make them interchangeable. Goal  
  47. Sending emails with several transport layers like SMTP or the

    PHP mail function. Practical Example  
  48. class Mailer { public function send(Message $message) { $to =

    $message->getRecipients(); $subject = $message->getSubject(); $body = $message->getBody(); $headers = $message->getHeaders(); if (!mail($to, $subject, $body, $headers)) { throw new RuntimeException('...'); } } }
  49. What if we want to use an SMTP transport to

    send the email? Practical Example  
  50. class Mailer { private $transport; public function __construct($transport) { $this->transport

    = $transport; } public function send(Message $message) { $to = $message->getRecipients(); // ... if ('smtp' === $this->transport) { // ... use SMTP transport } else { // ... use mail function } } }
  51. interface MailTransportInterface { function send(Message $message); } Clean strategy interface

  52. class MailTransport implements MailTransportInterface { public function send(Message $message) {

    $to = $message->getRecipients(); $subject = $message->getSubject(); $body = $message->getBody(); $headers = $message->getHeaders(); if (!mail($to, $subject, $body, $headers)) { throw new MailTransportException('...'); } } }
  53. class SmtpTransport implements MailTransportInterface { public function send(Message $message) {

    // ... use an SMTP server } } class NullTransport implements MailTransportInterface { public function send(Message $message) { // do nothing } }
  54. class Mailer { private $transport; public function __construct(MailTransportInterface $transport) {

    $this->transport = $transport; } public function send(Message $message) { try { $this->transport->send($message); } catch (MailTransportException $e) { // ... handle error } } } Abstraction
  55. $transport = new MailTransport(); $transport = new NullTransport(); $transport =

    new SendmailTransport(); $transport = new SmtpTransport('localhost', 'user'); $message = new Message(); $message->setTo('you@example.com'); $message->setFrom('me@example.com'); $message->setSubject('Example'); $message->setBody('Some body'); $mailer = new Mailer($transport); $mailer->send($message);
  56. Decorator http://soyou.wordpress.com/tag/poupees-russes/

  57. Adding responsibilities to objects without subclassing their classes. Goal  

  58. Adding new responsabilities to a database connection object. Practical Example

     
  59. class Connection implements ConnectionInterface { private $dbh; private $logger; private

    $queryCount; public function __construct(PDO $dbh, Logger $logger = null) { $this->logger = $logger; $this->dbh = $dbh; } public function getQueryCount() { return $this->queryCount; } }
  60. class Connection implements ConnectionInterface { // ... public function query($query)

    { if (null !== $this->logger) { $this->logger->log('Query: '.$query); $this->queryCount++; } return $this->dbh->query($query); } }
  61. Testing if the logger is set in production will add

    a small overhead for each executed SQL query.
  62. What if we want to extend the Connection class without

    introducing a dependency with the logger?
  63. Connection! ConnectionDecorator! MasterSlaveConnection! ProfiledConnection! query($query)! query($query)! query($query)! connection! query($query)!

  64. abstract class ConnectionDecorator implements ConnectionInterface { protected $connection; public function

    __construct(ConnectionInterface $connection) { $this->connection = $connection; } public function query($query) { return $this->connection->query($query); } }
  65. class ProfiledConnection extends ConnectionDecorator { private $logger; private $queryCount; function

    __construct(Connection $conn, Logger $logger) { parent::__construct($conn); $this->logger = $logger; $this->queryCount = 0; } public function getQueryCount() { return $this->queryCount; } }
  66. class ProfiledConnection extends ConnectionDecorator { public function query($query) { $this->logger->log('Query:

    '.$query); $this->queryCount++; return parent::query($query); } }
  67. $logger = new Logger(__DIR__.'/logs/demo.log'); $dbh = new PDO('mysql:...', 'root'); $conn

    = new Connection($dbh); $conn = new ProfiledConnection($conn, $logger); $conn->query("SET NAMES 'UTF8'"); $conn->query('SELECT * FROM users'); echo 'Queries count:'; echo $conn->getQueryCount(); // 2
  68. Pros and cons   + Easy way to extend an

    object capabilities + No need to change the underlying code - Object construction becomes more complex - Difficulty to test the concrete object type
  69. Composite http://www.flickr.com/photos/xtinalamb/59989462

  70. Goal   Composite lets clients treat individual objects and compositions

    of objects uniformly.
  71. Representing an HTML form as a list of Field objects.

    Practical Example  
  72. input[type=text] textarea input[type=text] input[type=file] Form Form

  73. A Form is a collection of Field. A Field can

    also be a Form when it embeds several fields.
  74. Field! setData($data)! Input! setData($name)! Form! setData($name)! add($name, Field $field)! remove($name)!

    getFields()!
  75. $form = new Form('product'); $form->add('name', new Input('text')); $form->add('description', new Textarea());

    $picture = new Form(); $picture->add('caption', new Input('text')); $picture->add('image', new Input('file')); $form->add('photo', $picture); $data = array( 'name' => 'Apple Macbook Air 11', 'description' => 'The finest laptop', 'photo' => array( 'caption' => 'The new Macbook Air.', ), ); $form->setData($data);
  76. abstract class Field { protected $name; protected $data; public function

    setName($name) { $this->name = $name; } public function setData($data) { $this->data = $data; } public function getData() { return $this->data; } }
  77. class Input extends Field { private $type; public function __construct($type)

    { $this->type = $type; } public function setData($data) { if ('file' !== $this->type) { parent::setData($data); } } }
  78. class Form extends Field { private $fields = array(); public

    function add($name, Field $field) { $this->fields[$name] = $field; $field->setName($name); } }
  79. class Form extends Field { public function setData($data) { foreach

    ($this->fields as $name => $field) { if (isset($data[$name])) { $field->setData($data[$name]); } } } public function getData() { $data = array(); foreach ($this->fields as $name => $field) { $data[$name] = $field->getData(); } return $data; } }
  80. Factory Method http://www.flickr.com/photos/jerryms/6704324441

  81. Goal   Define an interface for creating an object, but

    let subclasses decide which class to instantiate.
  82. Creating several kind of Document objects with a factory (images,

    pdf, text…) Practical Example  
  83. abstract class DocumentFactory { static public function getDocument($type) { switch

    ($type) { case "image": return new Image(); case "pdf": return new Pdf(); case "video": return new Video(); default: throw new InvalidArgumentException(); } } }
  84. Goal   Factory Method lets a class defer instantiation to

    subclasses.
  85. DocumentFactory + createDocument($name) + newDocument($name, $content) Document Image ImageFileFactory +

    createDocument($name)
  86. abstract class DocumentFactory { protected $directory; public function __construct($directory) {

    $this->directory = $directory; } abstract public function createDocument($name); public function newDocument($name, $content) { $document = $this->createDocument($name); $document->setDirectory($this->directory); $document->setContent($content); $document->save(); return $document; } }
  87. class TextFileFactory extends DocumentFactory { public function createDocument($name) { return

    new Document($name); } }
  88. class ImageFileFactory extends DocumentFactory { public function createDocument($name) { $image

    = new Image($name); $image->setWidth(90); $image->setHeight(120); return $image; } }
  89. $binary = $graphic->getBinaryContent(); $factory = new ImageFileFactory(__DIR__.'/images'); $image = $factory->newDocument('lolcat.jpg',

    $binary); $content = 'README CAREFULLY'; $factory = new TextFileFactory(__DIR__.'/files'); $file = $factory->newDocument('README.txt', $content);
  90. Observer

  91. Goal   A subject, the observable, emits a signal to

    a list of modules known as observers.
  92. Decoupling the dependencies of a domain object. Practical Example  

  93. 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); } }
  94. None
  95. interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface {

    function attach(ObserverInterface $observer); function notify(); }
  96. class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface

    $subject) { $reference = $subject->getReference(); $this->logger->log('New order #'. $reference); } }
  97. 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); } }
  98. 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); } }
  99. 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); } } }
  100. class Order implements ObservableInterface { public function confirm() { $this->status

    = 'confirmed'; $this->save(); $this->notifyObservers(); } }
  101. $order = new Order(); $order->attach(new LoggerNotifier($logger)); $order->attach(new CustomerNotifier($mailer)); $order->attach(new SalesNotifier($mailer));

    $order->customer = $customer; $order->amount = 150; $order->confirm();
  102. SplObserver SplSubject What’s next?  

  103. Conclusion…  

  104. None