События

События позволяют вводить пользовательский код в существующий код в определенных точках выполнения. Вы можете присоединить пользовательский код к событию, чтобы при запуске события код выполнялся автоматически. Например, объект mailer может инициировать событие messageSent, когда он успешно отправляет сообщение. Если вы хотите отслеживать успешно отправленные сообщения, вы можете просто присоединить код отслеживания к событию messageSent.

Yii вводит базовый класс с именем yii\base\Component для поддержки событий. Если класс должен запускать события, он должен распространяться от yii\base\Component или из дочернего класса.

Обработчики событий

Обработчик событий - это обратный вызов PHP, который запускается, когда инициируется событие, к которому он присоединен. Вы можете использовать любой из следующих обратных вызовов:

  • Глобальная функция PHP, указанная как строка (без круглых скобок), например, 'trim';
  • Метод объекта, указанный как массив объекта, и имя метода в виде строки (без скобок), например [$object, 'methodName'];
  • Статический метод класса, указанный как массив имени класса, и имя метода в виде строки (без скобок), например [ClassName, methodName];
  • Анонимная функция, например, функция ($ event) {...}.

Подпись обработчика события:

function ($event) {
    // $event is an object of yii\base\Event or a child class
}

Через параметр $event обработчик события может получить следующую информацию о произошедшем событии:

  • название события;
  • отправитель события: объект, для которого был вызван метод trigger();
  • настраиваемые данные: данные, которые предоставляются при присоединении обработчика событий (будет объяснено далее).

Присоединение обработчиков событий

Вы можете присоединить обработчик к событию, вызвав метод yii\base\Component::on(). Например:

$foo = new Foo;

// this handler is a global function
$foo->on(Foo::EVENT_HELLO, 'function_name');

// this handler is an object method
$foo->on(Foo::EVENT_HELLO, [$object, 'methodName']);

// this handler is a static class method
$foo->on(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']);

// this handler is an anonymous function
$foo->on(Foo::EVENT_HELLO, function ($event) {
    // event handling logic
});

Вы можете также присоединить обработчики событий через конфигурации. При присоединении обработчика событий вы можете предоставить дополнительные данные в качестве третьего параметра для yii\base\Component::on(). Данные будут доступны для обработчика при срабатывании события и вызове обработчика. Например:

// The following code will display "abc" when the event is triggered
// because $event->data contains the data passed as the 3rd argument to "on"
$foo->on(Foo::EVENT_HELLO, 'function_name', 'abc');

function function_name($event) {
    echo $event->data;
}

Заказ обработчика событий

Вы можете присоединить один или несколько обработчиков к одному событию. Когда событие инициируется, присоединяемые обработчики будут вызываться в том порядке, в котором они были привязаны к событию. Если обработчик должен остановить вызов следящих за ним обработчиков, он может установить значение свойства yii\base\Event::$handled параметра $event как true:

$foo->on(Foo::EVENT_HELLO, function ($event) {
    $event->handled = true;
});

По умолчанию недавно добавленный обработчик добавляется к существующей очереди обработчика для события. В результате, обработчик будет вызван на последнем месте, когда произойдет событие. Чтобы вставить новый обработчик в начало очереди обработчика, чтобы обработчик был вызван первым, вы можете вызвать yii\base\Component::on(), передав false для четвертого параметра $append:

$foo->on(Foo::EVENT_HELLO, function ($event) {
    // ...
}, $data, false);

Запуск событий

События запускаются вызовом метода yii\base\Component::trigger(). Для этого метода требуется имя события и, необязательно, объект события, который описывает параметры, которые должны передаваться обработчикам событий. Например:

namespace app\components;

use yii\base\Component;
use yii\base\Event;

class Foo extends Component
{
    const EVENT_HELLO = 'hello';

    public function bar()
    {
        $this->trigger(self::EVENT_HELLO);
    }
}

С помощью приведенного выше кода любые вызовы bar() вызовут событие с именем hello.

Иногда при запуске события вы можете передать дополнительную информацию обработчикам событий. Например, почтовому серверу может понадобиться передать информацию сообщения обработчикам события messageSent, чтобы обработчики могли знать сведения о отправленных сообщениях. Для этого вы можете предоставить объект события в качестве второго параметра для метода yii\base\Component::trigger(). Объект события должен быть экземпляром класса yii\base\Event или дочернего класса. Например:

namespace app\components;

use yii\base\Component;
use yii\base\Event;

class MessageEvent extends Event
{
    public $message;
}

class Mailer extends Component
{
    const EVENT_MESSAGE_SENT = 'messageSent';

    public function send($message)
    {
        // ...sending $message...

        $event = new MessageEvent;
        $event->message = $message;
        $this->trigger(self::EVENT_MESSAGE_SENT, $event);
    }
}

Когда вызывается метод yii\base\Component::trigger(), он вызывает все обработчики, прикрепленные к именованному событию.

Отладка обработчиков событий

Чтобы отсоединить обработчик от события, вызовите метод yii\base\Component::off(). Например:

// the handler is a global function
$foo->off(Foo::EVENT_HELLO, 'function_name');

// the handler is an object method
$foo->off(Foo::EVENT_HELLO, [$object, 'methodName']);

// the handler is a static class method
$foo->off(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']);

// the handler is an anonymous function
$foo->off(Foo::EVENT_HELLO, $anonymousFunction);

Обратите внимание, что в общем случае вам не следует пытаться отключать анонимную функцию, если вы ее где-нибудь не сохранили, когда она привязана к событию. В приведенном выше примере предполагается, что анонимная функция хранится как переменная $anonymousFunction.

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

$foo->off(Foo::EVENT_HELLO);

Обработчики событий уровня класса

В предыдущих подразделах описывалось, как присоединить обработчик к событию на уровне экземпляра. Иногда вам может потребоваться реагировать на событие, инициированное каждым экземпляром класса, а не только с конкретным экземпляром. Вместо того, чтобы прикреплять обработчик событий к каждому экземпляру, вы можете присоединить обработчик на уровне класса, вызывая статический метод yii\base\Event::on().

Например, объект Active Record будет вызывать событие EVENT_AFTER_INSERT всякий раз, когда он вставляет новую запись в базу данных. Чтобы отслеживать вставки, выполненные каждым объектом Active Record, вы можете использовать следующий код:

use Yii;
use yii\base\Event;
use yii\db\ActiveRecord;

Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
    Yii::trace(get_class($event->sender) . ' is inserted');
});

