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

Introduction to Design Patterns with PHP

Hugo Hamon
November 19, 2013

Introduction to Design Patterns with PHP

Observer, decorator, factory method, composite… All these 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 practical code samples, this talk will focus on the most useful patterns in order to help you design better PHP applications.

Hugo Hamon

November 19, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Introduction to design
    patterns with PHP

    View full-size slide

  2. Who are we?
    Hugo Hamon
    Julien Pauli

    View full-size slide

  3. Object Oriented
    Programming in
    PHP...

    View full-size slide

  4. Overview of OOP in PHP
    Objects and classes
    Abstract and final classes
    Interfaces
    Abstract & final
    Magic methods
    Garbage collector
    Object cloning
    Single inheritance
    SPL and more…
    Encapsulation
    Overloading (a bit...)
    Classes autoloading
    Type hinting
    Reflection
    Namespaces
    Traits

    View full-size slide

  5. Object Oriented
    Design Best
    Practices

    View full-size slide

  6. SOLID Principle
    Single Responsibility Principle
    Open Close Principle
    Liskov Substitution Principle
    Interface Segregation Principle
    Dependency Injection Principle

    View full-size slide

  7. Single Responsability (SRP)
    « A class should have one,
    and only one, reason to
    change. »
    Keep your classes small and
    focused on small responsabilities.

    View full-size slide

  8. class CsvDataImporter
    {
    public function import($file)
    {
    $records = array();
    if (false !== $handle = fopen($file, 'r')) {
    while ($record = fgetcsv($handle)) {
    $records[] = $record;
    }
    }
    fclose($handle);
    try {
    $this->db->beginTransaction();
    foreach ($records as $record) {
    $stmt = $this->db->prepare('INSERT INTO ...');
    $stmt->execute($record);
    }
    $this->db->commit();
    } catch (PDOException $e) {
    $this->db->rollback();
    throw $e;
    }
    }
    }

    View full-size slide

  9. class DataImporter
    {
    private $loader;
    private $table;
    function __construct(FileLoader $loader, TableGateway $table)
    {
    $this->loader = $loader;
    $this->table = $table;
    }
    public function import($file)
    {
    $records = $this->loader->load($file);
    return $this->table->insert($records);
    }
    }

    View full-size slide

  10. Open Close (OCP)
    Software entities should be open to
    extension and close to
    modifications.
    « You should be able to
    extend a classe’s behavior,
    without modifying it. »

    View full-size slide

  11. Open Close
    $loader = new CsvFileLoader();
    $loader = new XmlFileLoader();
    $loader = new JsonFileLoader();
    $importer = new DataImporter($loader, ...);
    $importer->import('foo.csv|xml|json');
    Using a different file loader is now very
    easy without changing the importer class.

    View full-size slide

  12. Liskov Substitution
    A dependency of type T should be
    replaced by another subtype S of T
    without breaking the code.
    « Derived classes must be
    substitutable for their base
    classes. »

    View full-size slide

  13. Liskov Substitution
    class TextFileLoader implements FileLoader
    {
    public function load($file)
    {
    // ...
    return [ ... ];
    }
    }
    class CsvFileLoader extends TextFileLoader
    {
    public function load($file)
    {
    // ...
    return [ ... ];
    }
    }

    View full-size slide

  14. Interface Segregation (ISP)
    Interface segregation split larger
    interfaces into smaller ones so that
    clients will only know about the
    methods they need to work with.
    « Make fine grained
    interfaces that are client
    specific. »

    View full-size slide

  15. Interface Segregation
    interface FileLoader
    {
    public function load($file);
    public function supports($type);
    }
    interface TableGateway
    {
    public function insert(array $records);
    }

    View full-size slide

  16. Interface Segregation
    class DataImporter
    {
    public function __construct(FileLoader $loader, TableGateway $table)
    {
    // ...
    }
    public function import($file)
    {
    $ext = pathinfo($file, PATHINFO_EXTENSION);
    if (!$this->loader->supports($ext)) {
    throw new UnsupportedDataFile();
    }
    $records = $this->loader->load($file);
    return $this->table->insert($records);
    }
    }

    View full-size slide

  17. Dependency Injection (DIP)
    D.I is where components are given their
    dependencies through their constructors,
    methods, or directly into fields.
    « Depend on abstractions,
    not on concretions. »

    View full-size slide

  18. DI – The Wrong Way
    class DataImporter
    {
    private $loader;
    private $table;
    public function __construct()
    {
    $this->loader = new CsvFileLoader();
    $this->table = new TableGateway();
    }
    }

    View full-size slide

  19. DI – The Right Way
    class DataImporter
    {
    private $loader;
    private $table;
    function __construct(FileLoader $loader, TableGateway $table)
    {
    $this->loader = $loader;
    $this->table = $table;
    }
    }

    View full-size slide

  20. DI – Injecting dependencies
    $importer = new DataImporter(
    new CsvFileLoader(),
    new TableGateway('data', new PDO(…))
    );
    $importer->import('foo1.csv');
    $importer->import('foo2.csv');
    $importer->import('foo3.csv');

    View full-size slide

  21. Discovering Design
    Patterns

    View full-size slide

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

    View full-size slide

  23. Communication

    View full-size slide

  24. Unit testability

    View full-size slide

  25. Design patterns families
    Abstract Factory
    Builder
    Factory Method
    Lazy Initialization
    Prototype
    Singleton
    Adapter
    Bridge
    Composite
    Decorator
    Facade
    Flyweight
    Proxy
    Chain of Responsability
    Command
    Interpreter
    Iterator
    Mediator
    Memento
    Observer
    State
    Strategy
    Template Method
    Visitor
    Creational Structural Behavioral

    View full-size slide

  26. Design Patterns in
    Practice with PHP

    View full-size slide

  27. Disclaimer
    Patterns are not the holy grail!

    View full-size slide

  28. The Context
    A web store wants to sell hard and
    digital products.
    Shipment fees are calculated based
    on the product caracteristics
    (volume, mass, type…)
    Products are shipped to customers.

    View full-size slide

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

    View full-size slide

  30. Composite
    Composite lets clients treat
    individual objects and
    compositions of objects
    uniformly.

    View full-size slide

  31. Common implementations
    2. Treating XML nodes.
    1. Representing categories trees.
    3. Dealing with embedded forms.

    View full-size slide

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

    View full-size slide

  33. Concrete Example
    My web store must be able to sell
    single items or grouped items.

    View full-size slide

  34. Product!
    getPrice()!
    HardProduct!
    getPrice()!
    Bundle!
    add(Product $product)!
    getPrice()!
    getMass()!
    getVolume()!

    View full-size slide

  35. abstract class Product
    {
    protected $name;
    protected $price;
    public function __construct($name, Money $price)
    {
    $this->name = $name;
    $this->price = $price;
    }
    abstract public function getMass();
    abstract public function getVolume();
    // ... a bunch of getters
    }

    View full-size slide

  36. class HardProduct extends Product
    {
    // ...
    private $mass;
    private $volume;
    public function setPhysics(Mass $mass, Volume $volume)
    {
    $this->mass = $mass;
    $this->volume = $volume;
    }
    public function getMass()
    {
    return $this->mass;
    }
    public function getVolume()
    {
    return $this->volume;
    }
    }

    View full-size slide

  37. class DigitalProduct extends Product
    {
    public function getMass()
    {
    return new Mass(0);
    }
    public function getVolume()
    {
    return new Volume(0);
    }
    }

    View full-size slide

  38. class Bundle extends Product
    {
    protected $products;
    public function __construct($name)
    {
    $this->name = $name;
    }
    public function setPrice(Money $price)
    {
    $this->price = $price;
    }
    public function add(ProductInterface $product)
    {
    $this->products[] = $product;
    }
    }

    View full-size slide

  39. class Bundle extends Product
    {
    public function getMass()
    {
    $mass = new Mass(0);
    foreach ($this->products as $product) {
    $mass = $mass->add($product->getMass());
    }
    return $mass;
    }
    public function getVolume()
    {
    $volume = new Volume(0);
    foreach ($this->products as $product) {
    $volume = $volume->add($product->getVolume());
    }
    return $volume;
    }
    }

    View full-size slide

  40. class Bundle extends Product
    {
    public function getPrice()
    {
    if ($this->price) {
    return $this->price;
    }
    $price = $this->products[0]->getPrice();
    for ($i = 1; $i < count($this->products); $i++) {
    $product = $this->products[$i];
    $price = $price->add($product->getPrice());
    }
    return $price;
    }
    }

    View full-size slide

  41. $b1 = new Bundle('C');
    $b1->setPrice(new Money(39));
    $b1->addProduct(new HardProduct('A', new Money(29)));
    $b1->addProduct(new DigitalProduct('B', new Money(20)));
    echo $b1->getPrice(); // 39 EUR
    $b2 = new Bundle('D');
    $b2->addProduct(new HardProduct('C', new Money(12)));
    echo $b2->getPrice(); // 12 EUR
    Usage: creating products

    View full-size slide

  42. $b1 = new Bundle('C');
    $b1->setPrice(new Money(39));
    $b1->addProduct(new HardProduct('A', new Money(29)));
    $b1->addProduct(new DigitalProduct('B', new Money(20)));
    $b2 = new Bundle('D');
    $b2->addProduct(new HardProduct('C', new Money(12)));
    $b3 = new Bundle('E');
    $b3->addProduct($b1);
    $b3->addProduct($b2);
    echo $b3->getPrice(); // 39 + 12 = 51 EUR
    Usage: embedding bundles

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. Common implementations
    1.  Adding some caching capability to a
    web service client to avoid network
    calls if not needed.
    2.  Adding some logging features to an
    object in a dev environment. For
    instance, a database connection.

    View full-size slide

  46. Concrete Example
    We want to offer discounts on
    orders without breaking existing
    domain model.
    Discount strategies may vary
    depending on the context.

    View full-size slide

  47. OrderInterface!
    Order!
    BasicRateDiscount!
    getAmount()!
    getAmount()!
    order!
    getAmount()!
    OrderDecorator!

    View full-size slide

  48. class Order implements OrderInterface
    {
    private $products = array();
    public function addProduct(Product $product)
    {
    $this->products[] = $product;
    }
    public function getAmount()
    {
    $total = new Money(0);
    foreach ($this->products as $product) {
    $total = $total->add($product->getPrice());
    }
    return $total;
    }
    }

    View full-size slide

  49. abstract class OrderDecorator implements OrderInterface
    {
    protected $order;
    public function __construct(OrderInterface $order)
    {
    $this->order = $order;
    }
    public function addProduct(Product $product)
    {
    return $this->order->addProduct($product);
    }
    }

    View full-size slide

  50. class BasicValueDiscount extends OrderDecorator
    {
    private $discount;
    function __construct(OrderInterface $order, Money $discount)
    {
    parent::__construct($order);
    $this->discount = $discount;
    }
    function getAmount()
    {
    $amount = $this->order->getAmount();
    return $amount->subtract($this->discount);
    }
    }

    View full-size slide

  51. $order = new Order(...);
    $order->addProduct(new DigitalProduct('A', new Money(15)));
    $order->addProduct(new DigitalProduct('B', new Money(10)));
    // Substract 5 euros to the total
    $order1 = new BasicValueDiscount($order, new Money(5));
    $order1->getAmount(); // 20 EUR
    // Substract 50% of the total
    $order2 = new BasicRateDiscount($order, 50);
    $order2->getAmount(); // 12.50 EUR
    // Combine discounts together
    $order3 = new BasicRateDiscount(
    new BasicValueDiscount($order, new Money(5)),
    50
    );
    $order3->getAmount(); // 10 EUR
    Usage

    View full-size slide

  52. Other discount strategies…
    + Minimum purchase amount required
    + Minimum ordered items required
    + Loyal clients only
    + Special first order discount
    + …

    View full-size slide

  53. 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

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

    View full-size slide

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

    View full-size slide

  56. Common implementations
    1.  Switching to another storage engine
    to store session data.
    2.  Comparing and sorting elements of
    an array.
    3.  Sending emails with SMTP,
    Sendmail or the mail() function.

    View full-size slide

  57. class Swift_Mailer
    {
    private $transport;
    public function __construct(Swift_Transport $transport)
    {
    $this->transport = $transport;
    }
    public function send(Swift_Message $message)
    {
    // do stuff…
    $this->transport->send($message);
    }
    }
    Swiftmailer example

    View full-size slide

  58. class Swift_MailTransport extends Swift_Transport
    {
    public function send(Swift_Message $message)
    {
    // use php mail() function to send a message
    }
    }
    class Swift_SmtpTransport extends Swift_Transport
    {
    public function send(Swift_Message $message)
    {
    // use an SMTP connection to send a message
    }
    }
    class Swift_NullTransport extends Swift_Transport
    {
    public function send(Swift_Message $message)
    {
    // fake email sending
    }
    }

    View full-size slide

  59. Concrete example
    We want to calculate the order
    shipment fees depending on the
    chosen delivery strategy.
    The customer can choose between 3
    delivery strategies.

    View full-size slide

  60. Concrete example (wrong!!!)
    class OrderService
    {
    function priceShippingCost(OrderInterface $order, $type)
    {
    switch ($type) {
    case 'warehouse-pickup':
    return new Money(0);
    case 'regular':
    return new Money(7);
    case 'express':
    return new Money(10);
    }
    }
    }

    View full-size slide

  61. Encapsulating algorithms
    interface ShippingStrategy
    {
    function calculateShippingCost(OrderInterface $order);
    }
    class WarehousePickup implements ShippingStrategy
    {
    function priceShippingCost(OrderInterface $order)
    {
    return new Money(0);
    }
    }

    View full-size slide

  62. Encapsulating algorithms
    class RegularShipping implements ShippingStrategy
    {
    function priceShippingCost(OrderInterface $order)
    {
    if ($order->getAmount() > 100) {
    return new Money(0);
    }
    if (count($order->getProduct()) > 3) {
    return new Money(12);
    }
    return new Money(7);
    }
    }

    View full-size slide

  63. Encapsulating algorithms
    class ExpressShipping implements ShippingStrategy
    {
    function priceShippingCost(OrderInterface $order)
    {
    if ($order->getWeight() < 5) {
    return new Money(6);
    }
    if ($order->getWeight() >= 5 && $order->getWeight() < 10) {
    return new Money(9);
    }
    return new Money(15);
    }
    }

    View full-size slide

  64. class OrderService
    {
    public function priceShippingCost(
    OrderInterface $order,
    ShippingStrategy $shipping
    )
    {
    // ... do stuff
    $fare = $shipping->priceShippingCost($order);
    $order->getAmount()->add($fare);
    // ... do stuff
    }
    }
    Strategy can be given as an
    argument of the client’s method.

    View full-size slide

  65. class OrderService
    {
    private $shippingStrategy;
    public function __construct(ShippingStrategy $shipping)
    {
    $this->shippingStrategy = $shipping;
    }
    public function priceShippingCost(OrderInterface $order, $delivery)
    {
    // ... do stuff
    $fare = $this->shippingStrategy->priceShippingCost($order);
    $order->getAmount()->add($fare);
    // ... do stuff
    }
    }
    Or it can be injected in the
    constructor method of the client.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. Factory Method lets
    a class defer
    instantiation to
    subclasses.

    View full-size slide

  69. Concrete example
    We want to encapsulate the way we
    create hard products, digital
    products and bundles.
    The goal is to keep instantiation
    processes in one single place.

    View full-size slide

  70. ProductFactory
    + createProduct($name)
    + newProduct($name, $price)
    Product
    HardProduct
    HardProductFactory
    + createProduct($name)

    View full-size slide

  71. abstract class ProductFactory
    {
    public function __construct(ReferenceGenerator $generator)
    {
    $this->refGenerator = $generator;
    }
    abstract protected function createProduct($name, $price, $mass = null, $volume = null);
    public function newProduct($name, $price, $mass = null, $volume = null)
    {
    $price = new Price($price, new Currency('EUR'));
    $product = $this->createProduct($name, $price, $mass, $volume);
    $product->setReference($this->refGenerator->generate($product));
    return $product;
    }
    }
    Usage

    View full-size slide

  72. class HardProductFactory extends ProductFactory
    {
    protected function createProduct(
    $name,
    Price $price,
    Mass $mass = null,
    Volume $volume = null
    )
    {
    $product = new HardProduct($name, $price);
    $product->setPhysics($mass, $volume);
    return $product;
    }
    }

    View full-size slide

  73. class DigitalProductFactory extends ProductFactory
    {
    protected function createProduct(
    $name,
    Price $price,
    Mass $mass = null,
    Volume $volume = null
    )
    {
    return new DigitalProduct($name, $price);
    }
    }

    View full-size slide

  74. class BundleFactory extends ProductFactory
    {
    protected function createProduct(
    $name,
    Price $price,
    Mass $mass = null,
    Volume $volume = null
    )
    {
    return new Bundle($name, $price);
    }
    }

    View full-size slide

  75. $generator = new ReferenceGenerator();
    $factory = new HardProductFactory($generator);
    $product = $factory->newProduct('A', 50, 4, 10);
    $factory = new DigitalProductFactory($generator);
    $product = $factory->newProduct('B', 25);
    $factory = new BundleFactory($generator);
    $bundle = $factory->newProduct('C', 99);
    Usage

    View full-size slide

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

    View full-size slide

  77. class Customer
    {
    public function pay(Order $order, Money $amount)
    {
    $order->setPaid();
    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

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

    View full-size slide

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

    View full-size slide

  80. class CustomerNotifier implements ObserverInterface
    {
    public $mailer;
    public function notify(ObservableInterface $subject)
    {
    $mail = new Email();
    $mail->recipient = $subject->getEmail();
    $mail->subject = 'Your order!';
    $mail->message = 'Thanks for ordering...';
    $this->mailer->send($mail);
    }
    }

    View full-size slide

  81. 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

  82. class Customer 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

  83. class Customer implements ObservableInterface
    {
    public function pay(Order $order)
    {
    $order->setPaid();
    $this->notifyObservers();
    }
    }

    View full-size slide

  84. $client = new Customer();
    $client->attach(new LoggerNotifier($logger));
    $client->attach(new CustomerNotifier($mailer));
    $client->attach(new SalesNotifier($mailer));
    $order = new Order();
    $client->pay($order, new Money(150));

    View full-size slide

  85. Conclusion…

    View full-size slide

  86. Good readings

    View full-size slide