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. 2.
  2. 4.

    Definition « In software design, a design pattern is an

    abstract generic solution to solve a particular redundant problem. »
  3. 8.

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

    Strategy o  Decorator o  Composite o  Factory Method o  Observer
  4. 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(); }
  5. 14.

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

    logic here] } public function getEntries() { [complex xml parsing logic here] } }
  6. 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; }
  7. 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']; } }
  8. 17.
  9. 20.

    A weather client object relies on the famous WahooWeather web

    service to collect daily forecasts for a city.
  10. 21.

    class WeatherService { private $api; public function __construct(WahooWeatherApi $api) {

    $this->api = $api; } public function getForecasts($city, $date) { return $this->api->getCityForecasts($city, $date); } }
  11. 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
  12. 23.

    But a new competitor, BoogleWeather, now comes with a much

    more powerful API to get daily forecasts for a city.
  13. 24.

    We want the weather service Client supports both weather providers

    although they don’t share the same API at all…
  14. 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', );
  15. 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” }';
  16. 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
  17. 28.

    Designing an Adapter helps to provide a single public API

    to support the two heterogeneous Adaptees.
  18. 29.
  19. 30.

    abstract class WeatherProviderAdapter { protected $date; public function setDate($date) {

    $this->date = new DateTime($date); } abstract public function getForecasts($city); }
  20. 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); } }
  21. 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); } }
  22. 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); } }
  23. 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
  24. 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');
  25. 38.

    Inserting or updating a record in a datastore like a

    relational or nosql database. Practical Example  
  26. 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)!
  27. 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); }
  28. 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; } }
  29. 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(); } }
  30. 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; } }
  31. 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); } }
  32. 46.

    The Strategy pattern encapsulates algorithms of the same nature into

    dedicated classes to make them interchangeable. Goal  
  33. 47.

    Sending emails with several transport layers like SMTP or the

    PHP mail function. Practical Example  
  34. 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('...'); } } }
  35. 49.

    What if we want to use an SMTP transport to

    send the email? Practical Example  
  36. 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 } } }
  37. 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('...'); } } }
  38. 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 } }
  39. 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
  40. 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);
  41. 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; } }
  42. 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); } }
  43. 61.

    Testing if the logger is set in production will add

    a small overhead for each executed SQL query.
  44. 62.

    What if we want to extend the Connection class without

    introducing a dependency with the logger?
  45. 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); } }
  46. 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; } }
  47. 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
  48. 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
  49. 73.

    A Form is a collection of Field. A Field can

    also be a Form when it embeds several fields.
  50. 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);
  51. 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; } }
  52. 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); } } }
  53. 78.

    class Form extends Field { private $fields = array(); public

    function add($name, Field $field) { $this->fields[$name] = $field; $field->setName($name); } }
  54. 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; } }
  55. 81.

    Goal   Define an interface for creating an object, but

    let subclasses decide which class to instantiate.
  56. 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(); } } }
  57. 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; } }
  58. 88.

    class ImageFileFactory extends DocumentFactory { public function createDocument($name) { $image

    = new Image($name); $image->setWidth(90); $image->setHeight(120); return $image; } }
  59. 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);
  60. 90.
  61. 91.

    Goal   A subject, the observable, emits a signal to

    a list of modules known as observers.
  62. 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); } }
  63. 94.
  64. 96.

    class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface

    $subject) { $reference = $subject->getReference(); $this->logger->log('New order #'. $reference); } }
  65. 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); } }
  66. 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); } }
  67. 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); } } }
  68. 100.

    class Order implements ObservableInterface { public function confirm() { $this->status

    = 'confirmed'; $this->save(); $this->notifyObservers(); } }
  69. 104.