Примеси в PHP (trait)

С версии 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 {
}

Вот такая у нас простая мебель получается, теперь давайте размышлять как мы можем расширить данные классы, и каким образом мы бы это делали:

  1. Если нам нужно узнать объём занимаемый мебелью – создадим ещё один метод в классе предке Furniture, т.к. данный функционал будет общим для всей мебели.
  2. Если нам нужно узнать цвет и материал мебели – то нам тоже подойдёт класс Furniture, лишь с одной оговоркой, что цвет и материал однородные.
  3. Если же нам надо указать материал обивки мебели, то нам уже надо либо вносить данный функционал в оба класса Chair и Couch, но это копи-паст и совсем не ООП, либо должен появится новый класс Upholstered, от которого и будут унаследованы эти классы.
  4. Теперь вспомним, что некоторая мебель у нас может раскладываться, и нам надо добавить эту информацию для классов 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:

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 '<a href="' . $href . '" ' . join(' ', $attrs) . '>' . __($text) . '</a>';
};

Теперь можно пользоваться:

$view = new View();
$link = $view->ahref("Homepage", "/", ["class" => "default"]);
echo $link; // <a href="/" class="default">Homepage</a>

Код сокращён и упрощён для наглядности

Выводы

Как можно заметить, trait’ы можно и нужно использовать, ведь таким образом мы сокращаем объём кода, который нам потребуется поддерживать, да и метод копи-пасты уже давно должен был кануть в лету, а с появлением примесей вам уж не будет оправдания :)

P.S.

Если у вас есть примеры использования примесей, прошу — оставляйте ссылки.

31 thoughts on “Примеси в PHP (trait)”

  1. Кроме преимуществ, стоит также описать и недостатки примесей и неоднозначности которые они привносят.

    Например такой случай, в котором использование “служебного” объекта более оправдано

    
    class A {
    
    public $arr = array();
    
    public function __construct()
    {
      $this->init();
    }
    
    protected function init()
    {
      $this->arr += array("arr");
    }
    
    protected function getArr()
    {
      return $this->arr;
    }
    
    }
    
    trait T {
    
    public function init()
    {
      // init trait data
    }
    
    }
    
    trait C {}
    trait D {}
    
    class B extends A {
    use C;
    use D;
    use T;
    
    public function getMoreArr()
    {
      return array_merge(parent::getArr(), array("more"));
    }
    
    }
    
    
    $test = new B();
    var_dump($test->getMoreArr()); //expected [arr, more]
    
    
    1. Приоритет ¶

      Наследуемый член из базового класса переопределяется членом, находящимся в трейте. Порядок приоритета следующий: члены из текущего класса переопределяют методы в трейте, которые в свою очередь переопределяют унаследованные методы.

      http://php.net/manual/ru/language.oop5.traits.php

  2. Трейты здорово повышают читабельность и избавляют нас от странных цепочек наследования.

    class User {
        use ORM, Cache, Singleton, Auth, Permission, MagicGetSet
    }
  3. Есть у меня небольшой самописный фреймворк, для узкого круга задач. В нем есть такой trait

    trait Configurable {
    
        private function _getConfig() {
            //parse config
        }
    
    }
    

    для того чтобы в нужные мне класы добавить возможность работать с конфигом. И есть такой

    trait MysqlAdapter {
    
        use Configurable;
    
        private function _getConnection() {
            var_dump('call _getConnection');
            $options = $this->_getConfig();
            //use $options for connect
        }
    
    }
    

    для того чтобы добавить в нужные мне класы возможность получать даные с базы например такой:

    class ConcretClass {
    
        use MysqlAdapter;
        
        public function foo($bar) {
            $conn = $this->_getConnection();
        }
    }
    

    но неочевидность получаеться в том что в класс ConcretClass уже добавлена примесь Configurable. Пока особых проблем с этим у меня не было.. но есть ощущение что что-то тут не так

  4. Эм… а вам не кажется, что вы запoздали с данной статьей? Трейты появились в PHP почти 2 года тому назад.

      1. Я не уполномочен говорить за всех, но как мне кажется, любой, кто более менее считает программирование на PHP своей профессией, прекрасно знают как использовать примеси.
        Есть темы куда более актуальные, к примеру, генераторы. Хотя и о них уже не мало написано на том же хабре.

      2. Я не совсем понимаю какую мысль вы хотите до меня донести?
        Не нужно писать статьи для начального уровня?
        Или просто не нужно ничего писать, т.к. всё уже написано?

  5. Не очень нравится что в Вашем абстрактном классе нет ни одного абстрактного метода. С таким же успехом мог быть вместо абстрактного – обычный класс.
    А вместо trait спокойно можно было бы использовать интерфейсы т.к с точки ООП это было бы самое правильное. А так пример немножко притянут за уши )) Синтаксический сахар да и только. В идеале ООП PHP должно стремится к ООП JAVA или C# без таких фич. Но статья интересная, спасибо )

  6. morgan, класс абстрактный потому что автор хотел показать что это не конечная реализация.

    Как можно вместо trait использовать интерфейс? Вы немного потерялись.

  7. Примеси, – имитация множественного наследования. В других языках, в том же Java пришлось бы использовать Интерфейсы. Вот для справки ссылочку прилагаю, если мне не верите, и обратите сразу внимание на пункт №5 уважаемый. (http://www.quizful.net/post/razlichie_v_primenenii_interfeysov_i_abstraktnih_klassov)

    1. Именно так, примеси – это лишь имитация множественного наследования, но даже она лучше чем ничего

    2. разве интерфейсы дают возможность реализовать функционал как примеси?

    3. пункт 5 по ссылке относится именно к Java, там действительно альтернатив интерфейсам нет. А в PHP есть, так почему бы их не использовать.

  8. а мне понравилась статья, сам прочитал и брату по скайпу рассказал, очень доступно все описано.

  9. Есть такой паттерн Mixin. Впринципе реализует почти все возможности множественного наследования. Можно увидеть в ядре yii.

  10. Ещё стоит 7 раз подумать, что хуже – копипаста или 100 мелких классов. Я бы предпочел видеть код целиком большими функциональными кусками, а не разбитым на 100 функций/классов/файлов

    1. 7 раз подумать – это безусловно не лишнее…
      но менять код при необходимости лучше в одном месте, чем в 28 копипастах (из 30, потому что про еще две забыли ;)

  11. В трейте нельзя создавать статические свойства. Такой синглтон не будет работать…

  12. отличная статья, мне как новичку, понравилась, доступно и понятно. Спасибо!!!!

  13. Почитал умные мануалы, не вкурил. Поискал “трейты для чайников” нашёл, прочитал, осознал. Спасибо большое автору именно за простую подачу материала.

  14. А почему нельзя писать полностью только на трейтах, ведь композиция лучше наследования? :)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.