Event handling with Titon\Event

Event-driven programming is a popular paradigm that is used in nearly every application. It allows for the manipulation or alteration of an applications process flow. Titon provides a very light-weight and robust event system through the aptly named Event package, which is based off of the observer pattern and requires PHP 5.3. Using the package however, is rather straight forward.

Registering observers

Before an event can be emitted (or dispatched depending on your background), an observer must be registered. In Titon, there are 2 kinds of observers: callbacks which utilize closures or callable references, or listeners which are instances of a class object. Two different approaches that generate the same result in the end. The register() and registerListener() methods can be used to register observers.

The register() method accepts 3 arguments: $event, $callback, and an array of $options (optional). The name of the event is usually a unique dot notated string that represents its purpose. The callback can either by an instance of a closure, an array that contains an object instance and a method name, or the name of a global function. Read up on the callable type for more examples.

use Titon\Event\Event;
use Titon\Event\Emitter;
// Create an emitter instance
$event = new Emitter();
// Register a closure
$event->register('app.beforeDispatch', function(Event $event) {
	// Execute some logic
});
// Or register a callback
$event->register('app.afterDispatch', [$object, 'methodName']);

The third argument accepts an array of options or an integer. Passing an integer is a shortcut for setting the event priority level. The following options are available.

  • priority (int:100) - The order in which observers will be emitted in ascending order
  • overwrite (bool:false) - Overwrite an observer with the same priority level, else shift down
  • once (bool:false) - Emit the observer once and then remove itself from the list
  • exit (bool:false) - Automatically stop the emit process if an observer returns a falsey value
$event->register('app.run', [$foo, 'methodName'], 50); // Register with a priority of 50
$event->register('app.run', [$bar, 'methodName'], 50); // Will shift to 51 since overwrite is false
// Supply multiple options
$event->register('app.run', [$baz, 'methodName'], [
	'priority' => 50,
	'overwrite' => true,
	'once' => true,
	'exit' => true
]);

Registering listeners is slightly different...

Handling listeners

Listeners are class instances that implement the Titon\Event\Listener interface, which in turn registers multiple observers for events when the class instance itself is registered. The interface provides a single method, registerEvents(), which must return a mapping of event to callbacks.

When returning an array, the array key must be the name of an event, and the value should be an array of options or a method name. The available options can be found above for register(). Each event can support a single callback, or multiple callbacks. The following example demonstrates all the possible combinations.

use Titon\Event\Event;
use Titon\Event\Listener;
class Foo implements Listener {
	public function registerEvents() {
		return [
			// Register a single method with no options
			'app.beforeDispatch' => 'beforeDispatch',
			// Register a single method with options
			'app.afterDispatch' => ['method' => 'afterDispatch', 'priority' => 30],
			// Register multiple methods with and without options
			'app.run' => [
				'run',
				['method' => 'runOnce', 'once' => true, 'exit' => true]
			]
		];
	}
	// Define the methods being registered.
	public function beforeDispatch(Event $event) {}
	public function afterDispatch(Event $event) {}
	public function run(Event $event) {}
	public function runOnce(Event $event) {}
}

Once the interface has been implemented, the class object can be registered in the Emitter.

$event->registerListener($listener);
Removing observers

Removing a registered observer can be quite tricky, as the original callback must also be used to remove it. When removing a closure, the same instance closure must be used. When removing a callable, the same reference must be used. When removing a listener, the same class instance must be used. The following example demonstrates this.

$closure = function(Event $e) { };
$event->register('event.foo', $closure);
$event->remove('event.foo', $closure); // Same reference
$event->register('event.foo', [$object, 'method']);
$event->remove('event.foo', [$object, 'method']);
$listener = new Foo();
$event->registerListener($listener);
$event->removeListener($listener); // Same reference
Convenience methods

Similar to popular JavaScript frameworks, a few convenience methods exist for observer registering and removing, they are on(), off(), and once(). The on and off methods accept either a callable or a listener, allowing for easier integration. The argument list for these methods is exactly the same as their counterparts.

// Register
$event->on('event.foo', [$object, 'method']);
$event->on('event.foo', $listener);
// Register once
$event->once('event.foo', function(Event $e) {});
// Remove
$event->off('event.foo', [$object, 'method']);
$event->off('event.foo', $listener);
Emitting events