Обработчик события будет вызываться всякий раз, когда экземпляр ActiveRecord или один из его дочерних классов запускает событие EVENT_AFTER_INSERT. В обработчике вы можете получить объект, который вызвал событие, через $event->sender.

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

Вы можете вызвать событие уровня класса, вызвав статический метод yii\base\Event::trigger(). Событие класса не связано с конкретным объектом. В результате это вызовет вызов только обработчиков событий уровня класса. Например:

use yii\base\Event;

Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) {
    var_dump($event->sender);  // displays "null"
});

Event::trigger(Foo::className(), Foo::EVENT_HELLO);

Обратите внимание, что в этом случае $event->sender имеет значение null вместо экземпляра объекта. Чтобы отсоединить обработчик событий класса, вызовите yii\base\Event::off(). Например:

// detach $handler
Event::off(Foo::className(), Foo::EVENT_HELLO, $handler);

// detach all handlers of Foo::EVENT_HELLO
Event::off(Foo::className(), Foo::EVENT_HELLO);

События с использованием интерфейсов

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

namespace app\interfaces;

interface DanceEventInterface
{
    const EVENT_DANCE = 'dance';
}

И два класса, которые его реализуют:

class Dog extends Component implements DanceEventInterface
{
    public function meetBuddy()
    {
        echo "Woof!";
        $this->trigger(DanceEventInterface::EVENT_DANCE);
    }
}

class Developer extends Component implements DanceEventInterface
{
    public function testsPassed()
    {
        echo "Yay!";
        $this->trigger(DanceEventInterface::EVENT_DANCE);
    }
}

Для обработки события EVENT_DANCE, вызванного любым из этих классов, вызовите Event::on() и передайте имя класса интерфейса в качестве первого аргумента:

Event::on('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) {
    Yii::trace(get_class($event->sender) . ' just danced'); // Will log that Dog or Developer danced
});

Вы можете вызвать событие этих классов:

// trigger event for Dog class
Event::trigger(Dog::className(), DanceEventInterface::EVENT_DANCE);

// trigger event for Developer class
Event::trigger(Developer::className(), DanceEventInterface::EVENT_DANCE);

Но обратите внимание, что вы не можете запускать все классы, реализующие интерфейс:

// DOES NOT WORK. Classes that implement this interface will NOT be triggered.
Event::trigger('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE);

Чтобы отключить обработчик событий, вызовите Event::off(). Например:

// detaches $handler
Event::off('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler);

// detaches all handlers of DanceEventInterface::EVENT_DANCE
Event::off('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE);

Глобальные события

Yii поддерживает так называемое глобальное событие, которое на самом деле является трюком, основанным на описанном выше механизме событий. Глобальное событие требует глобально доступного Синглтона, такого как сам экземпляр приложения.

Чтобы создать глобальное событие, отправитель события вызывает метод trigger() Singleton для запуска события вместо вызова собственного метода trigger(). Точно так же обработчики событий присоединяются к событию в Singleton. Например:

use Yii;
use yii\base\Event;
use app\components\Foo;

Yii::$app->on('bar', function ($event) {
    echo get_class($event->sender);  // displays "app\components\Foo"
});

Yii::$app->trigger('bar', new Event(['sender' => new Foo]));

Преимущество использования глобальных событий состоит в том, что вам не нужен объект при присоединении обработчика к событию, которое будет вызвано объектом. Вместо этого привязка обработчика и запуск события выполняются через Singleton (например, экземпляр приложения).

Однако, поскольку пространство имен глобальных событий совместно используется всеми сторонами, вы должны с умом назвать глобальные события, например, ввести какое-либо пространство имен (например, «frontend.mail.sent», «backend.mail.sent»).