decorator Декоратор (англ. Decorator) належати до класу структурних шаблонів. Він використовується для динамічного розширення функціональності об’єкта. Є гнучкою альтернативою успадкування.

Сенс полягає в тому, щоб можна було безболісно комбінувати різні декоратори у довільному порядку, навішуючи їх на різні об’єкти. До певної міри, це схоже на технологію traits, за винятком того, що декоратори динамічно навішуються на об’єкт, а traits - статично на клас.

Наприклад, ми маємо клас “Cheeseburger”, який реалізує інтерфейс “BurgerInterface”. Тут ми помічаємо, що в класі “Cheeseburger” не вистачає сиру. Це основний інгредієнт чизбургеру, і добре б нам дописати його в клас.. Але уявімо, що переписувати клас ми не можемо. “Давайте успадковуємо Cheeseburger і розширимо його”, скажете ви. А якщо нам потрібно ще щось додати до Cheeseburger? Знову успадкування. А якщо потрібно це все поєднати і ще щось додати? Знову успадкуємо. Потім знову і знову…

Декоратор допомагає нам зробити це дедалі гнучкіше.

Оскільки наш абстрактний декоратор AbstractDecorator реалізує інтерфейс BurgerInterface, ми можемо розширювати його лише на рівні абстракції.

Додамо сир засобами CheeseburgerCheeseDecorator.

Наш декоратор (спадкоємець AbstractDecorator) у конструктор чекає об’єкт типу BurgerInterface. Як згадувалося вище, AbstractDecorator реалізує інтерфейс BurgerInterface. Але метод BurgerInterface->makeBurger() він не реалізує, а позначає як абстрактний. Це означає, що спадкоємці мають реалізувати його. Ось у ньому ми декоруватимемо наш чизбургер.

А тепер давайте змінимо соус у чизбургері.

Створюємо новий декоратор CheeseburgerSauceDecorator (спадкоємець AbstractDecorator). Тепер ми можемо розширювати будь-який об’єкт типу ** BurgerInterface . Це може бути як вихідний чизбургер без сиру (об’єкт **Cheeseburger->makeBurger()), так і вже декорований з сиром (CheeseburgerCheeseDecorator->makeBurger()).

  • Burgers
    • BurgerInterface.php
                    
      <?php
      
      namespace DesignPatterns\Structural\Decorator\Burgers;
      
      /**
       * Interface BurgerInterface
       * @package DesignPatterns\Structural\Decorator\Burgers
       */
      interface BurgerInterface
      {
          /**
           * @return array
           */
          public function makeBurger(): array;
      }
      
                    
    • Cheeseburger.php
                    
      <?php
      
      namespace DesignPatterns\Structural\Decorator\Burgers;
      
      /**
       * Class Cheeseburger
       * @package DesignPatterns\Structural\Decorator\Burgers
       */
      class Cheeseburger implements BurgerInterface
      {
          /**
           * @var string
           */
          protected $meat = 'chicken';
      
          /**
           * @var string
           */
          protected $sauce = 'mayonnaise';
      
          /**
           * @var string
           */
          protected $muffin = 'muffin';
      
          /**
           * @return array
           */
          public function makeBurger(): array
          {
              return [
                  'muffin' => $this->muffin,
                  'sauce'  => $this->sauce,
                  'meat'   => $this->meat,
              ];
          }
      }
      
                    
  • AbstractDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    use DesignPatterns\Structural\Decorator\Burgers\BurgerInterface;
    
    /**
     * Class AbstractDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    abstract class AbstractDecorator implements BurgerInterface
    {
    
        /**
         * @var BurgerInterface
         */
        protected $burger;
    
        /**
         * AbstractDecorator constructor.
         *
         * @param BurgerInterface $burger
         */
        public function __construct(BurgerInterface $burger)
        {
            $this->burger = $burger;
        }
    
        /**
         * @return array
         */
        abstract public function makeBurger(): array;
    }
    
                  
  • CheeseburgerCheeseDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    /**
     * Class CheeseburgerCheeseDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    class CheeseburgerCheeseDecorator extends AbstractDecorator
    {
        const BEST_CHEESE_FOR_BURGER = 'Cheddar';
    
        /**
         * @return array
         */
        public function makeBurger(): array
        {
            $burger = $this->burger->makeBurger();
    
            return array_replace(
                $burger,
                [
                    'cheese' => self::BEST_CHEESE_FOR_BURGER,
                ]
            );
        }
    }
    
                  
  • CheeseburgerSauceDecorator.php
                  
    <?php
    
    namespace DesignPatterns\Structural\Decorator;
    
    /**
     * Class CheeseburgerSauceDecorator
     * @package DesignPatterns\Structural\Decorator
     */
    class CheeseburgerSauceDecorator extends AbstractDecorator
    {
        const NEW_SAUCE = 'Ukrainian sauce';
    
        /**
         * @return array
         */
        public function makeBurger(): array
        {
            $burger = $this->burger->makeBurger();
    
            return array_replace(
                $burger,
                [
                    'sauce' => self::NEW_SAUCE,
                ]
            );
        }
    }
    
                  
  • Test case:

    DecoratorTest.php
      
    <?php
    namespace DesignPatterns\Tests\Structural\Decorator;
    
    use DesignPatterns\Structural\Decorator\Burgers\Cheeseburger;
    use DesignPatterns\Structural\Decorator\CheeseburgerCheeseDecorator;
    use DesignPatterns\Structural\Decorator\CheeseburgerSauceDecorator;
    
    class DecoratorTest extends \PHPUnit_Framework_TestCase
    {
        public function testAddCheeseDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $cheeseDecorator = new CheeseburgerCheeseDecorator($cheeseburger);
    
            $this->assertArrayNotHasKey('cheese', $cheeseburger->makeBurger());
            $this->assertArrayHasKey('cheese', $cheeseDecorator->makeBurger());
        }
    
        public function testAddSauceDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $sauceDecorator = new CheeseburgerSauceDecorator($cheeseburger);
    
            $cheeseburger = $cheeseburger->makeBurger();
            $decorateCheeseburger = $sauceDecorator->makeBurger();
    
            $this->assertArrayHasKey('sauce', $cheeseburger);
            $this->assertArrayHasKey('sauce', $decorateCheeseburger);
    
            $this->assertEquals('mayonnaise', $cheeseburger['sauce']);
            $this->assertNotEquals('mayonnaise', $decorateCheeseburger['sauce']);
            $this->assertEquals($sauceDecorator::NEW_SAUCE, $decorateCheeseburger['sauce']);
        }
    
        public function testAddSauceAndCheeseDecorator()
        {
            $cheeseburger = new Cheeseburger();
            $cheeseDecorator = new CheeseburgerCheeseDecorator($cheeseburger);
            $sauceDecorator = new CheeseburgerSauceDecorator($cheeseDecorator);
    
            $decorateCheeseburger = $sauceDecorator->makeBurger();
    
            $this->assertArrayHasKey('cheese', $decorateCheeseburger);
    
            $this->assertArrayHasKey('cheese', $decorateCheeseburger);
            $this->assertEquals($sauceDecorator::NEW_SAUCE, $decorateCheeseburger['sauce']);
        }
    }