Поведения

Поведения (behaviors) — это экземпляры класса yii\base\Behavior или класса, унаследованного от него. Поведения, также известные как примеси, позволяют расширять функциональность существующих компонентов без необходимости изменения дерева наследования. После прикрепления поведения к компоненту, его методы и свойства "внедряются" в компонент, и становятся доступными так же, как если бы они были объявлены в самом классе компонента. Кроме того, поведение может реагировать на события, создаваемые компонентом, что позволяет тонко настраивать или модифицировать обычное выполнение кода компонента.

Создание поведений

Поведения создаются путем расширения базового класса yii\base\Behavior или его наследников. Например,

namespace app\components;

use yii\base\Behavior;

class MyBehavior extends Behavior
{
    public $prop1;

    private $_prop2;

    public function getProp2()
    {
        return $this->_prop2;
    }

    public function setProp2($value)
    {
        $this->_prop2 = $value;
    }

    public function foo()
    {
        // ...
    }
}

В приведенном выше примере, объявлен класс поведения app\components\MyBehavior содержащий 2 свойства prop1 и prop2, и один метод foo(). Обратите внимание, свойство prop2 объявлено с использованием геттера getProp2() и сеттера setProp2(). Это возможно, так как yii\base\Behavior является дочерним классом для yii\base\BaseObject, который предоставляет возможность определения свойств через геттеры и сеттеры.

Так как этот класс является поведением, когда он прикреплён к компоненту, компоненту будут также доступны свойства prop1 и prop2, а также метод foo().

Подсказка: Внутри поведения возможно обращаться к компоненту, к которому оно прикреплено, используя свойство yii\base\Behavior::$owner.

Примечание: При переопределении метода поведения yii\base\Behavior::__get() и/или yii\base\Behavior::__set() необходимо также переопределить yii\base\Behavior::canGetProperty() и/или yii\base\Behavior::canSetProperty().

Обработка событий компонента

Если поведению требуется реагировать на события компонента, к которому оно прикреплено, то необходимо переопределить метод yii\base\Behavior::events(). Например,

namespace app\components;

use yii\db\ActiveRecord;
use yii\base\Behavior;

class MyBehavior extends Behavior
{
    // ...

    public function events()
    {
        return [
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
        ];
    }

    public function beforeValidate($event)
    {
        // ...
    }
}

Метод events() должен возвращать список событий и соответствующих им обработчиков. В приведенном выше примере, объявлено событие EVENT_BEFORE_VALIDATE и его обработчик beforeValidate(). Указать обработчик события, можно одним из следующих способов:

  • строка с именем метода текущего поведения, как в примере выше;
  • массив, содержащий объект или имя класса, и имя метода, например, [$object, 'methodName'];
  • анонимная функция.

Функция обработчика события должна выглядеть как показано ниже, где $event содержит параметр события. Более детальная информация приведена в разделе События.

function ($event) {
}

Прикрепление поведений

Прикрепить поведение к компоненту можно как статически, так и динамически. На практике чаще используется статическое прикрепление.

Для того чтобы прикрепить поведение статически, необходимо переопределить метод behaviors() компонента, к которому его планируется прикрепить. Метод behaviors() должен возвращать список конфигураций поведений. Конфигурация поведения представляет собой имя класса поведения, либо массив его настроек:

namespace app\models;

use yii\db\ActiveRecord;
use app\components\MyBehavior;

class User extends ActiveRecord
{
    public function behaviors()
    {
        return [
            // анонимное поведение, прикрепленное по имени класса
            MyBehavior::className(),

            // именованное поведение, прикрепленное по имени класса
            'myBehavior2' => MyBehavior::className(),

            // анонимное поведение, сконфигурированное с использованием массива
            [
                'class' => MyBehavior::className(),
                'prop1' => 'value1',
                'prop2' => 'value2',
            ],

            // именованное поведение, сконфигурированное с использованием массива
            'myBehavior4' => [
                'class' => MyBehavior::className(),
                'prop1' => 'value1',
                'prop2' => 'value2',
            ]
        ];
    }
}

Вы можете связать имя с поведением, указав его в качестве ключа элемента массива, соответствующего конфигурации поведения. В таком случае, поведение называется именованным. В примере выше, два именованных поведения: myBehavior2 и myBehavior4. Если с поведением не связано имя, такое поведение называется анонимным.

Для того, чтобы прикрепить поведение динамически, необходимо вызвать метод yii\base\Component::attachBehavior() требуемого компонента:

use app\components\MyBehavior;

// прикрепляем объект поведения
$component->attachBehavior('myBehavior1', new MyBehavior);

// прикрепляем по имени класса поведения
$component->attachBehavior('myBehavior2', MyBehavior::className());

// прикрепляем используя массив конфигураций
$component->attachBehavior('myBehavior3', [
    'class' => MyBehavior::className(),
    'prop1' => 'value1',
    'prop2' => 'value2',
]);

Использование метода yii\base\Component::attachBehaviors() позволяет прикрепить несколько поведений за раз. Например,

$component->attachBehaviors([
    'myBehavior1' => new MyBehavior,  // именованное поведение
    MyBehavior::className(),          // анонимное поведение
]);

Так же, прикрепить поведение к компоненту можно через конфигурацию, как показано ниже:

