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 Slide

  2. Who are we?
    Hugo Hamon
    Julien Pauli

    View Slide

  3. Object Oriented
    Programming in
    PHP...

    View 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 Slide

  5. Object Oriented
    Design Best
    Practices

    View Slide

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

    View 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 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 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 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 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 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 Slide

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

    View 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 Slide

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

    View 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 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 Slide

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

    View 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 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 Slide

  21. Discovering Design
    Patterns

    View Slide

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

    View Slide

  23. Communication

    View Slide

  24. Unit testability

    View 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 Slide

  26. Design Patterns in
    Practice with PHP

    View Slide

  27. Disclaimer
    Patterns are not the holy grail!

    View 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 Slide

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

    View Slide

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

    View Slide

  31. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

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

  41. 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 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)));
    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 Slide

  43. $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 Slide

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

    View Slide

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

    View Slide

  46. View Slide

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

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

    View Slide

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

    View Slide

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

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

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

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

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

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

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

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

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

    View Slide

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

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

  71. View Slide

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

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

    View Slide

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

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

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

    View Slide

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

    View Slide

  78. $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 Slide

  79. Observer

    View Slide

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

    View Slide

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

  82. View Slide

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

    View Slide

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

    View Slide

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

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

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

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

    View Slide

  89. $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 Slide

  90. Conclusion…

    View Slide

  91. Good readings

    View Slide

  92. View Slide