- была нужна возможность порождать события по шаблону;
- интерфейс библиотеки визуально не понравился.
Конечно же, я принял решение доделать и причесать библиотеку «под себя».
Порождение событий по шаблону
Мне нужна была возможность с помощью шаблона порождать нужные события, имена которых представляют собой иерархические ключи (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 Yowsa\Eventable; 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 Yowsa\Eventable; class KeysResolver { public static function isValidKey($key) { return preg_match('/^(([\w\d\-]+)\.?)+[^\.]$/', $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 Yowsa\Eventable\Dispatcher(); $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 pee\n"; }); // trigger events echo "{$teacher} is entering the classroom.\n\n"; $dispatcher->trigger('teacher.comes', [$teacher]); echo "\n\n{$teacher} welcomes everyone personally\n\n"; $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
Возможно полезные ссылки
Вдохновился:
Получилось:
ссылка на оригинал статьи http://habrahabr.ru/post/202234/
Добавить комментарий