Недавно у меня появилась необходимость в простом и функциональном диспетчере событий. После непродолжительных поисков на Packagist-е я нашел пакет Evenement, который почти полностью подходил под мои требования. Но все же отбор он не прошел из-за двух параметров:
- была нужна возможность порождать события по шаблону;
- интерфейс библиотеки визуально не понравился.
Конечно же, я принял решение доделать и причесать библиотеку «под себя».
Порождение событий по шаблону
Мне нужна была возможность с помощью шаблона порождать нужные события, имена которых представляют собой иерархические ключи (foo.bar.baz
).
Например, для такого списка событий:
some.event
another.event
yet.another.event
something.new
Нужно породить все события, заканчивающиеся на «event». Или начинающиеся на «yet» и заканчивающиеся на «event», и не важно, что в середине.
Eventable
После небольших размышлений я принялся к реализации библиотеки, основываясь на ранее найденном Evenement.
Диспетчер событий
Думая над интерфейсом, я поглядывал на jQuery и его методы работы с событиями: on()
, one()
, off()
, trigger()
. Такой подход пришелся мне по душе по большей части из-за краткости и лаконичности.
В итоге получился следующий интерфейс:
Dispatcher {
public Dispatcher on(string $event, callable $listener)
public Dispatcher once(string $event, callable $listener)
public Dispatcher off([string $event [, callable $listener ]])
public Dispatcher trigger(string $event [, array $args ])
public Dispatcher fire(string $event [, array $args ])
}
Так, метод off()
может принимать два параметра, и тогда будет удален конкретный обработчик указанного события. Один параметр — в этом случае будут удалены все обработчики события. Или не принимать никаких параметров, что означает удаление всех событий и подписанных на них обработчиков.
trigger()
принимает шаблон ключа события, и порождает все подходящие события.
fire()
в свою очередь порождает одно, конкретно заданное событие.
Если обработчик должен быть выполнен единожды, он вешается на событие методом once()
namespace YowsaEventable;
class Dispatcher
{
protected $events = [];
public function on($event, callable $listener)
{
if (!KeysResolver::isValidKey($event)) {
throw new InvalidArgumentException('Invalid event name given');
}
if (!isset($this->events[$event])) {
$this->events[$event] = [];
}
$this->events[$event][] = $listener;
return $this;
}
public function once($event, callable $listener)
{
$onceClosure = function () use (&$onceClosure, $event, $listener) {
$this->off($event, $onceClosure);
call_user_func_array($listener, func_get_args());
};
$this->on($event, $onceClosure);
return $this;
}
public function off($event = null, callable $listener = null)
{
if (empty($event)) {
$this->events = [];
} elseif (empty($listener)) {
$this->events[$event] = [];
} elseif (!empty($this->events[$event])) {
$index = array_search($listener, $this->events[$event], true);
if (false !== $index) {
unset($this->events[$event][$index]);
}
}
return $this;
}
public function trigger($event, array $args = [])
{
$matchedEvents = KeysResolver::filterKeys($event, array_keys($this->events));
if (!empty($matchedEvents)) {
if (is_array($matchedEvents)) {
foreach ($matchedEvents as $eventName) {
$this->fire($eventName, $args);
}
} else {
$this->fire($matchedEvents, $args);
}
}
return $this;
}
public function fire($event, array $args = [])
{
foreach ($this->events[$event] as $listener) {
call_user_func_array($listener, $args);
}
return $this;
}
}
Разбор ключей
Половина работы сделана — диспетчер реализован и работает. Следующий шаг — добавить фильтрацию событий по шаблону.
Шаблоны представляют собой все те же ключи, но с метками для фильтрации:
*
— один сегмент, до разделителя;**
— любое количество сегментов.
Для ключа application.user.signin.error
можно составить такие корректные шаблоны:
application.**.error
**.error
application.user.*.error
application.user.**
Для реализации такой фильтрации, понадобилось три метода:
KeysResolver {
public static int isValidKey(string $key)
public static string getKeyRegexPattern(string $key)
public static mixed filterKeys(string $pattern [, array $keys ])
}
Ничего военного: валидация ключа, преобразование шаблона в регулярное выражение и фильтрация массива ключей.
namespace YowsaEventable;
class KeysResolver
{
public static function isValidKey($key)
{
return preg_match('/^(([wd-]+).?)+[^.]$/', $key);
}
public static function getKeyRegexPattern($key)
{
$pattern = ('*' === $key)
? '([^.]+)'
: (('**' === $key)
? '(.*)'
: str_replace(
array('**', '*'),
array('(.+)', '([^.]*)'),
preg_quote($key)
)
);
return '/^' . $pattern . '$/i';
}
public static function filterKeys($pattern, array $keys = array())
{
$matched = preg_grep(self::getKeyRegexPattern($pattern), $keys);
if (empty($matched)) {
return null;
}
if (1 === count($matched)) {
return array_shift($matched);
}
return array_values($matched);
}
}
Весь пакет вмещается в два простых класса, легко тестируем и оформлен composer-пакетом.
Does it work
Для демонстрации, как Eventable работает и в каких случаях это может быть полезно, ниже приведен простой пример.
require_once __DIR__ . '/../vendor/autoload.php';
$dispatcher = new YowsaEventableDispatcher();
$teacher = 'Mrs. Teacher';
$children = ['Mildred', 'Nicholas', 'Kevin', 'Bobby', 'Anna',
'Kelly', 'Howard', 'Christopher', 'Maria', 'Alan'];
// teacher comes in the classroom
// and children welcome her once
$dispatcher->once('teacher.comes', function($teacher) use ($children) {
foreach ($children as $kid) {
printf("%-12s- Hello, %s!n", $kid, $teacher);
}
});
// every kid answers to teacher once
foreach ($children as $kid) {
$dispatcher->once("children.{$kid}.says", function() use ($kid) {
echo "Hi {$kid}!n";
});
}
// boddy cannot stop to talk
$dispatcher->on('children.Bobby.says', function() {
echo "tBobby: I want peen";
});
// trigger events
echo "{$teacher} is entering the classroom.nn";
$dispatcher->trigger('teacher.comes', [$teacher]);
echo "nn{$teacher} welcomes everyone personallynn";
$dispatcher->trigger('children.*.says');
for ($i = 0; $i < 5; $i++) {
$dispatcher->trigger('children.Bobby.says');
}
Mrs. Teacher is entering the classroom.
Mildred — Hello, Mrs. Teacher!
Nicholas — Hello, Mrs. Teacher!
Kevin — Hello, Mrs. Teacher!
Bobby — Hello, Mrs. Teacher!
Anna — Hello, Mrs. Teacher!
Kelly — Hello, Mrs. Teacher!
Howard — Hello, Mrs. Teacher!
Christopher — Hello, Mrs. Teacher!
Maria — Hello, Mrs. Teacher!
Alan — Hello, Mrs. Teacher!
Mrs. Teacher welcomes everyone personally
Hi Mildred!
Hi Nicholas!
Hi Kevin!
Hi Bobby!
Bobby: I want pee
Hi Anna!
Hi Kelly!
Hi Howard!
Hi Christopher!
Hi Maria!
Hi Alan!
Bobby: I want pee
Bobby: I want pee
Bobby: I want pee
Bobby: I want pee
Bobby: I want pee
Возможно полезные ссылки
Вдохновился:
Получилось:
Автор: balkon_smoke