Диспетчер событий с фильтрацией по шаблону

от автора

Недавно у меня появилась необходимость в простом и функциональном диспетчере событий. После непродолжительных поисков на 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()

Dispatcher.php

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 ]) } 

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

KeysResolver.php

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *