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

Practical Design PHPatterns

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.

Hugo Hamon

January 26, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Practical Design PHPatterns
    Hugo Hamon – PHPBenelux 2013 - Antwerp
    http://wwp.greenwichmeantime.com

    View Slide

  2. View Slide

  3. What are design
    patterns?

    View Slide

  4. Definition
    « In software design, a
    design pattern is an
    abstract generic solution
    to solve a particular
    redundant problem. »

    View Slide

  5. Patterns categories
    Creation
    Structure
    Behavior

    View Slide

  6. Want to learn?

    View Slide

  7. Disclaimer
    Patterns are not the
    holly grail!

    View Slide

  8. Today’s patterns
    o  Facade
    o  Adapter
    o  Template Method
    o  Strategy
    o  Decorator
    o  Composite
    o  Factory Method
    o  Observer

    View Slide

  9. Facade
    http://www.flickr.com/photos/br1dotcom/5930994659

    View Slide

  10. Wrapping a complicated
    system in order to
    provide a simplier
    interface to use it.
    Goal  

    View Slide

  11. Parsing an Atom XML
    feed to extract some
    information.
    Practical Example  

    View Slide

  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 node
    $author = ...;
    $entry['author'] = $author;
    }
    $entries[] = $entry;
    $entry = array();
    }

    View Slide

  13. Encapsulating the
    complex xml
    parsing logic into a
    facade object.
    Solution  

    View Slide

  14. class FeedReader
    {
    public function getTitle()
    {
    [complex xml parsing logic here]
    }
    public function getEntries()
    {
    [complex xml parsing logic here]
    }
    }

    View Slide

  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;
    }

    View Slide

  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'];
    }
    }

    View Slide

  17. Adapter

    View Slide

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

    View Slide

  19. Fetching weather
    forecasts with two
    different weather
    providers, which don’t
    have the same API.
    Practical Example  

    View Slide

  20. A weather client object
    relies on the famous
    WahooWeather web
    service to collect daily
    forecasts for a city.

    View Slide

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

    View Slide

  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

    View Slide

  23. But a new competitor,
    BoogleWeather, now
    comes with a much
    more powerful API to
    get daily forecasts for a
    city.

    View Slide

  24. We want the weather
    service Client supports
    both weather
    providers although
    they don’t share the
    same API at all…

    View Slide

  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',
    );

    View Slide

  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”
    }';

    View Slide

  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

    View Slide

  28. Designing an Adapter
    helps to provide a
    single public API to
    support the two
    heterogeneous
    Adaptees.

    View Slide

  29. View Slide

  30. abstract class WeatherProviderAdapter
    {
    protected $date;
    public function setDate($date)
    {
    $this->date = new DateTime($date);
    }
    abstract public function getForecasts($city);
    }

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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

    View Slide

  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');

    View Slide

  36. Template Method
    http://www.flickr.com/photos/jmgeorges/4849023808

    View Slide

  37. Let subclasses redefine
    certain steps of an
    algorithm without
    changing the algorithm’s
    structure.
    Goal  

    View Slide

  38. Inserting or updating a
    record in a datastore
    like a relational or nosql
    database.
    Practical Example  

    View Slide

  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)!

    View Slide

  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);
    }

    View Slide

  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;
    }
    }

    View Slide

  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();
    }
    }

    View Slide

  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;
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  45. Strategy
    http://redmtncom.com/uploads/chess_pieces_photo.jpg

    View Slide

  46. The Strategy pattern
    encapsulates algorithms
    of the same nature into
    dedicated classes to make
    them interchangeable.
    Goal  

    View Slide

  47. Sending emails with
    several transport layers
    like SMTP or the PHP
    mail function.
    Practical Example  

    View Slide

  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('...');
    }
    }
    }

    View Slide

  49. What if we want to use
    an SMTP transport to
    send the email?
    Practical Example  

    View Slide

  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
    }
    }
    }

    View Slide

  51. interface MailTransportInterface
    {
    function send(Message $message);
    }
    Clean strategy interface

    View Slide

  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('...');
    }
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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

    View Slide

  55. $transport = new MailTransport();
    $transport = new NullTransport();
    $transport = new SendmailTransport();
    $transport = new SmtpTransport('localhost', 'user');
    $message = new Message();
    $message->setTo('[email protected]');
    $message->setFrom('[email protected]');
    $message->setSubject('Example');
    $message->setBody('Some body');
    $mailer = new Mailer($transport);
    $mailer->send($message);

    View Slide

  56. Decorator
    http://soyou.wordpress.com/tag/poupees-russes/

    View Slide

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

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  61. Testing if the logger
    is set in production
    will add a small
    overhead for each
    executed SQL query.

    View Slide

  62. What if we want to
    extend the Connection
    class without
    introducing a
    dependency with the
    logger?

    View Slide

  63. Connection!
    ConnectionDecorator!
    MasterSlaveConnection!
    ProfiledConnection!
    query($query)!
    query($query)!
    query($query)!
    connection!
    query($query)!

    View Slide

  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);
    }
    }

    View Slide

  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;
    }
    }

    View Slide

  66. class ProfiledConnection extends ConnectionDecorator
    {
    public function query($query)
    {
    $this->logger->log('Query: '.$query);
    $this->queryCount++;
    return parent::query($query);
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  69. Composite
    http://www.flickr.com/photos/xtinalamb/59989462

    View Slide

  70. Goal  
    Composite lets
    clients treat
    individual objects
    and compositions of
    objects uniformly.

    View Slide

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

    View Slide

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

    View Slide

  73. A Form is a collection
    of Field.
    A Field can also be a
    Form when it embeds
    several fields.

    View Slide

  74. Field!
    setData($data)!
    Input!
    setData($name)!
    Form!
    setData($name)!
    add($name, Field $field)!
    remove($name)!
    getFields()!

    View Slide

  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);

    View Slide

  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;
    }
    }

    View Slide

  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);
    }
    }
    }

    View Slide

  78. class Form extends Field
    {
    private $fields = array();
    public function add($name, Field $field)
    {
    $this->fields[$name] = $field;
    $field->setName($name);
    }
    }

    View Slide

  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;
    }
    }

    View Slide

  80. Factory Method
    http://www.flickr.com/photos/jerryms/6704324441

    View Slide

  81. Goal  
    Define an interface for
    creating an object, but
    let subclasses decide
    which class to
    instantiate.

    View Slide

  82. Creating several kind of
    Document objects with a
    factory (images, pdf,
    text…)
    Practical Example  

    View Slide

  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();
    }
    }
    }

    View Slide

  84. Goal  
    Factory Method
    lets a class defer
    instantiation to
    subclasses.

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

  87. class TextFileFactory extends DocumentFactory
    {
    public function createDocument($name)
    {
    return new Document($name);
    }
    }

    View Slide

  88. class ImageFileFactory extends DocumentFactory
    {
    public function createDocument($name)
    {
    $image = new Image($name);
    $image->setWidth(90);
    $image->setHeight(120);
    return $image;
    }
    }

    View Slide

  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);

    View Slide

  90. Observer

    View Slide

  91. Goal  
    A subject, the
    observable, emits a
    signal to a list of
    modules known as
    observers.

    View Slide

  92. Decoupling the
    dependencies of a
    domain object.
    Practical Example  

    View Slide

  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 = '[email protected]';
    $mail->subject = 'New order to ship!';
    $mail->message = '...';
    $this->mailer->send($mail);
    }
    }

    View Slide

  94. View Slide

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

    View Slide

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

    View Slide

  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);
    }
    }

    View Slide

  98. 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 Slide

  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);
    }
    }
    }

    View Slide

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

    View Slide

  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();

    View Slide

  102. SplObserver
    SplSubject
    What’s next?  

    View Slide

  103. Conclusion…  

    View Slide

  104. View Slide