Symfony Components, Event Dispatcher (теория, часть 2)

Привет. Это вторая часть перевода документации по Symfony компоненту Event Dispatcher, первая часть здесь. Вторая часть перевода представляет собой сборник общих рецептов по использованию компонента Event Dispatcher. Для тестирования приведенных примеров не нужно подключать фреймворк Symfony — как отмечалось в первой части, компоненты независимы от фреймворка. Еще раз хочу отметить, что код Symfony компонентов сейчас перерабатывается для использования с Symfony 2 (PHP >= 5.3.2). Данный перевод относится к стабильной версии компонента Event Dispatcher. Но, насколько я понял из сравнения стабильной версии компонента с текущей под Symfony 2, — функционально они мало чем отличаются, то есть документация будет полезна и использующим новую версию компонента (PHP >= 5.3.2). Итак начнем.

Пройдемся по возможностям объекта Event Dispatcher

Если вы просматривали код класса sfEventDispatcher, вы должны были отметить, что класс не работает по принципу шаблона проектирования Singleton (в нем нет статического метода getInstance()). Это сделано специально, так как вы можете захотеть работать с несколькими конкурирующими диспетчерами событий в одном PHP запросе. Но это также подразумевает, что вам нужен способ передачи диспетчера объектам, которые должны взаимодействовать с событиями или их порождать.

Лучшей практикой будет включение объекта диспетчера событий в ваши объекты по шаблону «внедрение зависимости» (dependency injection). 

От себя замечу: Мартин Фаулер (Fowler) выделяет 3 пути по которым объект может получать ссылку на внешний модуль, в соответствии с используемым шаблоном внедрения зависимости:

  1. Внедрение интерфейса или (interface injection), в котором внешний модуль обеспечивает интерфейс, которого его пользователи должны придерживаться для получения зависимостей во время выполнения;
  2. Внедрение установщика (setter injection), в котором зависимый модуль предоставляет метод установщика (setter), который фреймворк использует для внедрения зависимости;
  3. Внедрение конструктора (constructor injection), в котором зависимости обеспечиваются через конструктор внешнего класса.

Вы можете использовать внедрение конструктора (constructor injection):

class Foo {
protected $dispatcher = null;

public function __construct(sfEventDispatcher $dispatcher) {
$this->dispatcher = $dispatcher;
}
}

Или внедрение установщика (setter injection):

class Foo {
protected $dispatcher = null;

public function setEventDispatcher(sfEventDispatcher $dispatcher) {
$this->dispatcher = $dispatcher;
}
}

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

Примечание: если вы используете внедрение зависимости как в наших предыдущих двух примерах, вы легко можете использовать контейнер Symfony Dependency Injection для более элегантного управления этими объектами.

Делаем что-то до или сразу после вызова метода

Если вы хотите сделать что-либо до, или сразу после вызова метода, вы можете объявить событие, соответственно, в начале или в конце метода:

class Foo
{
// от себя:
// можно объявить некоторый статический метод (вызываем перед самим методом):
// static public function do_before($arr){
//  echo "before!!!";
// }

public function send($foo, $bar)
{
// делаем что-то до вызова метода
$event = new sfEvent($this, 'foo.do_before_send', array('foo' => $foo, 'bar' => $bar));
// от себя: привязываем событие к вызову статического метода:
// $this->dispatcher->connect('foo.do_before_send', 'Foo::do_before');
$this->dispatcher->notify($event);

// реально метод выполняется здесь
// $ret = ...;

// делаем что-то после вызова метода
$event = new sfEvent($this, 'foo.do_after_send', array('ret' => $ret));
$this->dispatcher->notify($event);

return $ret;
}
}

 

Добавление методов к классу

Чтобы позволить многим классам добавлять методы друг другу, вы можете объявить «магический» метод __call() в классе, который вы хотите расширить, таким образом:

class Foo
{
// ...

public function __call($method, $arguments)
{
// создаем событие под названием 'foo.method_is_not_found'
// и передаем название метода и передаваемые аргументы в этот метод
$event = new sfEvent($this, 'foo.method_is_not_found', array('method' => $method,'arguments' => $arguments));

// вызываем все обработчики пока один не сможет выполнить $method
$this->dispatcher->notifyUntil($event);

// ни один обработчик не смог обработать событие? Метод не существует
if (!$event->isProcessed())  {
throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
}

// возвращаем значение, которое вернул обработчик
return $event->getReturnValue();
}
}

Теперь создадим класс который будет хранить обработчик:

class Bar
{
public function addBarMethodToFoo(sfEvent $event)
{
// мы только хотим ответить на вызовы метода 'bar'
if ('bar' != $event['method'])
{
 // позволяем другому обработчику позаботится об этом неизвестном методе
 return false;
}

// объект контекста создания события (экземпляр класса foo)
$foo = $event->getSubject();

// аргументы метода bar
// от себя замечу, в документации может быть ошибка,
// у меня заработало так: 
// $arguments = $event['arguments'];
$arguments = $event['parameters'];

// делаем что-либо
// ...

// устанавливаем возвращаемое значение
$event->setReturnValue($someValue);

// сообщаем миру что мы обработали событие
return true;
}
}

В конце концов, добавляем новый метод bar к классу Foo:

$dispatcher->connect('foo.method_is_not_found', array($bar, 'addBarMethodToFoo'));

 

Модифицируем аргументы

Если вы хотите позволить внешним классам модифицировать аргументы, передаваемые методу сразу перед его выполнением, добавьте событие фильтра в начало метода:

class Foo
{
// ...

public function render($template, $arguments = array())
{
// фильтруем аргументы
$event = new sfEvent($this, 'foo.filter_arguments');
// от себя: в тестах я писал так:
// $this->dispatcher->connect('foo.filter_arguments', array(new Bar(), 'filterFooArguments'));
$this->dispatcher->filter($event, $arguments);

// получаем отфильтрованные аргументы
$arguments = $event->getReturnValue();

// ... здесь начинается метод
// от себя: ну например
// return $arguments;
}
}

В роли фильтра может служить такой класс:

class Bar {
public function filterFooArguments(sfEvent $event, $arguments) {
// от себя: для того чтоб увидеть изменения
// $arguments = array('33', '44');
$arguments['processed'] = true;
return $arguments;
}
}

Вот пока что все. Далее хотелось бы написать про то, чем отличаются новые версии компонент на PHP 5.3.2 от их стабильных аналогов на PHP 5.2. Или же начну писать про компонент Dependency Injection. Еще, я думаю, было бы интересно написать про то как Sensio Labs тестирует код (например, тех же Components при помощи PHPUnit). До новых встреч.