Pull to refresh

ZF2 EventManager

Reading time11 min
Views12K
Слегка вольный перевод статьи о EventManager в Zend Framework 2 из блога Matthew Weier O'Phinney.
Статья в примерах рассказывает о том, что такое Zend\EventManager, как им пользоваться, какие преимущества дает событийный способ решения программистских задач на PHP. О том что нового нас ждет в ZF2.
Оригинал и перевод был написан при релизе zf2.dev4, перед .beta1, существенных изменений не произошло. Но все равно статью нужно использовать для ознакомления, не более.

Терминология


  • Event Manager (Менеджер событий)
    объект, который агрегирует обработчики событий (Listener) для одного и более именованных событий (Event), а также инициирует обработку этих событий.
  • Listener (Обработчик событий)
    функция/метод обратного вызова.
  • Event (Событие)
    действие, при инициации которого запускается выполнение определенных обработчиков событий

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

Начнем


Минимальные вещи, необходимые чтобы работать со всем этим:
  • экземпляр класса EventManager.
  • Один и более обработчик событий, привязанный к одному или нескольким событиям.
  • Вызвать EventManager::trigger() для инициации обработки события.

use Zend\EventManager\EventManager;

$events = new EventManager();

$events->attach('do', function($e) {
    $event  = $e->getName();
    $params = $e->getParams();
    printf(
        'Handled event "%s", with parameters %s',
        $event,
        json_encode($params)
    );
});

$params = array('foo' => 'bar', 'baz' => 'bat');
$events->trigger('do', null, $params);


На выходе получим:
Handled event "do", with parameters {"foo":"bar","baz":"bat"}

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

Но что за второй аргумент «null» в методе $events->trigger()?

Как правило, объект EventManager используется в пределах класса, и событие инициируется в пределах какого-то метода этого класса. И этот второй аргумент является «контекстом», или «целью», и в описанном случае, был бы экземпляром этого класса. Это предоставляет доступ обработчиков событий к объекту запроса, что порой может быть полезно/необходимо.

use Zend\EventManager\EventCollection,
    Zend\EventManager\EventManager;

class Example
{
    protected $events;

    public function setEventManager(EventCollection $events)
    {
        $this->events = $events;
    }

    public function events()
    {
        if (!$this->events) {
            $this->setEventManager(new EventManager(
                array(__CLASS__, get_called_class())
            );
        }
        return $this->events;
    }

    public function do($foo, $baz)
    {
        $params = compact('foo', 'baz');
        $this->events()->trigger(__FUNCTION__, $this, $params);
    }

}

$example = new Example();

$example->events()->attach('do', function($e) {
    $event  = $e->getName();
    $target = get_class($e->getTarget()); // "Example"
    $params = $e->getParams();
    printf(
        'Handled event "%s" on target "%s", with parameters %s',
        $event,
        $target,
        json_encode($params)
    );
});

$example->do('bar', 'bat');


Этот пример, по сути, делает то же самое, что и первый. Главным отличием является то, что вторым аргументом метода trigger() мы передаем обработчику контекст (объект — запустивший процесс обработки этого события), и обработчик получает его через метод $e->getTarget() – и может сделать с ним чтонибудь (в рамках разумного :) ).

У вас может возникнуть 2 вопроса:
  • Что такое EventCollection?
  • И что за аргументы мы передаем конструктору EventManager?

Ответ далее.

EventCollection vs EventManager


Один из принципов, которым стараются следовать в ZF2 это Принцип подстановки Лисков. Интерпретацией этого принципа может быть следующее: Для любого класса, который в будущем может понадобиться переопределить другим классом, должен быть определен “базовый” интерфейс. И это позволяет разработчикам использовать другую реализацию какого-то класса, определив методы этого интерфейса.

Поэтому был разработан интерфейс EventCollection, который описывает объект, способный к агрегации слушателей на события, и инициации этих событий. EventManager — стандартная реализация, которая войдет в ZF2.

