ZF2 EventManager

Слегка вольный перевод статьи о 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.
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 15
  • 0
    Спасибо.
    Приятно что PHP и ZF продолжает развиваться, и радовать нас новыми вкусняхами.
    • +2
      >… это Принцип подстановки Лисков. Интерпретацией этого принципа может быть следующее: Для любого
      > класса, который в будущем может понадобиться переопределить другим классом, должен быть определен
      > “базовый” интерфейс. И это позволяет разработчикам использовать другую реализацию какого-то класса,
      > определив методы этого интерфейса.

      Специально посмотрел в оригинал и там тоже это есть. И тут же ссылка на википедию с правильным определением.
      ИМХО, то что main contributor фреймворка не понимает LSP уже должно много говорить о фреймворке.
      • 0
        > $results = $this->events()->triggerUntil(__FUNCTION__, $this, $params, function ($r) {
        > return ($r instanceof SomeResultClass);
        > });

        А вот здесь LSP и нарушается.
        • 0
          Не уловил где противоречие… Есть интерфейс EventCollection, описывающий поведение объектов, способных к агрегации обработчиков событий и «запуск» выполнения этих обработчиков.
          EventManager — объект реализующий этот интерфейс, и соответственно он может агрегировать и исполнять обработчики.
          И если Вас не устраивает реализация EventManager, вы можете написать свой объект, реализующий интерфейс EventCollection — со своей логикой — и использовать его заместо EventManager. Главное условие, чтобы:
          «Поведение наследуемых классов не должно противоречить поведению, заданному базовым классом, т.е. поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа.» (с) Wiki
          Только базовый класс здесь — интерфейс EventCollection.
          PS.: метод triggerUntil объявлен в интерфейсе EventCollection
          • +1
            Противоречие здесь не в EventCollection, а в том, что по сути не определен интерфейс самого обработчика события. Что приводит к этим самым проверкам на тип возвращаемого значения, для того чтобы понять какой все-таки хэндлер запустился.

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

            • 0
              Наверное я все таки что-то не понимаю…

              1) Везде и во всем следовать ЛСП (для всего создавать базовый класс/интерфейс) довольно сложное занятие, и зачастую лишнее. Стараются эти интерфейсы определять для ключевых элементов фрэймворка.

              2) Есть интерфейс EventDescription — если вы про него — оно и представляет собой событие (в статье про него не упоминается вроде)

              3) есть класс ResponseCollection — он агрегирует результаты работы обработчиков.

              4) Если вся претензия к «return ($r instanceof SomeResultClass)» то тут нет никаких противоречий.

              $results = $this->events()->triggerUntil(__FUNCTION__, $this, $params, function ($r) {
              return ($r instanceof SomeResultClass);
              });

              Это говорит Менеджеру событий исполнять все обработчики, подписанные на событие, до тех пор, пока один из них не вернет результат работы типа SomeResultClass. Что это за класс (а результат работы обработчика не обязательно должен быть объектом, скалярные тоже пойдут) фрэймворк не описывает.
              — Если опять не в тему — то сорри — не понимаю :)
              • 0
                Мы не проверяем кто запустился, нам не важно это, нам важен результат который это кто-то выдал, эта проверка и осуществляется кодом анонимки. И это, имхо, не нарушает ЛСП. С другой стороны Вы правы, мы сами назначаем когда закончить отдавать обработчикам несчастное событие и вернуть результат, тоже считаю что не совсем верно, это должно осуществляться в обработчике, обработчик указал что всё, ребята, больше тут делать нечего — не отдаем событие другим. Но может пример не совсем удачный. Т.о. Метод triggerUntil не нужен, имхо, он просто добавляет дополнительный функционал (и гибкость?).
                • 0
                  >мы сами назначаем когда закончить отдавать обработчикам несчастное событие и вернуть результат, тоже считаю что не совсем верно, это должно осуществляться в обработчике

                  Где-то это может и верно (JacaScript может,...), но идея событийности в zf2 (точнее в разрабатываемом прототипе MVC), заключается в том что обработчик ничего не знают о других обработчиках. И он не может знать нужно ли прерывать обработку других событий. Прерывание обработчиков — обязанность того — кто инициировал событие — инициатор события заинтересован в получении каких-то результатов от событий, а не наоборот (в этом смысле stopPropagation() — добавляет дополнительный функционал, а рабочей лошадкой является именно triggerUntil() ).

                  К примеру при выполнении ActionController::dispatch()
                  инициируется событие:

                  $result = $this->events()->trigger('dispatch', $e, function($test) {
                  return ($test instanceof Response);
                  });

                  т.е. метод dispatch исполняет всех обработчиков, пока какой-нибудь обработчик не вернет объект Response.
                  На событие 'dispatch' могут быть подписаны разные обработчики — кто то занимается кэшированием, ктото ведением логов, проверкой прав доступа, еще чтото. Но методу dispatch() это не важно, он всего лишь хочет, чтоб хоть ктонибуть вернул Response.
                  Ну и соответственно обработчикам тоже все равно кто инициировал событие, и чего он хочет — они просто делают свою работу.
                  както так :)
                  • 0
                    поправлюсь
                    stopPropagation() — не совсем дополнительная фишка событийности в zf2. А очень даже необходимый функционал.
                    Обработчикам нужен такой функционал, к примеру при проверки прав доступа: Если обработчик решит что недостаточно прав для доступа к какому-то ресурсу — то он вполне может захотеть прервать обработку события.
        • +1
          Я знаю, здесь не принято, но можете поподробнее рассказать?
          • +1
            Упс, это к комментарию retran
            • +1
              Без проблем.

              Смотрите: пусть есть некоторый класс A с каким-либо интерфейсом. И в это понятие интерфейса входит некий набор публичных методов и полей, некоторый набор исключений, которые могут бросаться этими методами. Иногда даже некие сайд-эффекты (хотя это плохо, но бывает всякий легаси, который нужно поддерживать в процессе рефакторинга).

              Теперь вы наследуете от класса A некий класс B, который каким-либо образом переопределяет поведение класса А. Так вот, LSP говорит нам о том, что мы должны реализовать класс B таким образом, чтобы он полностью сохранил внешний интерфейс класса А, и что некий внешний код, который использует эти классы ничего не должен знать о разнице между классами A и B.

              И все это вместе позволяет писать код, поведение которого можно расширять не изменяя данный код, а просто расширяя (наследуя) существующие классы и внедряя объекты в нужные места, например с помощью DI-контейнеров.

              Пример:
              Вы пишете некий юнит-тест, который тестирует класс A, который в свою очередь зависит от некоего класса B. Для того чтобы тест был именно модульный, вы должны заменить класс B неким «фальшивым» классом MockB с таким же интерфейсом как и у класса B. Соответственно, класс A должен быть написан таким образом, чтобы он не видел разницы между B и MockB.
              • 0
                Немножко неточно. Суть не в сохранении интерфейса, так как он в любом случае сохраняется, а в сохранении и НЕ модификации поведения класса предка (А) в классе наследнике (В). Проще говоря, если вы в коде замените все экземпляры класса А на экземпляры класса В и после это все работает абсолютно корректно, то вы соблюли LSP.

                Вы слегка перепутали LSP и обычный code by contract (использование интерфейсов вместо реализаций) в вашем примере с моками.
              • 0
                Отсюда — регулирование поведения кода в зависимости от типа пришедшего объекта, кроме случаев валидации аргументов, как правило уже bad smell.

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