Антон Шевчук // Web-разработчик

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

Elephpant

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

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

© Антон Шевчук 2007-2014