Events
The icanboogie/event package allows you to provide hooks which other developers can attach to, to be notified when certain events occur inside the application and take action.
Inside ICanBoogie, events are often used to alter initial parameters, take action before/after an operation is processed or when it fails, take action before/after a request is dispatched or to rescue an exception.
A twist on the Observer pattern
The pattern used by the API is similar to the Observer pattern, although instead of attaching event hooks to objects they are attached to their class. When an event is fired upon a target object, the hierarchy of its class is used to filter event hooks.
Consider the following class hierarchy:
ICanBoogie\Operation
└─ ICanBoogie\Module\Operation\SaveOperation
└─ Icybee\Modules\Node\Operation\SaveOperation
└─ Icybee\Modules\Content\Operation\SaveOperation
└─ Icybee\Modules\News\Operation\SaveOperation
When the process
event is fired upon a …\News\Operation\SaveOperation
instance, all event
hooks attached to the classes for this event are called, starting from the event hooks attached
to the instance class (…\News\Operation\SaveOperation
) all the way up to those attached
to its root class.
Thus, event hooks attached to the …\Node\Operation\SaveOperation
class are called
when the process
event is fired upon a …\News\Operation\SaveOperation
instance. One could
consider that event hooks are inherited.
Getting started
To be emitted, events need an event collection, which holds event hooks. Because a new event collection is created for you when required, you don't need to setup one yourself. Still you might want to do so if you have a bunch of event hooks that you need to attach while creating the event collection. To do so, you need to define a provider that will return your event collection when required.
The following example demonstrates how to setup a provider that instantiates an event collection with event hooks provided by an application configuration:
<?php
use ICanBoogie\EventCollection;
use ICanBoogie\EventCollectionProvider;
/* @var $app */
EventCollectionProvider::define(function() use ($app) {
static $collection;
return $collection ?: $collection = new EventCollection($app->configs['event']);
});
# Getting the event collection
$events = EventCollectionProvider::provide();
# or
$events = \ICanBoogie\get_events();
Typed events
An instance of an Event subclass is used to provide contextual information about an event to the event hooks processing it. It is passed as the first argument, with the target object as second argument (if any). This instance contain information directly relating to the type of event they accompany.
For example, a process
event is usually instantiated from a ProcessEvent
class, and a
process:before
event—fired before a process
event—is usually instantiated
from a BeforeProcessEvent
instance.
The following code demonstrates how a ProcessEvent
class may be defined for a process
event type:
<?php
namespace ICanBoogie\Operation;
use ICanBoogie\Event;
use ICanBoogie\HTTP\Response;
use ICanBoogie\HTTP\Request;
use ICanBoogie\Operation;
/**
* Event class for the `ICanBoogie\Operation::process` event.
*
* @property mixed $rc
* @property-read Response $response
* @property-read Request $request
*/
class ProcessEvent extends Event
{
/**
* Reference to the response result property.
*
* @var mixed
*/
private $rc;
protected function get_rc()
{
return $this->rc;
}
protected function set_rc($rc)
{
$this->rc = $rc;
}
/**
* The response object of the operation.
*
* @var Response
*/
private $response;
protected function get_response()
{
return $this->response;
}
/**
* The request that triggered the operation.
*
* @var Request
*/
private $request;
protected function get_request()
{
return $this->request;
}
/**
* The event is constructed with the type `process`.
*
* @param Operation $target
* @param Request $request
* @param Response $response
* @param mixed $rc
*/
public function __construct(Operation $target, Request $request, Response $response, &$rc)
{
$this->request = $request;
$this->response = $response;
$this->rc = &$rc;
parent::__construct($target, 'process');
}
}
Event types
The event type is usually the name of an associated method. For example, the process
event
type is fired after the ICanBoogie\Operation::process
method was called, and the process:before
event type is fired before.
Namespacing and naming
Event classes should be defined in a namespace unique to their target object. Events
targeting ICanBoogie\Operation
instances should be defined in the ICanBoogie\Operation
namespace.
The class name should match the event type. ProcessEvent
for the process
event type,
BeforeProcessEvent
for the process:before
event.
Firing events
Events are fired as they are instantiated.
The following example demonstrates how the process
event is fired upon an
ICanBoogie\Operation
instance:
<?php
namespace ICanBoogie;
class Operation
{
// …
public function __invoke()
{
// …
$response->rc = $this->process();
new Operation\ProcessEvent($this, $request, $response, $response->rc);
// …
}
// …
}
Attaching event hooks
Attaching event hooks using the events
config
The recommended way to define event hooks is through events
configuration fragments, in part
because the configuration can be cached.
The following example demonstrates how an application can attach event hooks to be notified when nodes are saved (or nodes subclasses), and when an authentication exception is thrown during the dispatch of a request.
<?php
// config/event.php
namespace App;
use ICanBoogie;
use Icybee;
$hooks = Hooks::class . '::';
return [
Icybee\Modules\Nodes\SaveOperation::class . '::process' => $hooks . 'on_nodes_save',
ICanBoogie\HTTP\AuthenticationRequired::class . '::rescue' => $hooks . 'on_authentication_required_rescue'
];
The following example demonstrates how to obtain the event
config and the event collection.
<?php
namespace ICanBoogie;
require 'vendor/autoload.php';
$app = boot();
$app->configs['event']; // obtain the "event" config.
$app->events; // obtain an EventCollection instance created with the "event" config.
Attaching events after the application has booted
After the application has booted, event hooks can be attached using the attach()
method of the
event collection. The method is smart enough to create the event type from the parameters type. This
works with any callable: closure, invokable objects, static class methods, functions.
The following example demonstrates how a closure may be attached to a ICanBoogie\Operation::process:before
event type.
<?php
use ICanBoogie\Operation;
/* @var $events \ICanBoogie\EventCollection */
$events->attach(function(Operation\BeforeProcessEvent $event, Operation $target) {
// …
});
The following example demonstrates how an invokable object may be attached to that same event type.
<?php
class ValidateOperation
{
private $rules;
public function __construct(array $rules)
{
$this->rules = $rules;
}
public function __invoke(Operation\BeforeProcessEvent $event, Operation $target)
{
// …
}
}
// …
/* @var $events \ICanBoogie\EventCollection */
/* @var $rules array */
$events->attach(new ValidateOperation($rules));
Attaching an event hook to a specific target
Using the attach_to()
method, an event hook can be attached to a specific target, and is only
invoked for that target.
<?php
use ICanBoogie\Routing\Controller;
// …
/* @var $events \ICanBoogie\EventCollection */
$events->attach_to($controller, function(Controller\ActionEvent $event, Controller $target) {
echo "invoked!";
});
$controller_clone = clone $controller;
new Controller\ActionEvent($controller_clone, …); // nothing happens
new Controller\ActionEvent($controller, …); // echo "invoked!"
Attaching a one time event hook
The once()
method attaches event hooks that are automatically detached after they have been used.
<?php
use ICanBoogie\Event;
$n = 0;
/* @var $events \ICanBoogie\EventCollection */
$events->once('flash', function() use(&$n) {
$n++;
});
new Event(null, 'flash');
new Event(null, 'flash');
new Event(null, 'flash');
echo $n; // 1
Attaching event hooks to the finish chain
The finish chain is executed after the event chain was traversed without being stopped.
The following example demonstrates how an event hook may be attached to the finish chain of
the count
event to obtain the string "0123". If the third event hook was defined like the
others we would obtain "0312".
<?php
class CountEvent extends \ICanBoogie\Event
{
public $count;
public function __construct($count)
{
$this->count = $count;
parent::__construct(null, 'count');
}
}
/* @var $events \ICanBoogie\EventCollection */
$events->attach('count', function(CountEvent $event) {
$event->count .= 2;
});
$events->attach('count', function(CountEvent $event) {
$event->count .= 1;
});
$events->attach('count', function(CountEvent $event) {
$event->chain(function(CountEvent $event) {
$event->count .= 3;
});
});
$event = new CountEvent(0);
echo $event->count; // 0123
Breaking an event hook chain
The processing of an event hook chain can be broken by an event hook using the stop()
method:
<?php
use ICanBoogie\Operation;
function on_event(Operation\ProcessEvent $event, Operation $operation)
{
$event->rc = true;
$event->stop();
}
Instantiating non-firing events
Events are designed to be fired as they are instantiated, but sometimes you want to be able to create an Event instance without it to be fired immediately, for instance when you need to test that event, or alter it before it is fired.
The from()
method creates non-firing event instances from an array of parameters.
The following example demonstrates how to create an non-firing instance of the
ProcessEvent
class we saw earlier:
<?php
use ICanBoogie\Operation\ProcessEvent;
use ICanBoogie\EventReflection;
$rc = null;
// …
$event = ProcessEvent::from([
'target' => $operation,
'request' => $request,
'response' => $response,
'rc' => &$rc
]);
$event->rc = "ABBA";
echo $rc; // ABBA
Array keys must match construct arguments, an exception will fire otherwise. Also, if a constructor argument must be passed by reference keep in mind that it must be passed by reference in the array as well.
The event can later be fired using the fire()
method:
<?php
/* @var $event \ICanBoogie\Event */
$event->fire();
Profiling events
The EventProfiler class is used to collect timing information about unused events and event hook calls. All time information is measured in floating microtime.
<?php
use ICanBoogie\EventProfiler;
foreach (EventProfiler::$unused as list($time, $type))
{
// …
}
foreach (EventProfiler::$calls as list($time, $type, $hook, $started_at))
{
// …
}
Helpers
get_events()
: Returns the current event collection. If the event collection provider is defined the method defines one that provides a new EventCollection instance.
Bindings
The icanboogie/bind-event package binds icanboogie/event to ICanBoogie, using its
Autoconfig feature. It provides a config synthesizer for event hooks defined in event
configuration fragments, and an events
getter for Application instances.
<?php
namespace ICanBoogie;
require 'vendor/autoload.php';
$app = boot();
$app->configs['event']; // obtain the "event" config.
$app->events; // obtain an EventCollection instance created with the "event" config.
Attaching event hooks using the event
config
The event
config can be used to define event hooks.
The following example demonstrates how an application can attach event hooks to be notified when nodes are saved (or nodes subclasses), and when an authentication exception is thrown during the dispatch of a request.
<?php
// config/event.php
namespace App;
$hooks = Hooks::class . '::';
return [
\App\Modules\Articles\SaveOperation::class . '::process' => $hooks . 'on_articles_save',
\ICanBoogie\HTTP\AuthenticationRequired::class . '::rescue' => $hooks . 'on_authentication_required_rescue'
];