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:
  • 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.event
  • another.event
  • yet.another.event
  • something.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 methodonce()

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
  • **.error
  • application.user.*.error
  • application.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:

Also popular now: