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 full-size slide

  2. What are design
    patterns?

    View full-size slide

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

    View full-size slide

  4. Patterns categories
    Creation
    Structure
    Behavior

    View full-size slide

  5. Want to learn?

    View full-size slide

  6. Disclaimer
    Patterns are not the
    holly grail!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

  14. 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 full-size slide

  15. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  23. $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 full-size slide

  24. $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 full-size slide

  25. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. // 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. 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 full-size slide

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

    View full-size slide

  47. 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 full-size slide

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

    View full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

  52. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

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

    View full-size slide

  64. $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 full-size slide

  65. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  72. $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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

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

    View full-size slide

  76. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  80. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  83. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  86. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

  89. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  92. 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 full-size slide

  93. 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 full-size slide

  94. 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 full-size slide

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

    View full-size slide

  96. $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 full-size slide

  97. SplObserver
    SplSubject
    What’s next?  

    View full-size slide

  98. Conclusion…  

    View full-size slide