StaticEventManager


Одним аспектом, которым обеспечивает реализация EventManager, является возможность взаимодействовать с StaticEventCollection. Этот класс позволяет присоединять обработчики не только к именованным событиям, но и к событиям, инициируемых определенным контекстом или целью. EventManager, при обработке событий, также берет обработчики событий (подписанных на текущий контекст) из объекта StaticEventCollection и исполняет их.

Как это работает?

use Zend\EventManager\StaticEventManager;

$events = StaticEventManager::getInstance();
$events->attach('Example', 'do', function($e) {
    $event  = $e->getName();
    $target = get_class($e->getTarget()); // "Example"
    $params = $e->getParams();
    printf(
        'Handled event "%s" on target "%s", with parameters %s',
        $event,
        $target,
        json_encode($params)
    );
});


Этот пример практически идентичен предыдущему. Разница лишь в том, что первым аргументом в методе attach(), мы передаем контекст — 'Example', на который хотим присоединить наш обработчик. Другими словами при обработке события 'do', если это событие инициируется контекстом 'Example', то вызываем наш обработчик.

Это как раз то место, где параметры конструктора EventManager играют роль. Конструктор позволяет передать строку, или массив строк, определяя имя/имена контекстов, для которых нужно брать обработчики событий из StaticEventManager. Если передается массив контекстов, то все обработчики событий из этих контекстов будут выполнены. Обработчики событий, присоединенные непосредственно к EventManager будут выполнены раньше обработчиков, определенных в StaticEventManager.

Объединим определение класса Example и статический обработчик события из 2х последних примеров, и дополним следующим:

$example = new Example();
$example->do('bar', 'bat');


На выходе получим:
Handled event "do" on target "Example", with parameters {"foo":"bar","baz":"bat"}

А сейчас расширим класс Example:

class SubExample extends Example
{
}


Обратите внимание на то, какие параметры мы передаем конструктору EventManager — это массив из __CLASS__ и get_called_class(). Это значит, что при вызове метода do() класса SubExample, наш обработчик события также выполнится. Если бы мы в конструкторе указали только 'SubExample’, то наш обработчик выполнится только при SubExample::do(), но не при Example::do().

Имена, используемые в качестве контекстов или целей, не обязательно должны быть именами классов; можно использовать произвольные имена. К примеру, если у вас есть набор классов, отвечающих за Кэширование или ведение Логов, вы можете именовать контексты как «log» и «cache», и использовать эти имена, а не имена классов.

Если вы не хотите чтобы Менеджер событий обрабатывал статические события, можно передать параметр null методу setStaticConnections():

$events->setStaticConnections(null);


Для того чтобы обратно подключить обработку статических событий:

$events->setStaticConnections(StaticEventManager::getInstance());


Listener Aggregates


Вам может понадобиться подписать целый класс на обработку нескольких событий, и в этом “классе обработчике” определить методы для обработки каких-то событий. Чтобы это сделать, можно реализовать в своем “классе обработчике” интерфейс HandlerAggregate. Этот интерфейс определяет 2 метода attach(EventCollection $events) и detach(EventCollection $events).

(Сам не понял что перевел, пример ниже более понятен).

use Zend\EventManager\Event,
    Zend\EventManager\EventCollection,
    Zend\EventManager\HandlerAggregate,
    Zend\Log\Logger;

class LogEvents implements HandlerAggregate
{
    protected $handlers = array();
    protected $log;

    public function __construct(Logger $log)
    {
        $this->log = $log;
    }

    public function attach(EventCollection $events)
    {
        $this->handlers[] = $events->attach('do', array($this, 'log'));
        $this->handlers[] = $events->attach('doSomethingElse', array($this, 'log'));
    }

