New in version 2.2.
There are several reasons to want a piece of code to be run before any controller code is executed or right before the response is sent to the client, such as response caching, header tuning, special authentication or just to provide access to a mission-critical API response in lesser time than a complete request dispatching cycle would take.
CakePHP provides for such cases a clean and extensible interface for attaching filters to this dispatching cycle, similar to a middleware layer thought to provide stackable services or routines for every request. We call them Dispatcher Filters
Filters are usually configured in the bootstrap.php
file, but you could easily
load them from any other configuration file before the request is dispatched.
Adding and removing filters is done through the Configure class, using the
special key Dispatcher.filters
. By default CakePHP comes with a couple filter
classes already enabled for all requests, let’s take a look at how they are
added:
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
'CacheDispatcher'
));
Each of those array values are class names that will be instantiated and added
as listeners for the events generated at dispatcher level. The first one,
AssetDispatcher
is meant to check whether the request is referring to a theme
or plugin asset file, such as a CSS, JavaScript or image stored on either a
plugin’s webroot folder or the corresponding one for a Theme. It will serve the
file accordingly if found, stopping the rest of the dispatching cycle. The CacheDispatcher
filter, when Cache.check
config variable is enabled, will check if the
response was already cached in the file system for a similar request and serve
the cached code immediately.
As you can see, both provided filters have the responsibility of stopping any further code and send the response right away to the client. But filters are not limited to this role, as we will show shortly in this section.
You can add your own class names to the list of filters, and they will get
executed in the order they were defined. There is also an alternative way for
attaching filters that do not involve the special DispatcherFilter
classes:
Configure::write('Dispatcher.filters', array(
'my-filter' => array(
'callable' => array($classInstance, 'methodName'),
'on' => 'after'
)
));
As shown above, you can pass any valid PHP callback
type, as you may remember, a callback is anything that PHP can execute with
call_user_func
. We do make a little exception, if a string is provided it will
be treated as a class name, not as a possible function name. This of course
gives the ability to PHP 5.3 users to attach anonymous functions as filters:
Configure::write('Dispatcher.filters', array(
'my-filter' => array('callable' => function($event) {...}, 'on' => 'before'),
//more filters here
));
The on
key only takes before
and after
as valid values, and evidently
means whether the filter should run before or after any controller code is
executed. Additionally to defining filters with the callable
key, you also
get the chance to define a priority for your filters, if none is specified then
a default of 10
is selected for you
As all filters will have default priority 10
, should you want to run a filter before
any other in the list, select lower priority numbers as needed:
Configure::write('Dispatcher.filters', array(
'my-filter' => array(
'callable' => function($event) {...},
'on' => 'before',
'priority' => 5
),
'other-filter' => array(
'callable' => array($class, 'method'),
'on' => 'after',
'priority' => 1
),
//more filters here
));
Obviously, when defining priorities the order in which filters are declared does not matter but for those having the same. When defining filters as class names there is no option to define priority in-line, we will get into that soon. Finally, CakePHP’s plugin notation can be used to define filters located in plugins:
Configure::write('Dispatcher.filters', array(
'MyPlugin.MyFilter',
));
Feel free to remove the default attached filters if you choose to use a more advanced/faster way of serving theme and plugin assets or if you do not wish to use built-in full page caching, or just implement your own.
If you need to pass constructor parameters or settings to you dispatch filter classes you can do that by providing an array of settings:
Configure::write('Dispatcher.filters', array(
'MyAssetFilter' => array('service' => 'google.com')
));
When the filter key is a valid classname, the value can be an array of
parameters that are passed to the dispatch filter. By default the base class
will assign these settings to the $settings
property after merging them with
the defaults in the class.
Changed in version 2.5: You can now provide constructor settings to dispatch filters in 2.5.
Dispatcher filters, when defined as class names in configuration, should extend
the class DispatcherFilter
provided in the Routing CakePHP’s directory.
Let’s create a simple filter to respond to a specific URL with a ‘Hello World’
text:
App::uses('DispatcherFilter', 'Routing');
class HelloWorldFilter extends DispatcherFilter {
public $priority = 9;
public function beforeDispatch(CakeEvent $event) {
$request = $event->data['request'];
$response = $event->data['response'];
if ($request->url === 'hello-world') {
$response->body('Hello World');
$event->stopPropagation();
return $response;
}
}
}
This class should be saved in a file in app/Routing/Filter/HelloWorldFilter.php
and configured in the bootstrap file according to how it was explained in the
previous section. There is plenty to explain here, let’s begin with the
$priority
value.
As mentioned before, when using filter classes you can only define the order in
which they are run using the $priority
property in the class, default value is
10 if the property is declared, this means that it will get executed _after_ the
Router class has parsed the request. We do not want this to happen in our
previous example, because most probably you do not have any controller set up
for answering to that URL, hence we chose 9 as our priority.
DispatcherFilter
exposes two methods that can be overridden in subclasses,
they are beforeDispatch
and afterDispatch
, and are executed before or after
any controller is executed respectively. Both methods receive a CakeEvent
object containing the request
and response
objects
(CakeRequest
and CakeResponse
instances) along with an
additionalParams
array inside the data
property. The latter contains
information used for internal dispatching when calling requestAction
.
In our example we conditionally returned the $response
object as a result,
this will tell the Dispatcher to not instantiate any controller and return such
object as response immediately to the client. We also added
$event->stopPropagation()
to prevent other filters from being executed after
this one.
Let’s now create another filter for altering response headers in any public
page, in our case it would be anything served from the PagesController
:
App::uses('DispatcherFilter', 'Routing');
class HttpCacheFilter extends DispatcherFilter {
public function afterDispatch(CakeEvent $event) {
$request = $event->data['request'];
$response = $event->data['response'];
if ($request->params['controller'] !== 'pages') {
return;
}
if ($response->statusCode() === 200) {
$response->sharable(true);
$response->expires(strtotime('+1 day'));
}
}
}
This filter will send a expiration header to 1 day in the future for
all responses produced by the pages controller. You could of course do the same
in the controller, this is just an example of what could be done with filters.
For instance, instead of altering the response you could cache it using the
Cache
class and serve the response from the beforeDispatch
callback.
Our last example will use an anonymous function (only available on PHP 5.3+) to
serve a list of posts in JSON format, we encourage you to do so using
controllers and the JsonView
class, but let’s imagine you need to save a
few milliseconds for this mission-critical API endpoint:
$postsList = function($event) {
if ($event->data['request']->url !== 'posts/recent.json') {
return;
}
App::uses('ClassRegistry', 'Utility');
$postModel = ClassRegistry::init('Post');
$event->data['response']->body(json_encode($postModel->find('recent')));
$event->stopPropagation();
return $event->data['response'];
};
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
'CacheDispatcher',
'recent-posts' => array(
'callable' => $postsList,
'priority' => 9,
'on'=> 'before'
)
));
In previous example we have selected a priority of 9
for our filter, so to skip
any other logic either placed in custom or core filters such as CakePHP internal
routing system. Although it is not required, it shows how to make your important
code run first in case you need to trim as much fat as possible from some requests.
For obvious reasons this has the potential of making your app very difficult to maintain. Filters are an extremely powerful tool when used wisely, adding response handlers for each URL in your app is not a good use for it. But if you got a valid reason to do so, then you have a clean solution at hand. Keep in mind that not everything needs to be a filter, Controllers and Components are usually a more accurate choice for adding any request handling code to your app.