Once observers have been registered, an event can be emitted to loop through and execute its observers. This can be done through the emit() method. The response when emitting will either be a Titon\Event\Event object, or an array of objects, depending on the event scope.

$response = $event->emit('event.foo');

An array of parameters can be supplied as a second argument to emit(), which will be passed on as arguments to the observers. Parameters can also be passed by reference allowing observers to modify data.

$response = $event->emit('event.foo', [&$data, $flag]);
// Available as arguments in the observer callback
function(Event $event, array &$data, $flag) {
	$data['foo'] = 'bar';
}

Multiple events can also be emitted when separating names with a space.

$responses = $event->emit('event.foo event.bar');

Or multiple events can be emitted by using a wildcard. This will resolve all event names that fall under that path.

$responses = $event->emit('event.*');
The Event

When an event is emitted, a Titon\Event\Event object is created, passed through all the observers, and finally returned as a response. The object represents the current state of the event and provides functionality for managing that state — the most popular being the stopping of propagation.

When the propagation is stopped, the event will exit out of the observer loop and cease processing. This can be done by calling stop() on the event object within an observer.

function (Event $event) {
	$event->stop();
}

Communication between observers can also be quite complicated (or never implemented). Titon provides a way of persisting data through the event object using setData() and getData(). This allows for an observer to set data at the beginning of the loop, which in turn can be used by another observer at the end of a loop.

function (Event $event) {
	$event->setData(['foo' => 'bar']);
}
function (Event $event) {
	$data = $event->getData();
}

Data can also be set by returning a value from the observer.

function (Event $event) {
	return ['foo' => 'bar'];
}

There are also times when the priority order of the loop is uncertain. The getCallstack() method can be called to return an array of all observers in order of priority. The getIndex() method returns that position in the loop (not the priority level), and the getTime() method returns the timestamp of when the event started.

Emittable trait

As mentioned above, the package itself requires PHP 5.3. However, there is a PHP 5.4 trait available, Titon\Event\Traits\Emittable, that provides an event managed layer within a class. This allows events to be registered and emitted in context to that class; very useful for classes that require callback hooks. The trait provides the following methods: getEmitter(), setEmitter(), on(), off(), and emit().

use Titon\Event\Traits\Emittable;
class Foo {
	use Emittable;
	public function doSomething() {
		$this->emit('foo.do');
	}
}
$foo = new Foo();
$foo->on('foo.do', [$object, 'method']);
$foo->doSomething();
In closing

The Titon\Event package is very powerful and provides additional functionality that other event dispatching libraries do not provide. Some of these features include:

  • Multiple event resolving
  • Wildcard event resolving
  • Event data persistence through the object or observer return
  • Automatic propagation stopping through the exit option
  • Observer once execution through the once option
  • Observer call stack generation
  • Observer priority overwriting
  • Class layer event management through Emittable trait
  • And many more awesome features

So give it a whirl and let me know what you think!

Restricting an input field to numeric only

I was always amazed that a feature like this wasn't available in HTML 4 (but is in HTML 5). It would be really cool to restrict an input field to only accept numeric characters (or alphabetic), without the need for JavaScript. But I digress as I had to use JavaScript anyways. I first tried to steer away from using an event, but found it difficult to not allow the letter without it showing up in the input field first, or finding it too strange to regex the non-numeric characters out. So I caved in and used the event handlers. Here's a quick JavaScript snippet that will make an input field only accept numeric characters, a backspace and a tab.

function numericOnly(event) {
	var key = window.event.keyCode || event.keyCode;
	return ((key >= 48 && key <= 57) || (key >= 96 && key <= 105) || (key == 8) || (key == 9));
}

The function will accept the 0-9 range of numbers and all numbers on the numlock keypad. However, this will not work in all locales since some keyboard layouts are different but will work on all basic QWERTY keyboards. Finally, to implement the function, you would use onkeydown, as it checks the event before the character shows up (unlike onkeyup or onkeypress).

<input name="field" onkeydown="return numericOnly(event);" />

There are probably better and more thought out alternatives, so if you have one please post it in the comments. Would be nice to get multiple variations.