    public function detach(EventCollection $events)
    {
        foreach ($this->handlers as $key => $handler) {
            $events->detach($handler);
            unset($this->handlers[$key];
        }
        $this->handlers = array();
    }

    public function log(Event $e)
    {
        $event  = $e->getName();
        $params = $e->getParams();
        $log->info(sprintf('%s: %s', $event, json_encode($params)));
    }
}


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

$doLog = new LogEvents($logger);
$events->attachAggregate($doLog);


и любое событие, которое должен обработать наш обработчик (LogEvents) будет обработан соответствующим методом класса. Это позволяет вам определять “комплексные” обработчики событий в одном месте (stateful обработчики).

Обратите внимание на метод detach(). Точно так же как и attach(), в качестве аргумента он принимает объект EventManager, и “отсоединяет” все обработчики (из нашего массива обработчиков — $this->handlers[]) из менеджера событий. Это возможно потому что EventManager::attach() возвращает объект, представляющий обработчик — который мы ‘присоединяли’ ранее в методе LogEvents::attach().

Результат работы обработчиков


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

EventManager возвращает объект ResponseCollection. Этот класс наследуется от класса SplStack, и предоставляет вам доступ к результатам работы всех обработчиков (Результат работы последнего обработчика будет в начале стека результатов).

ResponseCollection, помимо методов SplStack, имеет дополнительные методы:
  • first() — результат выполнения первого обработчика
  • last() — результат выполнения последнего обработчика
  • contains($value) — проверка на наличие результата в стеке результатов, возвращает true/false.

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

Прерывание обработки события


Если один из обработчиков получил результат, который ожидал инициатор события; или обработчик вдруг решит что что-то идет не так; или один из обработчиков, по каким-то причинам, решит что не нужно исполнять последующие обработчики — у вас есть механизм прерывать выполнение ‘стека’ обработчиков событий.

Примером где это может понадобиться может служить механизм кэширования, построенный на основе EventManager. В начале вашего метода вы инициируете событие “поиска данных в кеше”, и если один из обработчиков найдет в ответственном ему кэше нужные данные, то прерывается выполнение остальных обработчиков, и вы возвращаете данные, полученные из кэша. Если не найдет, то вы генерируете данные, и запускаете событие “запись в кэш”

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

Пример:

public function someExpensiveCall($criteria1, $criteria2)
{
    $params  = compact('criteria1', 'criteria2');
    $results = $this->events()->triggerUntil(__FUNCTION__, $this, $params, function ($r) {
	return ($r instanceof SomeResultClass);
    });
    if ($results->stopped()) {
	return $results->last();
    }

    // ... do some work ...
}


аргументы метода triggerUntil() подобны аргументам метода trigger(), за исключением дополнительного аргумента в конце — функция обратного вызова, которая и занимается проверкой результата каждого обработчика, и если она возвращает true, то выполнение последующих обработчиков прерывается.

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

Другим способом прервать обработку события, является использование метода stopPropagation(true) в теле самого обработчика. Что заставит менеджер событий остановить исполнение последующих обработчиков.

$events->attach('do', function ($e) {
    $e->stopPropagation();
    return new SomeResultClass();
});


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

Порядок выполнения обработчиков


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

EventManager::attach() и StaticEventManager::attach() имеют необязательный аргумент priority (по умолчанию он равен 1), с помощью которого вы и можете управлять приоритетом выполнения обработчиков. Обработчик с большим приоритетом исполняется раньше обработчиков с меньшим приоритетом.

$priority = 100;
$events->attach('Example', 'do', function($e) {
    $event  = $e->getName();
    $target = get_class($e->getTarget()); // "Example"
    $params = $e->getParams();
    printf(
        'Handled event "%s" on target "%s", with parameters %s',
        $event,
        $target,
        json_encode($params)
    );
}, $priority);


Matthew Weier O'Phinney рекомендует использоват приоритеты только в случае крайней необходимости. И я, пожалуй, с ним соглашусь.

Соберем все вместе: Простой механизм кэширования


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

Сперва определим метод, который мог бы использовать кэширование.

Matthew Weier O'Phinney в своих примерах часто в качестве имени события использует __FUNCTION__, и считает это хорошей практикой, поскольку позволяет легко написать макрос для запуска событий, а также позволяет однозначно определять уникальность этих имен (тем более что контекстом, обычно, выступает класс инициирующий событие). А для разделения событий, вызываемых в рамках одного метода, использовать постфиксы типа «do.pre», «do.post», «do.error» и т.п.

Кроме того, $params, передаваемый событию — является списком аргументов, переданных методу. Это потому, что аргументы могут не сохранятся в объекте, и обработчики могут не получить нужные им параметры из контекста. Но остается вопрос, как именовать результирующий параметр для события, записывающего в кэш? В примере используется __RESULT__, что удобно, поскольку двойное подчеркивание с двух сторон, как правило зарезервировано системой.

Наш метод мог бы выглядеть примерно так:

public function someExpensiveCall($criteria1, $criteria2)
{
    $params  = compact('criteria1', 'criteria1');
    $results = $this->events()->triggerUntil(__FUNCTION__ . '.pre', $this, $params, function ($r) {
	return ($r instanceof SomeResultClass);
    });
    if ($results->stopped()) {
	return $results->last();
    }

    // ... do some work ...

    $params['__RESULT__'] = $calculatedResult;
    $this->events()->trigger(__FUNCTION__ . '.post', $this, $params);
    return $calculatedResult;
}


Теперь определим обработчики события, работающие с кешем. Нам нужно присоединить обработчики к событиям 'someExpensiveCall.pre' and 'someExpensiveCall.post'. В первом случае, если данные найдены в кэше, мы их возвращаем. В последнем, мы сохраняем данные в кэш.

Также мы предполагаем, что переменная $cache определена ранее, и схожа с объектом Zend_Cache. Для обработчика 'someExpensiveCall.pre' мы устанавливаем приоритет 100, чтобы гарантировать выполнение обработчика раньше других, а для 'someExpensiveCall.post' приоритет -100, на случай если другие обработчики захотят модифицировать данные до записи в кэш.

$events->attach('someExpensiveCall.pre', function($e) use ($cache) {
    $params = $e->getParams();
    $key    = md5(json_encode($params));
    $hit    = $cache->load($key);
    return $hit;
}, 100);

$events->attach('someExpensiveCall.post', function($e) use ($cache) {
    $params = $e->getParams();
    $result = $params['__RESULT__'];
    unset($params['__RESULT__']);
    $key    = md5(json_encode($params));
    $cache->save($result, $key);
}, -100);

Примечание: мы моглибы определить HandlerAggregate, и хранить $cache в свойстве класса, а не импортировать его в анонимную функцию.


Конечно, мы могли бы реализовать механизм кэширования в самом объекте, а не выносить в обработчик событий. Но такой подход дает нам возможность подключать обработчики кэширования к другим событиям (реализовать механизм кеширования для других классов, храня логику выборки из кэша и сохрания в кэш в одном месте), или присоединить другие обработчики к этим событиям (которые бы занимались к примеру ведением логов, или валидацией). Дело в том, что, если вы проектируете ваш класс с использованием событий — вы делаете его более гибким и расширяемым.

Заключение


EventManager это новое и мощное дополнение к Zend Framework. Уже сейчас он используется с новым прототипом MVC для расширения возможностей некоторых его аспектов. После релиза ZF2 событийная модель, уверен, будет очень востребована.

Конечно, есть кое какие шероховатости, над устранением которых народ работает.

От себя добавлю — ничего кардинально нового нет, приятно, что такая штука появится в Zende — использовать буду однозначно.
Текст думаю перенасыщен терминами и тяжело читаем (отчасти из-за малого опыта в переводе статей).
Ничего не имею против критики.

Оригинал: http://weierophinney.net/matthew.
Tags:
Hubs:
+25
Comments15

Articles

Change theme settings