$30 off During Our Annual Pro Sale. View Details »

Practical Approach of Using Design Patterns

Hugo Hamon
February 25, 2016

Practical Approach of Using Design Patterns

Design patterns are conceptual solutions to solve common redundant problems in software engineering. However, learning them is not easy as litterature or tutorials on the Internet often introduce them with theorical examples. This talk gives you a slightly different approach by introducing design patterns with practical code samples to solve real world problems. You won't just learn design patterns, you will also discover how to leverage them!

Hugo Hamon

February 25, 2016
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Practical Approach of Using
    Design Patterns
    Confoo 2016 – Montréal - Hugo Hamon

    View Slide

  2. Hugo Hamon
    Head of training
    SensioLabs
    Book author
    Speaker at conferences
    5th time speaking at Confoo
    Symfony contributor
    Bengal cat lover
    @hhamon / @catlannister

    View Slide

  3. Introduction

    View Slide

  4. In software design, a
    design pattern is an
    abstract generic solution
    to solve a particular
    common problem.

    View Slide

  5. Patterns Families
    •  23 GoF Patterns
    •  Creational
    •  Structural
    •  Behavioral

    View Slide

  6. Pros of Design Patterns
    •  Team communication
    •  Testability
    •  Maintainance
    •  Extensibility
    •  Loose coupling

    View Slide

  7. Cons of Design Patterns
    •  Hard to learn
    •  Hard to find real world examples
    •  Hard to know when to apply
    •  Require good knowledge of OOP
    •  Not always the holly grail!

    View Slide

  8. Today’s talk focuses on
    the Factory Method,
    Decorator and
    Composite design
    patterns

    View Slide

  9. http://images.wisegeek.com/factory-with-pipes.jpg
    Factory Method

    View Slide

  10. Encapsulating the Creation
    of Objects

    Centralizing the way objects are created.

    View Slide

  11. The Factory object encapsulates the process for
    creating, assembling and initializing objects. It hides
    this whole process from the client code that just
    needs to get a new made object.
    The Need for a Factory
    Having a Factory also allows to change the process
    for creating objects at any time without impacting the
    client code.

    View Slide

  12. Real World Factories

    View Slide

  13. Why having factories?
    The car factory may want to change the steps to
    assemble a new car in order to reduce costs and generate
    more profits. In the end, the final customer still buys a
    functional car whatever ways it has been produced.
    The baker may want to slightly change the recipe of his
    bread to make more profits or improve quality. In the
    end, the customer still buys a « baguette » at the bakery.

    View Slide

  14. The Static & Simple
    Factories

    View Slide

  15. class MediaFactory
    {
    public static function createMedia($path)
    {
    $format = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if ('pdf' === $format) {
    return new PdfFile($path);
    }
    if (in_array($format, [ 'jpg', 'png', 'gif' ])) {
    return new ImageFile($path);
    }
    if ('txt' === $format) {
    return new TextFile($path);
    }
    throw new \UnexpectedValueException('Unexpected format '. $format);
    }
    }
    The Static Method Approach

    View Slide

  16. $file1 = MediaFactory::createMedia('/path/to/file.jpg'); // ImageFile
    $file2 = MediaFactory::createMedia('/path/to/file.png'); // ImageFile
    $file3 = MediaFactory::createMedia('/path/to/file.gif'); // ImageFile
    $file4 = MediaFactory::createMedia('/path/to/file.pdf'); // PdfFile
    $file5 = MediaFactory::createMedia('/path/to/file.txt'); // TextFile
    The Static Method Approach
    This snippet of code shows how different types of
    objects are created based on the given file path
    extension. The static factory method either returns
    an ImageFile, or a PdfFile or a TextFile object.

    View Slide

  17. class DocumentRepository
    {
    private $filesystem;
    private $imageManipulator;
    function __construct(Filesystem $filesystem, ImageManipulator $manipulator)
    {
    $this->filesystem = $filesystem;
    $this->imageManipulator = $nanipulator;
    }
    public function move($path, $target)
    {
    $file = MediaFactory::createMedia($path);
    if ($file instanceof ImageFile) {
    $file = $this->imageManipulator->resize($file, 120, 120);
    }
    $this->filesystem->move($file->getRealPath(), $target);
    }
    }
    The Static Method in Action

    View Slide

  18. Pros & Cons of the Static Method
    •  Tight coupling between the factory and the client,
    •  Client code is more difficult to unit test,
    •  Unable to switch to another factory,
    •  Difficulty to rely on dependencies in the factory.
    •  Very simple,
    •  Very pragmatic,
    •  Easy to understand.

    View Slide

  19. class MediaFactory implements MediaFactoryInterface
    {
    public function createMedia($path)
    {
    $format = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if ('pdf' === $format) {
    return new PdfFile($path);
    }
    if (in_array($format, [ 'jpg', 'png', 'gif' ])) {
    return new ImageFile($path);
    }
    if ('txt' === $format) {
    return new TextFile($path);
    }
    throw new \UnexpectedValueException('Unexpected format '. $format);
    }
    }
    The Simple Factory Approach

    View Slide

  20. $factory = new MediaFactory();
    $file1 = $factory->createMedia('/path/to/file.jpg'); // ImageFile
    $file2 = $factory->createMedia('/path/to/file.png'); // ImageFile
    $file3 = $factory->createMedia('/path/to/file.gif'); // ImageFile
    $file4 = $factory->createMedia('/path/to/file.pdf'); // PdfFile
    $file5 = $factory->createMedia('/path/to/file.txt'); // TextFile
    The Simple Factory Approach
    This snippet of code shows how different types of objects
    are created based on the given file path extension. The
    factory is now an instance with a method that either returns
    an ImageFile, or PdfFile or TextFile object.

    View Slide

  21. class DocumentRepository
    {
    private $mediaFactory;
    // ...
    public function __construct(
    FilesystemInterface $filesystem,
    MediaFactoryInterface $mediaFactory,
    ImageManipulatorInterface $imageManipulator
    )
    {
    $this->filesystem = $filesystem;
    $this->mediaFactory = $mediaFactory;
    $this->imageManipulator = $imageManipulator;
    }
    public function move($path, $target)
    {
    $file = $this->mediaFactory->createMedia($path);
    // ...
    }
    }
    The Simple Factory in Action

    View Slide

  22. Pros & Cons of the Simple Factory
    •  It’s still not a real factory method object.
    •  The factory produces multiple kinds of objects.
    •  Simple & pragmatic approach.
    •  Loose coupling between factory and client code.
    •  Uses dependency injection.
    •  Easy to unit test the client code.
    •  Easy to replace the actual factory by another one.

    View Slide

  23. The Real Factory
    Method Pattern

    View Slide

  24. The Factory Method
    Creational pattern

    •  Encapsulate the creation of an object
    •  Algorithm to create object is partially implemented
    •  Object creation is delegated to subclasses
    •  Client code doesn’t know the type of the created object
    •  Each factory creates one kind of object

    View Slide

  25. UML Diagram

    View Slide

  26. Media Gallery
    System
    Example

    View Slide

  27. Diagram for Media Metadata Factories

    View Slide

  28. namespace MediaGallery;
    interface MediaMetadataFactoryInterface
    {
    /**
    * Loads the metadata of a given multimedia file.
    *
    * @param string $path The multimedia file path
    *
    * @return MediaMetadataInterface $metadata
    * @throws MediaNotFoundException
    */
    public function loadMetadata($path);
    }
    The Abstract Metadata Factory

    View Slide

  29. namespace MediaGallery\Metadata;
    use MediaGallery\Analyzer\MediaAnalyzerInterface;
    use MediaGallery\MediaMetadataFactoryInterface;
    use MediaGallery\MediaMetadataInterface;
    use MediaGallery\MediaNotFoundException;
    abstract class MediaMetadataFactory implements MediaMetadataFactoryInterface
    {
    private $analyzer;
    public function __construct(MediaAnalyzerInterface $analyzer)
    {
    $this->analyzer = $analyzer;
    }
    abstract protected function createMetadata(\SplFileInfo $file);
    protected function analyze(\SplFileInfo $file)
    {
    return $this->analyzer->analyze($file);
    }
    }
    The Abstract Metadata Factory

    View Slide

  30. abstract class MediaMetadataFactory implements MediaMetadataFactoryInterface
    {
    // ...
    /** @return MediaMetada */
    public function loadMetadata($path)
    {
    if (!is_readable($path)) {
    throw new MediaNotFoundException(sprintf('%s not readable.', $path));
    }
    $file = new \SplFileInfo($path);
    $metadata = $this->createMetadata($file);
    $metadata->initialize($file);
    return $metadata;
    }
    }
    The Abstract Metadata Factory

    View Slide

  31. The Abstract MediaMetada Class
    The abstract MediaMetadata class is designed to
    encapsulate the common shared attributes by all
    media files types.
    For each media file, it stores its real path, its size
    and its creation date on the filesystem.

    View Slide

  32. namespace MediaGallery\Metadata;
    use MediaGallery\MediaMetadataInterface;
    abstract class MediaMetadata implements MediaMetadataInterface
    {
    private $realPath;
    private $size;
    private $createdAt;
    public function initialize(\SplFileInfo $file)
    {
    $this->size = $file->getSize();
    $this->realPath = $file->getRealPath();
    $this->createdAt = $file->getMTime();
    }
    // … plus one getter for each private property
    }
    The Abstract MediaMetadata Class

    View Slide

  33. The Image Metadata Factory
    The ImageMetadataFactory class is responsible for
    producing ImageMetadata objects. To analyze a picture,
    the factory uses an ImageAnalyzer instance.
    Each ImageMetadata instance has a real path, a size, a
    creation date but also a set of dimensions (width & height)
    and an orientation (portrait, landscape or square).

    View Slide

  34. namespace MediaGallery\Metadata;
    class ImageMetadata extends MediaMetadata
    {
    const SQUARE = 'square';
    const PORTRAIT = 'portrait';
    const LANDSCAPE = 'landscape';
    private $width;
    private $height;
    public function __construct($width, $height)
    {
    $this->width = (int) $width;
    $this->height = (int) $height;
    }
    // … plus getter methods
    }
    The ImageMetadata Class

    View Slide

  35. namespace MediaGallery\Analyzer;
    class ImageAnalyzer implements MediaAnalyzerInterface
    {
    public function analyze(\SplFileInfo $file)
    {
    $path = $file->getRealPath();
    if (!$path || !$metadata = @getimagesize($path)) {
    throw new AnalysisFailedException(sprintf(
    'Unable to extract image metadata for path %s.',
    $path
    ));
    }
    return [ 'width' => $metadata[0], 'height' => $metadata[1] ];
    }
    }
    The ImageAnalyzer Class

    View Slide

  36. namespace MediaGallery\Metadata;
    class ImageMetadataFactory extends MediaMetadataFactory
    {
    /**
    * Creates the specific ImageMetadata object.
    *
    * @param \SplFileInfo $file
    * @return ImageMetadata
    */
    protected function createMetadata(\SplFileInfo $file)
    {
    $infos = $this->analyze($file);
    return new ImageMetadata($infos['width'], $infos['height']);
    }
    }
    The ImageMetadaFactory Class

    View Slide

  37. use MediaGallery\Analyzer\ImageAnalyzer;
    use MediaGallery\Metadata\ImageMetadataFactory;
    $factory = new ImageMetadataFactory(new ImageAnalyzer());
    $metadata = $factory->loadMetadata('/path/to/image.png');
    echo 'Path: ', $metadata->getRealPath() ,"\n";
    echo 'Date: ', $metadata->getCreatedAt() ,"\n";
    echo 'Size: ', $metadata->getSize() ,"\n";
    echo 'Width: ', $metadata->getWidth() ," px\n";
    echo 'Height: ', $metadata->getHeight() ," px\n";
    echo 'Orientation: ', $metadata->getOrientation() ,"\n";
    Using the Image Metada Factory

    View Slide

  38. The Movie Metadata Factory
    The MovieMetadataFactory class is responsible for
    producing MovieMetadata objects. To analyze a video
    clip, the factory uses an VideoAnalyzer instance.
    Each MovieMetadata instance has a real path, a size,
    a creation date but also a set of specific attributes: a
    resolution in pixels (x & y axis), a duration, a frame rate
    and a frame count.

    View Slide

  39. namespace MediaGallery\Metadata;
    class MovieMetadata extends MediaMetadata
    {
    private $xResolution;
    private $yResolution;
    private $frameRate;
    private $frameCount;
    private $duration;
    function __construct($xResolution, $yResolution, $duration, $frameRate, $frameCount)
    {
    $this->xResolution = (int) $xResolution;
    $this->yResolution = (int) $yResolution;
    $this->frameRate = (int) round($frameRate);
    $this->frameCount = (int) $frameCount;
    $this->duration = (int) round($duration);
    }
    // ... getters for all attributes
    }
    The MovieMetadata Class

    View Slide

  40. namespace MediaGallery\Analyzer;
    class VideoAnalyzer extends AudioVideoAnalyzer
    {
    public function analyze(\SplFileInfo $file)
    {
    $metadata = $this->extractMetadata($file->getRealPath());
    $format = $metadata['video']['dataformat'];
    return [
    'resolution_x' => $metadata['video']['resolution_x'],
    'resolution_y' => $metadata['video']['resolution_y'],
    'duration' => $metadata['playtime_seconds'],
    'frame_rate' => $metadata['video']['frame_rate'],
    'frame_count' => $metadata[$format]['video']['frame_count'],
    ];
    }
    }
    The VideoAnalyzer Class

    View Slide

  41. namespace MediaGallery\Metadata;
    class MovieMetadataFactory extends MediaMetadataFactory
    {
    protected function createMetadata(\SplFileInfo $file)
    {
    // The $metadata is filled with information
    // provided by the awesome GetId3Core class
    // from the eponym library
    $metadata = $this->analyze($file);
    return new MovieMetadata(
    $metadata['resolution_x'],
    $metadata['resolution_y'],
    $metadata['duration'],
    $metadata['frame_rate'],
    $metadata['frame_count']
    );
    }
    }
    The MovieMetadataFactory Class

    View Slide

  42. use MediaGallery\Analyzer\VideoAnalyzer;
    use MediaGallery\Metadata\MovieMetadataFactory;
    $factory = new MovieMetadataFactory(new VideoAnalyzer());
    $metadata = $factory->loadMetadata('/path/to/movie.mp4');
    echo 'Path: ', $metadata->getRealPath() ,"\n";
    echo 'Date: ', $metadata->getCreatedAt() ,"\n";
    echo 'Size: ', $metadata->getSize() ,"\n";
    echo 'Resolution X: ', $metadata->getXResolution() ,' px ', "\n";
    echo 'Resolution Y: ', $metadata->getYResolution() ,' px ', "\n";
    echo 'Duration: ', $metadata->getDuration() ," seconds\n";
    echo 'Frame rate: ', $metadata->getFrameRate() ," fps\n";
    echo 'Frames: ', $metadata->getFrameCount() ," frames\n";
    Using the Movie Metadata Factory

    View Slide

  43. Composite

    View Slide

  44. Processing Single Objects
    & Collections Uniformly

    Sharing a unified interface.

    View Slide

  45. The Composite
    Structural pattern
    •  Treating single objects and collections uniformly.
    •  Sharing a unified interface.
    •  Combining objects easily.
    •  Processing recursiving operations on objects.

    View Slide

  46. UML Diagram

    View Slide

  47. Composite Usage
    $nestedComposite = new ConcreteComposite();
    $nestedComposite->add(new ConcreteLeaf());
    $nestedComposite->add(new ConcreteLeaf());
    $composite = new ConcreteComposite();
    $composite->add(new ConcreteLeaf());
    $composite->add(new ConcreteLeaf());
    $composite->add($nestedComposite);
    $composite->operation();
    $leaf = new ConcreteLeaf();
    $leaf->operation();

    View Slide

  48. Use Cases For a Composite
    •  File explorer
    •  Organization chart
    •  Family tree
    •  Content management (nested pages)
    •  XML file parsing
    •  Nested web forms
    •  Navigation bar (items including subitems)
    •  …

    View Slide

  49. Implementing the
    Composite Pattern

    View Slide

  50. Single & Combo Products
    •  The goal is to sell single products and combos.
    •  Each product can be either physical / digital or a combo.
    •  Unless physical products, digital ones don’t have a mass.
    •  The unit price belongs to each product.
    •  Combo products can have a fixed or dynamic price.
    •  Combo’s total weight is always the sum of its combined products.

    View Slide

  51. UML Diagram

    View Slide

  52. namespace Shop;
    interface ProductInterface
    {
    public function getName();
    /** @return \Sebastian\Money\Money */
    public function getPrice();
    /** @return \Physics\Metrics\Mass */
    public function getMass();
    }
    The Product Interface

    View Slide

  53. namespace Shop;
    use Physics\Metrics\Mass;
    use SebastianBergmann\Money\Money;
    abstract class Product implements ProductInterface
    {
    protected $name;
    protected $price;
    protected $mass;
    public function __construct($name, Money $price, Mass $mass)
    {
    $this->name = $name;
    $this->price = $price;
    $this->mass = $mass;
    }
    public function getPrice()
    {
    return $this->price;
    }
    }
    The Abstract Product Class

    View Slide

  54. namespace Shop;
    abstract class Product implements ProductInterface
    {
    // ...
    public function getName()
    {
    return $this->name;
    }
    public function getMass()
    {
    return $this->mass;
    }
    }
    The Abstract Product Class

    View Slide

  55. namespace Shop;
    class HardProduct extends Product
    {
    }
    The Product Concrete Classes
    namespace Shop;
    use Physics\Metrics\Mass;
    use SebastianBergmann\Money\Money;
    class DigitalProduct extends Product
    {
    public function __construct($name, Money $price)
    {
    parent::__construct($name, $price, new Mass(0));
    }
    }

    View Slide

  56. $paperBook = new HardProduct(
    'PHP 5 Design Patterns',
    Money::fromString('49.00', 'EUR'),
    Mass::fromString('960.00 g')
    );
    $digitalBook = new DigitalProduct(
    'PHP 5 Design Patterns',
    Money::fromString('22.00', 'EUR')
    );
    Creating Physical & Digital Products

    View Slide

  57. namespace Shop;
    use SebastianBergmann\Money\Money;
    class Combo extends Product
    {
    private $products;
    function __construct($name, array $products, Money $price = null)
    {
    $this->setProducts($products);
    parent::__construct($name, $price ?: $this->getTotalPrice(), $this->getTotalMass());
    }
    // ...
    private function add(ProductInterface $product)
    {
    $this->products[] = $product;
    }
    }
    The Combo Concrete Class

    View Slide

  58. class Combo extends Product
    {
    // ...
    private function setProducts(array $products)
    {
    if (count($products) < 2) {
    throw new \LogicException('At least 2 items required.');
    }
    foreach ($products as $product) {
    $this->add($product);
    }
    }
    }
    The Combo Concrete Class

    View Slide

  59. class Combo extends Product
    {
    // ...
    private function getTotalPrice()
    {
    $total = $this->getPriceAt(0);
    for ($i = 1; $i < count($this->products); $i++) {
    $total = $total->add($this->getPriceAt($i));
    }
    return $total;
    }
    private function getPriceAt($index)
    {
    return $this->get($index)->getPrice();
    }
    private function get($index)
    {
    return $this->products[$index];
    }
    }
    Get Combo Total Price

    View Slide

  60. class Combo extends Product
    {
    // ...
    private function getTotalMass()
    {
    $total = $this->getMassAt(0);
    for ($i = 1; $i < count($this->products); $i++) {
    $total = $total->add($this->getMassAt($i));
    }
    return $total;
    }
    private function getMassAt($index)
    {
    return $this->get($index)->getMass();
    }
    }
    Get Combo Total Mass

    View Slide

  61. $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera & Bag', $products, new EUR(83900));
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Mass: ', $combo->getMass()->getValue() ," g\n";
    echo 'Price: ', $combo->getPrice()->getAmount() ," €\n";
    The Combo with a Fixed Price
    Name: Digital Camera & Bag
    Mass: 1117 g
    Price: 83900 €

    View Slide

  62. $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera & Bag', $products);
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Mass: ', $combo->getMass()->getValue() ,"\n";
    echo 'Price: ', $combo->getPrice()->getAmount() ,"\n";
    The Combo with a Dynamic Price
    Name: Digital Camera & Bag
    Mass: 1117 g
    Price: 90700 €

    View Slide

  63. $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera Combo Pack + Tripod', [
    new HardProduct('Lightweight Tripod', new EUR(2690), new Mass(570)),
    new Combo('Digital Camera & Bag', $products, new EUR(83900)),
    ]);
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Weight: ', $combo->getWeight()->getValue() ,"\n";
    echo 'Price: ', $combo->getPrice()->getAmount() ,"\n";
    The Super Dupper Combo!
    Name: Digital Camera Combo Pack + Tripod
    Mass: 1687 g
    Price: 86590 €

    View Slide

  64. Decorator

    View Slide

  65. Composition over
    Inheritance

    Extending objects at will!

    View Slide

  66. The Decorator
    Structural pattern
    •  Adding new responsabilities to an object
    •  Don’t break / change the existing API
    •  Use composition over inheritance
    •  Allow infinite responsabilities combinations
    •  Respect SOLID principle

    View Slide

  67. View Slide

  68. Decorator Usage
    $instance = new OtherConcreteDecorator(
    new SomeConcreteDecorator(
    new ConcreteDecorator(
    new DecoratedComponent()
    )
    )
    );
    $instance->operation();

    View Slide

  69. StackPHP Middlewares
    http://stackphp.com/

    View Slide

  70. Orders &
    Coupons
    Decorator Implementation

    View Slide

  71. Coupon Codes
    •  We want to offer discount codes to customers.
    •  Each discount can be value (-5 EUR) or rate (-10%) based.
    •  Coupon codes must be combinable.
    •  Each coupon is applicable upon certain conditions.
    •  There must be no limits inventing new coupon codes.
    •  Coupons reduce an Order’s total amount.

    View Slide

  72. Coupon Restrictions
    •  Valid for a specific period of time,
    •  Minimum total amount required,
    •  Minimum quantity of ordered items required,
    •  Valid for a specific geographical area,
    •  Customer must own the loyalty membership card,
    •  Coupon is valid for Premium / VIP customers only,
    •  Valid for the customer’s very first order,
    •  Some products are not eligible for discounts,
    •  Valid only on some specific products areas (luxury, food…),
    •  etc.

    View Slide

  73. UML Diagram

    View Slide

  74. namespace Shop\Discount;
    use SebastianBergmann\Money\Money;
    use Shop\OrderableInterface;
    interface CouponInterface
    {
    public function getCode();
    /**
    * Returns the new total amount after the coupon has been
    * applied on the given order.
    *
    * @param OrderableInterface $order The order to discount
    * @return Money The new order total amount
    * @throws CouponException When coupon is not applicable
    */
    public function applyDiscount(OrderableInterface $order);
    }
    The Coupon Interface

    View Slide

  75. namespace Shop\Discount;
    use SebastianBergmann\Money\Money;
    use Shop\OrderableInterface;
    class ValueCoupon implements CouponInterface
    {
    private $code;
    private $discount;
    public function __construct($code, Money $discount)
    {
    $this->code = $code;
    $this->discount = $discount;
    }
    public function getCode()
    {
    return $this->code;
    }
    public function applyDiscount(OrderableInterface $order)
    {
    return $order->getTotalAmount()->subtract($this->discount);
    }
    }
    The ValueCoupon Class

    View Slide

  76. namespace Shop\Discount;
    use Shop\OrderableInterface;
    class RateCoupon implements CouponInterface
    {
    private $code;
    private $rate;
    public function __construct($code, $rate)
    {
    if (!is_float($rate)) {
    throw new \InvalidArgumentException('$rate must be a float.');
    }
    if ($rate <= 0 || $rate > 1){
    throw new \InvalidArgumentException('$rate must be in ]0, 1].');
    }
    $this->code = $code;
    $this->rate = $rate;
    }
    }
    The RateCoupon Class

    View Slide

  77. namespace Shop\Discount;
    use Shop\OrderableInterface;
    class RateCoupon implements CouponInterface
    {
    // ...
    public function getCode()
    {
    return $this->code;
    }
    public function applyDiscount(OrderableInterface $order)
    {
    $amount = $order->getTotalAmount();
    return $amount->subtract($amount->multiply($this->rate));
    }
    }
    The RateCoupon Class

    View Slide

  78. namespace Shop\Discount;
    abstract class CouponDecorator implements CouponInterface
    {
    protected $coupon;
    public function __construct(CouponInterface $coupon)
    {
    $this->coupon = $coupon;
    }
    public function getCode()
    {
    return $this->coupon->getCode();
    }
    protected function createCouponException($message, \Exception $previous = null)
    {
    return new CouponException($message, 0, $previous);
    }
    }
    The Abstract CouponDecorator Class

    View Slide

  79. namespace Shop\Discount;
    use Shop\OrderableInterface;
    class LimitedLifetimeCoupon extends CouponDecorator
    {
    private $startAt;
    private $expiresAt;
    public function __construct(CouponInterface $coupon, $startAt, $expiresAt)
    {
    if (!$startAt instanceof \DateTime) {
    $startAt = new \DateTime($startAt);
    }
    if (!$expiresAt instanceof \DateTime) {
    $expiresAt = new \DateTime($expiresAt);
    }
    if ($startAt > $expiresAt) {
    throw new \InvalidArgumentException('$startAt cannot be greater than $expiresAt.');
    }
    parent::__construct($coupon);
    $this->startAt = $startAt;
    $this->expiresAt = $expiresAt;
    }
    }
    The LimitedLifetimeCoupon Decorator Class

    View Slide

  80. class LimitedLifetimeCoupon extends CouponDecorator
    {
    public function applyDiscount(OrderableInterface $order)
    {
    $now = new \DateTime('now');
    if ($this->startAt > $now) {
    throw $this->createCouponException(sprintf(
    'Coupon is usable from %s.',
    $this->startAt->format('Y-m-d H:i:s')
    ));
    }
    if ($now > $this->expiresAt) {
    throw $this->createCouponException(sprintf(
    'Coupon was valid until %s.',
    $this->expiresAt->format('Y-m-d H:i:s')
    ));
    }
    return $this->coupon->applyDiscount($order);
    }
    }
    The LimitedLifetimeCoupon Decorator Class

    View Slide

  81. use SebastianBergmann\Money\Money;
    use Shop\Discount\LimitedLifetimeCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Coupon offers a discount value of 20 €.
    $coupon1 = new LimitedLifetimeCoupon(
    new ValueCoupon('3s2h7pd65s', Money::fromString('20.00', EUR')),
    '2014-05-01',
    '2014-08-31'
    );
    // Coupon offers 25% off.
    $coupon2 = new LimitedLifetimeCoupon(
    new RateCoupon('76cqa6qr19', 0.25),
    '2014-05-01',
    '2014-08-31'
    );
    The LimitedLifetimeCoupon Decorator Class

    View Slide

  82. namespace Shop\Discount;
    use SebastianBergmann\Money\Money;
    use Shop\OrderableInterface;
    class MinimumPurchaseAmountCoupon extends CouponDecorator
    {
    private $minimumAmount;
    function __construct(CouponInterface $coupon, Money $minAmount)
    {
    parent::__construct($coupon);
    $this->minimumAmount = $minAmount;
    }
    }
    The MinimumPurchaseAmountCoupon Decorator Class

    View Slide

  83. class MinimumPurchaseAmountCoupon extends CouponDecorator
    {
    public function applyDiscount(OrderableInterface $order)
    {
    $amount = $order->getTotalAmount();
    if ($amount->lessThan($this->minimumAmount)) {
    throw $this->createCouponException(sprintf(
    'Coupon requires a minimum amount of %u.',
    $this->minimumAmount->getAmount()
    ));
    }
    return $this->coupon->applyDiscount($order);
    }
    }
    The MinimumPurchaseAmountCoupon Decorator Class

    View Slide

  84. use SebastianBergmann\Money\Money;
    use Shop\Discount\MinimumPurchaseAmountCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Get 20 € off if total amount is greater than 300 €
    $coupon1 = new MinimumPurchaseAmountCoupon(
    new ValueCoupon('3s2h7pd65s', Money::fromString('20.00', EUR')),
    Money::fromString('300.00', EUR')
    );
    // Get 25% off if total amount is greater than 300 €
    $coupon2 = new MinimumPurchaseAmountCoupon(
    new RateCoupon('76cqa6qr19', 0.25),
    Money::fromString('300.00', EUR')
    );
    The MinimumPurchaseAmountCoupon Decorator Class

    View Slide

  85. namespace Shop\Discount;
    use Shop\OrderableInterface;
    class CustomerFirstOrderCoupon extends CouponDecorator
    {
    public function applyDiscount(OrderableInterface $order)
    {
    $customer = $order->getCustomer();
    if ($customer->hasPastOrders()) {
    throw $this->createCouponException(
    'Customer already has past orders.’
    );
    }
    return $this->coupon->applyDiscount($order);
    }
    }
    The CustomerFirstOrderCoupon Decorator Class

    View Slide

  86. use SebastianBergmann\Money\Money;
    use Shop\Discount\CustomerFirstOrderCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Get 20 € off on the first order
    $coupon1 = new CustomerFirstOrderCoupon(
    new ValueCoupon(
    '3s2h7pd65s',
    Money::fromString('20.00', EUR')
    )
    );
    // Get 25% off on the first order
    $coupon2 = new CustomerFirstOrderCoupon(
    new RateCoupon('76cqa6qr19', 0.25)
    );
    The CustomerFirstOrderCoupon Decorator Class

    View Slide

  87. use SebastianBergmann\Money\Money;
    use Shop\Discount\ValueCoupon;
    use Shop\Discount\LimitedLifetimeCoupon;
    use Shop\Discount\MinimumPurchaseAmountCoupon;
    use Shop\Discount\CustomerFirstOrderCoupon;
    // Create the special coupon
    $coupon = new ValueCoupon('3s2h7pd65s', Money::fromString('20.00', 'EUR'));
    $coupon = new LimitedLifetimeCoupon($coupon, 'now', '+60 days');
    $coupon = new MinimumPurchaseAmountCoupon($coupon, Money::fromString('170.00', 'EUR'));
    $coupon = new CustomerFirstOrderCoupon($coupon);
    // Create the order instance
    $customer = new \Shop\Customer('[email protected]');
    $order = new \Shop\Order($customer, Money::fromString('200.00', 'EUR'));
    // Apply discount coupon on the order
    $discountedAmount = $coupon->applyDiscount($order);
    Multiple Restrictions Coupon

    View Slide

  88. Thank You!
    Questions?
    Confoo 2016 – Montréal - Hugo Hamon

    View Slide