On this page
Intercepting Filters
Intercepting filters are a design pattern used for providing mechanisms to alter the workflow of an application. Implementing them provides a way to have a standard public interface, with the ability to attach arbitrary numbers of filters that will take the incoming arguments in order to alter the workflow.
laminas-eventmanager provides an intercepting filter implementation via
Laminas\EventManager\FilterChain
.
Preparation
To use the FilterChain
implementation, you will need to install laminas-stdlib,
if you have not already:
$ composer require laminas/laminas-stdlib
FilterChainInterface
Laminas\EventManager\FilterChain
is a concrete implementation of
Laminas\EventManager\Filter\FilterInterface
, which defines a workflow for
intercepting filters. This includes the following methods:
interface FilterInterface
{
public function run($context, array $params = []);
public function attach(callable $callback);
public function detach(callable $callback);
public function getFilters();
public function clearFilters();
public function getResponses();
}
In many ways, it's very similar to the EventManagerInterface
, but with a few
key differences:
- A filter essentially defines a single event, which obviates the need for
attaching to multiple events. As such, you pass the target and parameters only
when "triggering" (
run()
) a filter. - Instead of passing an
EventInterface
to each attached filter, aFilterInterface
implementation will pass:- The
$context
- The
$params
- A
FilterIterator
, to allow the listener to call on the next filter.
- The
FilterIterator
When executing run()
, a FilterInterface
implementation is expected to
provide the stack of attached filters to each listener. This stack will
typically be a Laminas\EventManager\Filter\FilterIterator
instance.
FilterIterator
extends Laminas\Stdlib\FastPriorityQueue
, and, as such, is
iterable, and provides the method next()
for advancing the queue.
As such, a listener should decide if more processing is necessary, and, if so,
call on $chain->next()
, passing the same set of arguments.
Filters
A filter attached to a FilterChain
instance can be any callable. However,
these callables should expect the following arguments:
function ($context, array $argv, FilterIterator $chain)
A filter can therefore act on the provided $context
, using the provided
arguments.
Part of that execution can also be deciding that other filters should be called.
To do so, it will call $chain->next()
, providing it the same arguments:
function ($context, array $argv, FilterIterator $chain)
{
$message = isset($argv['message']) ? $argv['message'] : '';
$message = str_rot13($message);
$filtered = $chain->next($context, ['message' => $message], $chain);
return str_rot13($filtered);
}
You can choose to call $chain->next()
at any point in the filter, allowing you
to:
- pre-process arguments and/or alter the state of the
$context
. - post-process results and/or alter the state of the
$context
based on the results. - skip processing entirely if criteria is not met (e.g., missing arguments,
invalid
$context
state). - short-circuit the chain if no processing is necessary (e.g., a cache hit is detected).
Execution
When executing a filter chain, you will provide the $context
, which is usually
the object under observation, and arguments, which are typically the arguments
passed to the method triggering the filter chain.
As an example, consider the following filter-enabled class:
use Laminas\EventManager\FilterChain;
class ObservedTarget
{
private $filters = [];
public function attachFilter($method, callable $listener)
{
if (! method_exists($this, $method)) {
throw new \InvalidArgumentException('Invalid method');
}
$this->getFilters($method)->attach($listener);
}
public function execute($message)
{
return $this->getFilters(__FUNCTION__)
->run($this, compact('message'));
}
private function getFilters($method)
{
if (! isset($this->filters[$method])) {
$this->filters[$method] = new FilterChain();
}
return $this->filters[$method];
}
}
Now, let's create an instance of the class, and attach some filters to it.
$observed = new ObservedTarget();
$observed->attach(function ($context, array $args, FilterIterator $chain) {
$args['message'] = isset($args['message'])
? strtoupper($args['message'])
: '';
return $chain->next($context, $args, $chain);
});
$observed->attach(function ($context, array $args, FilterIterator $chain) {
return (isset($args['message'])
? str_rot13($args['message'])
: '');
});
$observed->attach(function ($context, array $args, FilterIterator $chain) {
return (isset($args['message'])
? strtolower($args['message'])
: '');
});
Finally, we'll call the method, and see what results we get:
$observed->execute('Hello, world!');
Since filters are run in the order in which they are attached, the following will occur:
- The first filter will transform our message into
HELLO, WORLD!
, and then call on the next filter. - The second filter will apply a ROT13 transformation on the string and return
it:
!DLROW ,OLLEH
.
Because the second filter does not call $chain->next()
, the third filter never
executes.
Notes
We recommend using the construct run($this, compact(method argument names)
when invoking a FilterChain
. This makes the argument keys predictable inside
filters.
We also recommend putting the default logic for the method invoking the filter chain in a filter itself, and attaching it at invocation. This allows intercepting filters to replace the main logic, while still providing a default path. This might look like:
// Assume that the class contains the `attachFilter()` implementation from above.
class ObservedTarget
{
private $attached = [];
public function execute($message)
{
if (! isset($this->attached[__FUNCTION__])) {
$this->attachFilter(__FUNCTION__, $this->getExecuteFilter();
}
return $this->getFilters(__FUNCTION__)
->run($this, compact('message'));
}
private function getExecuteFilter()
{
$this->attached['execute'] = true;
return function ($context, array $args, FilterIterator $chain) {
return $args['message'];
};
}
}
Intercepting filters are a powerful way to introduce aspect-oriented programming paradigms into your code, as well as general-purpose mechanisms for introducing plugins.