[
    'as myBehavior2' => MyBehavior::className(),

    'as myBehavior3' => [
        'class' => MyBehavior::className(),
        'prop1' => 'value1',
        'prop2' => 'value2',
    ],
]

Более детальная информация приведена в разделе Конфигурации.

Использование поведений

Для использования поведения, его необходимо прикрепить к компоненту как описано выше. После того, как поведение прикреплено к компоненту, его использование не вызывает сложностей.

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

// публичное свойство "prop1" объявленное в классе поведения
echo $component->prop1;
$component->prop1 = $value;

Аналогично, вы можете вызывать публичные методы поведения,

// публичный метод foo() объявленный в классе поведения
$component->foo();

Обратите внимание, хотя $component не имеет свойства prop1 и метода foo(), они могут быть использованы, как будто являются членами этого класса.

В случае, когда два поведения, имеющие свойства или методы с одинаковыми именами, прикреплены к одному компоненту, преимущество будет у поведения, прикрепленного раньше.

Если при прикреплении поведения к компоненту указано имя, можно обращаться к поведению по этому имени, как показано ниже:

$behavior = $component->getBehavior('myBehavior');

Также можно получить все поведения, прикрепленные к компоненту:

$behaviors = $component->getBehaviors();

Отвязывание поведений

Чтобы отвязать поведение от компонента, необходимо вызвать метод yii\base\Component::detachBehavior(), указав имя, связанное с поведением:

$component->detachBehavior('myBehavior1');

Так же, возможно отвязать все поведения:

$component->detachBehaviors();

Использование поведения TimestampBehavior

В заключении, давайте посмотрим на yii\behaviors\TimestampBehavior — поведение, которое позволяет автоматически обновлять атрибуты с метками времени при сохранении Active Record моделей через insert(), update() или save().

Для начала, необходимо прикрепить поведение к классу Active Record, в котором это необходимо:

namespace app\models\User;

use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;

class User extends ActiveRecord
{
    // ...

    public function behaviors()
    {
        return [
            [
                'class' => TimestampBehavior::className(),
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
                // если вместо метки времени UNIX используется datetime:
                // 'value' => new Expression('NOW()'),
            ],
        ];
    }
}

Конфигурация выше описывает следующее:

  • при вставке новой записи поведение должно присвоить текущую метку времени UNIX атрибутам created_at и updated_at;
  • при обновлении существующей записи поведение должно присвоить текущую метку времени UNIX атрибуту updated_at.

Примечание: Для того, чтобы приведённая выше конфигурация работала с MySQL, тип created_at и updated_at должен быть int(11). В нём будет храниться UNIX timestamp.

Теперь, если сохранить объект User, то в его атрибуты created_at и updated_at будут автоматически установлены значения метки времени UNIX на момент сохранения записи:

$user = new User;
$user->email = 'test@example.com';
$user->save();
echo $user->created_at;  // выведет метку времени на момент сохранения записи

Поведение TimestampBehavior так же содержит полезный метод touch(), который устанавливает текущую метку времени указанному атрибуту и сохраняет его в базу данных:

$user->touch('login_time');

Другие поведения

Кроме затронутых выше, есть и другие уже реализованные поведения. Как встроенные, так и сторонние:

  • yii\behaviors\BlameableBehavior - автоматически заполняет указанные атрибуты ID текущего пользователя.
  • yii\behaviors\SluggableBehavior - автоматически заполняет указанные атрибут пригодным для URL текстом, получаемым из другого атрибута.
  • yii\behaviors\AttributeBehavior - автоматически задаёт указанное значение одному или нескольким атрибутам ActiveRecord при срабатывании определённых событий.
  • yii2tech\ar\softdelete\SoftDeleteBehavior - предоставляет методы для «мягкого» удаления и воосстановления ActiveRecord. То есть выставляет статус или флаг, который показывает, что запись удалена.
  • yii2tech\ar\position\PositionBehavior - позволяет управлять порядком записей через специальные методы. Информация сохраняется в целочисленном поле.

Сравнение с трейтами

Несмотря на то, что поведения схожи с трейтами тем, что "внедряют" свои свойства и методы в основной класс, они имеют множество отличий. Они оба имеют свои плюсы и минусы, и, скорее, дополняют друг друга, а не заменяют.

Плюсы поведений

Поведения, как и любые другие классы, поддерживают наследование. Трейты можно рассматривать как копипейст на уровне языка. Они наследование не поддерживают.

Поведения могут быть прикреплены и отвязаны от компонента динамически, без необходимости модифицирования класса компонента. Для использования трейтов необходимо модифицировать класс.

Поведения, в отличие от трейтов, можно настраивать.

Поведения можно настраивать таким образом, чтобы они реагировали на события компонента.

Конфликты имен свойств и методов поведений, прикрепленных к компоненту, разрешаются на основе порядка их подключения. Конфликты имен, вызванные различными трейтами, требуют ручного переименования конфликтующих свойств или методов.

Плюсы трейтов

Трейты являются гораздо более производительными, чем поведения, которые, являясь объектами, требуют дополнительного времени и памяти.

Многие IDE поддерживают работу с трейтами, так как они являются стандартными конструкциями языка.