The security process can be understood as a filter by which all requests must pass before executing the action. According to some tests executed in the filter, the processing of the request is modified--for instance, by changing the action executed (default/secure instead of the requested action in the case of the security filter). Symfony extends this idea to filter classes. You can specify any number of filter classes to be executed before the action execution or before the response rendering, and do this for every request. You can see filters as a way to package some code, similar to preExecute()
and postExecute()
, but at a higher level (for a whole application instead of for a whole module).
6.7.1. The Filter Chain
Symfony actually sees the processing of a request as a chain of filters. When a request is received by the framework, the first filter (which is always the sfRenderingFilter
) is executed. At some point, it calls the next filter in the chain, then the next, and so on. When the last filter (which is always sfExecutionFilter
) is executed, the previous filter can finish, and so on back to the rendering filter. Figure 6-3 illustrates this idea with a sequence diagram, using an artificially small filter chain (the real one contains more filters).
This process justifies the structure of the filter classes. They all extend the sfFilter
class, and contain one execute()
method, expecting a $filterChain
object as parameter. Somewhere in this method, the filter passes to the next filter in the chain by calling $filterChain->execute()
. See Listing 6-28 for an example. So basically, filters are divided into two parts:
- The code before the call to
$filterChain->execute()
executes before the action execution. - The code after the call to
$filterChain->execute()
executes after the action execution and before the rendering.
Listing 6-28 - Filter Class Struture
class myFilter extends sfFilter
{
public function execute ($filterChain)
{
// Code to execute before the action execution
...
// Execute next filter in the chain
$filterChain->execute();
// Code to execute after the action execution, before the rendering
...
}
}
The default filter chain is defined in an application configuration file called filters.yml
, and is shown in Listing 6-29. This file lists the filters that are to be executed for every request.
Listing 6-29 - Default Filter Chain, in frontend/config/filters.yml
rendering: ~
security: ~
# Generally, you will want to insert your own filters here
cache: ~
common: ~
execution: ~
These declarations have no parameter (the tilde character, ~
, means "null" in YAML), because they inherit the parameters defined in the symfony core. In the core, symfony defines class
and param
settings for each of these filters. For instance, Listing 6-30 shows the default parameters for the rendering
filter.
Listing 6-30 - Default Parameters of the rendering Filter, in $sf_symfony_lib_dir/config/config/filters.yml
rendering:
class: sfRenderingFilter # Filter class
param: # Filter parameters
type: rendering
By leaving the empty value (~
) in the application filters.yml
, you tell symfony to apply the filter with the default settings defined in the core.
You can customize the filter chain in various ways:
- Disable some filters from the chain by adding an
enabled: off
parameter. For instance, to disable thecommon
filter, which inserts CSS and JavaScript into the head, write:
common:
enabled: off
- Do not remove an entry from the
filters.yml
to disable a filter; symfony would throw an exception in this case. - Add your own declarations somewhere in the chain (usually after the
security
filter) to add a custom filter (as discussed in the next section). Be aware that therendering
filter must be the first entry, and theexecution
filter must be the last entry of the filter chain. - Override the default class and parameters of the default filters (notably to modify the security system and use your own security filter).
6.7.2. Building Your Own Filter
It is pretty simple to build a filter. Create a class definition similar to the one shown in Listing 6-28, and place it in one of the project's lib/ folders to take advantage of the autoloading feature.
As an action can forward or redirect to another action and consequently relaunch the full chain of filters, you might want to restrict the execution of your own filters to the first action call of the request. The isFirstCall()
method of the sfFilter
class returns a Boolean for this purpose. This call only makes sense before the action execution.
These concepts are clearer with an example. Listing 6-31 shows a filter used to auto-log users with a specific MyWebSite
cookie, which is supposedly created by the login action. It is a rudimentary but working way to implement the "remember me" feature offered in login forms.
Listing 6-31 - Sample Filter Class File, Saved in apps/frontend/lib/rememberFilter.class.php
class rememberFilter extends sfFilter
{
public function execute($filterChain)
{
// Execute this filter only once
if ($this->isFirstCall())
{
// Filters don't have direct access to the request and user objects.
// You will need to use the context object to get them
$request = $this->getContext()->getRequest();
$user = $this->getContext()->getUser();
if ($request->getCookie('MyWebSite'))
{
// sign in
$user->setAuthenticated(true);
}
}
// Execute next filter
$filterChain->execute();
}
}
In some cases, instead of continuing the filter chain execution, you will need to forward to a specific action at the end of a filter. sfFilter
doesn't have a forward()
method, but sfController
does, so you can simply do that by calling the following:
return $this->getContext()->getController()->forward('mymodule', 'myAction');
Note The sfFilter
class has an initialize()
method, executed when the filter object is created. You can override it in your custom filter if you need to deal with filter parameters (defined in filters.yml
, as described next) in your own way.
6.7.3. Filter Activation and Parameters
Creating a filter file is not enough to activate it. You need to add your filter to the filter chain, and for that, you must declare the filter class in the filters.yml
, located in the application or in the module config/
directory, as shown in Listing 6-32.
Listing 6-32 - Sample Filter Activation File, Saved in apps/frontend/config/filters.yml
rendering: ~
security: ~
remember: # Filters need a unique name
class: rememberFilter
param:
cookie_name: MyWebSite
condition: %APP_ENABLE_REMEMBER_ME%
cache: ~
common: ~
execution: ~
When activated, the filter is executed for each request. The filter configuration file can contain one or more parameter definitions under the param
key. The filter class has the ability to get the value of these parameters with the getParameter() method. Listing 6-33 demonstrates how to get a filter parameter value.
Listing 6-33 - Getting the Parameter Value, in apps/frontend/lib/rememberFilter.class.php
class rememberFilter extends sfFilter
{
public function execute ($filterChain)
{
// ...
if ($request->getCookie($this->getParameter('cookie_name')))
{
// ...
}
// ...
}
}
The condition
parameter is tested by the filter chain to see if the filter must be executed. So your filter declarations can rely on an application configuration, just like the one in Listing 6-32. The remember filter will be executed only if your application app.yml
shows this:
all:
enable_remember_me: on
6.7.4. Sample Filters
The filter feature is useful to repeat code for every action. For instance, if you use a distant analytics system, you probably need to put a code snippet calling a distant tracker script in every page. You could put this code in the global layout, but then it would be active for all of the application. Alternatively, you could place it in a filter, such as the one shown in Listing 6-34, and activate it on a per-module basis.
Listing 6-34 - Google Analytics Filter
class sfGoogleAnalyticsFilter extends sfFilter
{
public function execute($filterChain)
{
// Nothing to do before the action
$filterChain->execute();
// Decorate the response with the tracker code
$googleCode = '
<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
</script>
<script type="text/javascript">
_uacct="UA-'.$this->getParameter('google_id').'";urchinTracker();
</script>';
$response = $this->getContext()->getResponse();
$response->setContent(str_ireplace('</body>', $googleCode.'</body>',$response->getContent()));
}
}
Be aware that this filter is not perfect, as it should not add the tracker on responses that are not HTML.
Another example would be a filter that switches the request to SSL if it is not already, to secure the communication, as shown in Listing 6-35.
Listing 6-35 - Secure Communication Filter
class sfSecureFilter extends sfFilter
{
public function execute($filterChain)
{
$context = $this->getContext();
$request = $context->getRequest();
if (!$request->isSecure())
{
$secure_url = str_replace('http', 'https', $request->getUri());
return $context->getController()->redirect($secure_url);
// We don't continue the filter chain
}
else
{
// The request is already secure, so we can continue
$filterChain->execute();
}
}
}
Filters are used extensively in plug-ins, as they allow you to extend the features of an application globally. Refer to Chapter 17 to learn more about plug-ins, and see the online wiki (http://trac.symfony-project.org/) for more filter examples.