Template Filtering Event Manager
Recently, I needed a simple and functional event dispatcher. After a brief search on the Packagist, I found the Evenement package , which almost completely fit my requirements. But still, he did not pass the selection because of two parameters:
Of course, I decided to finish and comb the library “for myself”.
I needed the ability to use the template to generate the necessary events whose names are hierarchical keys (
For example, for such a list of events:
You need to spawn all events ending in "event". Or starting with "yet" and ending with "event", and it doesn’t matter what is in the middle.
After a little thought, I set about implementing the library, based on the previously found Evenement.
Thinking on the interface, I looked at jQuery and its working methods to the events:
The result is the following interface:
So, a method
If the handler must be executed once, it is hung on the event by the method
Half the work is done - the dispatcher is implemented and working. The next step is to add event filtering by template.
The templates are all the same keys, but with labels for filtering:
For the key,
To implement this filtering, three methods were needed:
Nothing military: key validation, converting a pattern to a regular expression, and filtering a key array.
The whole package fits in two simple classes, it is easy to test and composer-package designed.
To demonstrate how Eventable works and in which cases it can be useful, here is a simple example.
Inspired by:
Happened:
- I needed the ability to generate events by pattern;
- the library interface was not visually liked.
Of course, I decided to finish and comb the library “for myself”.
Event generation by pattern
I needed the ability to use the template to generate the necessary events whose names are hierarchical keys (
foo.bar.baz). For example, for such a list of events:
some.eventanother.eventyet.another.eventsomething.new
You need to spawn all events ending in "event". Or starting with "yet" and ending with "event", and it doesn’t matter what is in the middle.
Eventable
After a little thought, I set about implementing the library, based on the previously found Evenement.
Event dispatcher
Thinking on the interface, I looked at jQuery and its working methods to the events:
on(), one(), off(), trigger(). I liked this approach for the most part because of its brevity and conciseness. The result is the following interface:
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 ])
}
So, a method
off()can take two parameters, and then a specific handler for the specified event will be deleted. One parameter - in this case, all event handlers will be deleted. Or do not accept any parameters, which means deleting all events and handlers subscribed to them. trigger()accepts an event key pattern, and raises all matching events. fire()in turn, generates one, specifically given event. If the handler must be executed once, it is hung on the event by the method
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;
}
}
Parsing keys
Half the work is done - the dispatcher is implemented and working. The next step is to add event filtering by template.
The templates are all the same keys, but with labels for filtering:
*- one segment, before the separator;**- any number of segments.
For the key,
application.user.signin.erroryou can make the following correct patterns:application.**.error**.errorapplication.user.*.errorapplication.user.**
To implement this filtering, three methods were needed:
KeysResolver {
public static int isValidKey(string $key)
public static string getKeyRegexPattern(string $key)
public static mixed filterKeys(string $pattern [, array $keys ])
}
Nothing military: key validation, converting a pattern to a regular expression, and filtering a key array.
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);
}
}
The whole package fits in two simple classes, it is easy to test and composer-package designed.
Does it work
To demonstrate how Eventable works and in which cases it can be useful, here is a simple example.
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');
}
Result
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
Perhaps useful links
Inspired by:
Happened: