З версії 5.4 в PHP з’явився такий цікавий механізм як домішки (trait), який за задумом розробників повинен допомогти розрулювати ситуації коли вже дуже хочеться застосувати множинне спадкування, але не можна. Ось про деяких подібних ситуаціях я і розповім далі.

Приклади не надумані, а цілком робочі з фреймворку Bluz 😉

Теорія

Тут буде короткий переказ офіційній документації в моїй інтерпретації, якщо не цікаво — промотати трохи далі…

Хоча ні, краще залишіться і прочитайте, адже мені абсолютно не подобається подача матеріалу в офіційній документації, для розуміння домішок треба починати розмову з необхідності даного нововведення в PHP. Почну здалеку, ось один з класичних прикладів ООП: є наступні класи – абстрактний клас Меблі, що лише знає що у меблів є розміри, Стіл з площею стільниці і Стілець з якоїсь максимально можливим навантаженням, в доважок є Диван, поки просто диван:

abstract class Furniture {
protected $width;
protected $height;
protected $length;
public function getDimension() {
return [$this->width, $this->height, $this->length];
}
}
class Table extends Furniture {
protected $square;
public function getSquare() {
return $this->square;
}
}
class Chair extends Furniture {
protected $maxWeight;
public function getMaxWeight() {
return $this->maxWeight;
}
}
class Couch extends Furniture {
}

Ось така у нас проста меблі виходить, тепер давайте міркувати як ми можемо розширити дані класи, і яким чином ми це робили:

  • Якщо нам потрібно дізнатися обсяг займаний меблями – створимо ще один метод у класі предка Furniture, т. к. даний функціонал буде спільним для всієї меблів.
  • Якщо нам потрібно дізнатися колір і матеріал меблів – то нам теж підійде клас Furniture, лише з одним застереженням, що колір і матеріал однорідні.
  • Якщо ж нам треба вказати матеріал оббивки меблів, то нам вже треба або вносити даний функціонал в обидва класу Chair і Couch, але це копі-паст і зовсім не ООП, або повинен з’явиться новий клас Upholstered, від якого і будуть успадковані ці класи.
  • Тепер згадаємо, що деяка меблі у нас може розкладатися, і нам треба додати цю інформацію для класів Table і Couch, можна було б створити ще один клас Folding і розширювати його, але ця зміна буде конфліктувати з попереднім рішенням, і виходить, що єдиний вихід – копіпаст методів між класами.
  • Давайте-ка розпишемо даний підхід у коді:

    abstract class Furniture {
    protected $width;
    protected $height;
    protected $length;
    public function getDimension() {
    return [$this->width, $this->height, $this->length];
    }
    // requirement 01
    public function getVolume() {
    return $this->width * $this->height * $this->length;
    }
    // requirement 02
    protected $color;
    protected $material;
    public function getColor() {
    return $this->color;
    }
    public function getMaterial() {
    return $this->material;
    }
    }
    class Table extends Furniture {
    protected $square;
    public function getSquare() {
    return $this->square;
    }
    // requirement 04
    protected $maxSquare;
    public function getMaxSquare() {
    return $this->maxSquare;
    }
    }
    class Chair extends Furniture {
    protected $maxWeight;
    public function getMaxWeight() {
    return $this->maxWeight;
    }
    // requirement 03
    protected $upholstery;
    public function getUpholstery() {
    return $this->upholstery;
    }
    }
    class Couch extends Furniture {
    // requirement 03
    protected $upholstery;
    public function getUpholstery() {
    return $this->upholstery;
    }
    // requirement 04
    protected $maxSquare;
    public function getMaxSquare() {
    return $this->maxSquare;
    }
    }

    Та тут неозброєним оком видно копипасту, і дуже хотілося б позбавиться від неї, хотілося б реалізацію вимог 3 і 4 закинути в окремий клас, і наслідувати його, але в PHP немає множинного спадкоємства, може бути тільки один клас предок. І ось в PHP 5.4 на сцену виходять домішки (trait), чимось вони схожі на класи, але лише здалеку, домішки лише групують якийсь набір функціоналу під однією вивіскою, але не більше. Давайте таки опишемо необхідний функціонал в домішках:

    // requirement 03
    trait Upholstery {
    protected $upholstery;
    public function getUpholstery() {
    return $this->upholstery;
    }
    }
    // requirement 04
    trait MaxSquare {
    protected $maxSquare;
    public function getMaxSquare() {
    return $this->maxSquare;
    }
    }

    Тепер даний домішки легко можна підключити в наших класах:

    class Table extends Furniture {
    // requirement 04
    use MaxSquare;
    protected $square;
    public function getSquare() {
    return $this->square;
    }
    }
    class Chair extends Furniture {
    // requirement 03
    use Upholstery;
    protected $maxWeight;
    public function getMaxWeight() {
    return $this->maxWeight;
    }
    }
    class Couch extends Furniture {
    // requirement 03
    use Upholstery;
    // requirement 04
    use MaxSquare;
    }

    Як бачимо, нічого складного і результат читається, і код акуратний. Як по мені – домішки – це копіпаст на рівні мови програмування, і головне, не слід їх використовувати як альтернативу класичному спадкоємства, це лише додатковий інструмент, що допомагає обійти обмеження єдиного спадкування.

    Реалізація шаблону Singleton

    Можна багато сперечатися про даному шаблоні, є у нього і плюси і мінуси, але мова не про це, а про його реалізацію, при чому так, в один use 😉

    class Registry {
    use Singleton;
    /* … */
    }

    Щоб це стало можливим слід реалізувати ось таку домішка:

    trait Singleton
    {
    protected static $instance;
    protected function __construct()
    {
    static::setInstance($this);
    }
    final public static function setInstance($instance)
    {
    static::$instance = $instance;
    return static::$instance;
    }
    final public static function getInstance()
    {
    return isset(static::$instance)
    ? static::$instance
    : static::$instance = new static;
    }
    }

    Повний лістинг класу можна знайти в репозиторії – Bluz/Common/Singleton.php. Приклад не претендує на універсальність, але він юзабелен і має право на життя.

    Зверніть увагу, реалізація шаблону Singleton жодним чином не зобов’язує вас на хоч якісь об’єднання класів під одним предком, т. к. це виглядає дуже дивно, а от для позбавлення від практики копіювання функціоналу з класу в клас нам і потрібен цей trait

    Реалізація інтерфейсу ініціалізації

    Дуже часто траплялося мені зустрічати класи, у яких є пачка «сетеров», та ще й якийсь метод setSettings або setOptions, який приймає масив параметрів і всі ці «сетеры» смикає, раніше для цього ми б описували якийсь інтерфейс Options, який би зобов’язував розробника писати реалізацію цього нещасливого методу setOptions (а скоріше за все його б скопіювали з аналогічного класу). Але подібний підхід застарів, і для такого випадку був створений trait Options:

    trait Options {
    public function setOptions(array $options)
    {
    // apply options
    foreach ($options as $key => $value) {
    $method = ‘set’ . $this->normalizeKey($key);
    if (method_exists($this, $method)) {
    $this->$method($value);
    }
    }
    }
    private function normalizeKey($key)
    {
    $option = str_replace(‘_’, ‘ ‘, strtolower($key));
    $option = str_replace(‘ ‘, “, ucwords($option));
    return $option;
    }
    }

    Тепер спробуємо заюзать дану домішка в простому шаблонизаторе:

    class View {
    use Options;
    protected $path;
    protected $template;
    public function setPath($path) {
    $this->path = $path;
    }
    public function setTemplate($template) {
    $this->template = $template;
    }
    }

    А ось і приклад використання:

    $view = new View();
    $view->setOptions(
    ‘path’ => ‘dir/with/templates’,
    ‘template’ => ‘view.phtml’
    );

    Домішки дають нам можливість не тільки описати інтерфейс, але і фактично реалізувати його, без необхідності копіювати ідентичний код з класу в клас

    Реалізація помічників класу

    Про що це я, та про досить популярному прийомі, коли функціонал одного класу поділяють по різних класів і функцій з ледачою ініціалізацією, найбільш наочний приклад — це помічники View у Zend Framework. У фреймворку Bluz даний підхід реалізований в одному договорі:

    trait Helper
    {
    abstract protected getHelperPath();
    public function __call($method, $args)
    {
    $helperPath = $this->getHelperPath();
    $helperFullPath = realpath($helperPath . ‘/’ . ucfirst($method) . ‘.php’);
    if ($helperFullPath ) {
    $helperInclude = include $helperFullPath ;
    if ($helperInclude instanceof \Closure) {
    return call_user_func_array($helperInclude, $args);
    } else {
    throw new Exception(“Helper ‘$method’ not found in file ‘$helperPath'”);
    }
    }
    throw new Exception(“Helper ‘$method’ not found for ‘” . __CLASS__ . “‘”);
    }
    }

    Для прикладу будемо використовувати все той же шаблонизатор:

    // file View/View.php
    /**
    * @method string ahref(\string $text, \string $href array $attributes = [])
    */
    class View {
    use Helper;
    protected getHelperPath() {
    return dirname(__FILE__) .’/Helper/’;
    }
    }

    Як ледачого помічника у нас буде анонімна функція:

    // file View/Helper/Ahref.php
    return
    function ($text, $href array $attributes = []) {
    /** @var View $this */
    $attrs = [];
    foreach ($attributes as $attr => $value) {
    $attrs[] = $attr . ‘=”‘ . $value . ‘”‘;
    }
    return ” . __($text) . “;
    };

    Тепер можна користуватися:

    $view = new View();
    $link = $view->ahref(“Homepage”, “/”, [“class” => “default”]);
    echo $link; // Homepage

    Код скорочений і спрощений для наочності

    Висновки

    Як можна помітити, trait’и можна і потрібно використовувати, адже таким чином ми скорочуємо обсяг коду, який нам потрібно підтримувати, так і метод копі-пасти вже давно повинен був канути в лету, а з появою домішок вам вже не буде виправдання 🙂

    P. S.

    Якщо у вас є приклади використання домішок, прошу — залишайте посилання.