Among the current limitations of PHP, one of the most annoying is you can't have a class extend more than one class. Another limitation is you can't add new methods to an existing class or override existing methods. To palliate these two limitations and to make the framework truly extendable, symfony introduces an event system, inspired by the Cocoa notification center, and based on the Observer design pattern (http://en.wikipedia.org/wiki/Observer_pattern).
17.1.1. Understanding Events
Some of the symfony classes "notify an event" at various moments of their life. For instance, when the user changes its culture, the user object notifies a change_culture
event. This event is like a shout in the project's space, saying: "I'm doing that. Do whatever you want about it".
You can decide to do something special when an event is fired. For instance, you could save the user culture to a database table each time the change_culture
event is notified, to remember the user prefered culture. In order to do so, you need to register an event listener, which is a complicated sentence to say that you must declare a function to be called when the event occurs. Listing 17-1 shows how to register a listener on the user's change_culture
event.
Listing 17-1 - Registering an Event Listener
$dispatcher->connect('user.change_culture', 'changeUserCulture');
function changeUserCulture(sfEvent $event)
{
$user = $event->getSubject();
$culture = $event['culture'];
// do something with the user culture
}
All events and listener registrations are managed by a special object called the event dispatcher. This object is available from everywhere in symfony by way of the sfContext
singleton, and most symfony objects offer a getDispatcher()
method to get direct access to it. Using the dispatcher's connect()
method, you can register any PHP callable (either a class method or a function) to be called when an event occurs. The first argument of connect()
is the event identifier, which is a string composed of a namespace and a name. The second argument is a PHP callable.
Once the function registered in the event dispatcher, it sits and waits until the event is fired. The event dispatcher keeps a record of all event listeners, and knows which ones to call when an event is notified. When calling these methods or functions, the dispatcher passes them an sfEvent
object as parameter.
The event object stores information about the notified event. The event notifier can be retrieved thanks to the getSubject()
method, and the event parameters are accessible by using the event object as an array (for example, $event['culture']
can be used to retrieve the culture
parameter passed by sfUser
when notifying user.change_culture
).
To wrap it up, the event system allows you to add abilities to an existing class or modify its methods at runtime, without using inheritance.
Note In version 1.0, symfony used a similar system but with a different syntax. You may see calls to static methods of an sfMixer
class to register and notify events in symfony 1.0 code, instead of calls to methods of the event dispatcher. sfMixer
calls are deprecated, yet they still work in symfony 1.1.
17.1.2. Notifying an Event
Just like symfony classes notify events, your own classes can offer runtime extensibility and notify events at certain occasions. For instance, let's say that your application requests several third-party web services, and that you have written a sfRestRequest
class to wrap the REST logic of these requests. A good idea would be to notify an event each time this class makes a new request. This would make the addition of logging or caching capabilities easier in the future. Listing 17-2 shows the code you need to add to an existing fetch()
method to make it notify an event.
Listing 17-2 - Notifying an Event
class sfRestRequest
{
protected $dispatcher = null;
public function __construct(sfEventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* Makes a query to an external web service
*/
public function fetch($uri, $parameters = array())
{
// Notify the beginning of the fetch process
$this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_prepare', array(
'uri' => $uri,
'parameters' => $parameters
)));
// Make the request and store the result in a $result variable
// ...
// Notify the end of the fetch process
$this->dispatcher->notify(new sfEvent($this, 'rest_request.fetch_success', array(
'uri' => $uri,
'parameters' => $parameters,
'result' => $result
)));
return $result;
}
}
The notify()
method of the event dispatcher expects an sfEvent
object as argument; this is the very same object that is passed to the event listeners. This object always carries a reference to the notifier (that's why the event instance is initialized with this
) and an event identifier. Optionally, it accepts an associative array of parameters, giving listeners a way to interact with the notifier's logic.
Tip Only the classes that notify events can be extended by way of the event system. So even if you don't know whether you will need to extend a class in the future or not, it is always a good idea to add notifications in the key methods.
17.1.3. Notifying an Event Until a Listener handles it
By using the notify()
method, you make sure that all the listeners registered on the notified event are executed. But in some cases, you need to allow a listener to stop the event and prevent further listeners from being notified about it. In this case, you should use notifyUntil()
instead of notify()
. The dispatcher will then execute all listeners until one returns true
, and then stop the event notification. In other terms, notifyUntil()
is like a shout in the project space saying: "I'm doing that. If somebody cares, then I won't tell anybody else". Listing 17-3 shows how to use this technique in combination with a magic __call()
method to add methods to an existing class at runtime.
Listing 17-3 - Notifying an Event Until a Listener Returns True
class sfRestRequest
{
// ...
public function __call($method, $arguments)
{
$event = $this->dispatcher->notifyUntil(new sfEvent($this, 'rest_request.method_not_found', array(
'method' => $method,
'arguments' => $arguments
)));
if (!$event->isProcessed())
{
throw new sfException(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
}
return $event->getReturnValue();
}
}
An event listener registered on the rest_request.method_not_found
event can test the requested $method
and decide to handle it, or pass to the next event listener callable. In Listing 17-4, you can see how a third party class can add put()
and delete()
methods to the sfRestRequest
class at runtime with this trick.
Listing 17-4 - Handling a "Notify Until"-Type Event
class frontendConfiguration extends sfApplicationConfiguration
{
public function configure()
{
// ...
// Register our listener
$this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
}
}
class sfRestRequestExtension
{
static public function listenToMethodNotFound(sfEvent $event)
{
switch ($event['method'])
{
case 'put':
self::put($event->getSubject(), $event['arguments'])
return true;
case 'delete':
self::delete($event->getSubject(), $event['arguments'])
return true;
default:
return false;
}
}
static protected function put($restRequest, $arguments)
{
// Make a put request and store the result in a $result variable
// ...
$event->setReturnValue($result);
}
static protected function delete($restRequest, $arguments)
{
// Make a delete request and store the result in a $result variable
// ...
$event->setReturnValue($result);
}
}
In practice, notifyUntil()
offers multiple inheritance capabilities, or rather mixins (the addition of methods from third-party classes to an existing class), to PHP. You can now "inject" new methods to objects that you can't extend by way of inheritance. And this happens at runtime. You are not limited by the Object Oriented capabilities of PHP anymore when you use symfony.
Tip As the first listener to catch a notifyUntil()
event prevents further notification, you may worry about the order of which listeners are executed. This order corresponds to the order in which listeners were registered — first registered, first executed. But in practice, cases when this could be disturbing seldom happen. If you realize that two listeners conflict on a particular event, perhaps your class should notify several events, for instance one at the beginning and one at the end of the method execution. And if you use events to add new methods to an existing class, name your methods wisely so that other attempts at adding methods don't conflict. Prefixing method names with the name of the listener class is a good practice.
17.1.4. Changing the Return Value of a Method
You can probably imagine how a listener can not only use the information given by an event, but also modify it, to alter the original logic of the notifier. If you want to allow this, you should use the filter()
method of the event dispatcher rather than notify()
. All event listeners are then called with two parameters: the event object, and the value to filter. Event listeners must return the value, whether they altered it or not. Listings 17-5 show show filter()
can be used to filter a response from a web service and escape special characters in that response.
Listing 17-5 - Notifying and Handling a Filter Event
class sfRestRequest
{
// ...
/**
* Make a query to an external web service
*/
public function fetch($uri, $parameters = array())
{
// Make the request and store the result in a $result variable
// ...
// Notify the end of the fetch process
return $this->dispatcher->filter(new sfEvent($this, 'rest_request.filter_result', array(
'uri' => $uri,
'parameters' => $parameters,
), $result));
}
}
// Add escaping to the web service response
$dispatcher->connect('rest_request.filter_result', 'rest_htmlspecialchars');
function rest_htmlspecialchars(sfEvent $event, $result)
{
return htmlspecialchars($result, ENT_QUOTES, 'UTF-8');
}
17.1.5. Built-In Events
Many of symfony's classes have built-in events, allowing you to extend the framework without necessarily changing the class itself. Table 17-1 lists these events, together with their type and arguments.
Table 17-1 - Symfony's Events
event namespace | event name | type | notifiers | arguments |
---|---|---|---|---|
application | log | notify | lot of classes | priority |
throw_exception | notifyUntil | sfException | - | |
command | log | notify | sfCommand* classes | priority |
pre_command | notifyUntil | sfTask | arguments, options | |
post_command | notify | sfTask | - | |
filter_options | filter | sfTask | command_manager | |
configuration | method_not_found | notifyUntil | sfProjectConfiguration | method, arguments |
component | method_not_found | notifyUntil | sfComponent | method, arguments |
context | load_factories | notify | sfContext | - |
controller | change_action | notify | sfController | module, action |
method_not_found | notifyUntil | sfController | method, arguments | |
page_not_found | notify | sfController | module, action | |
plugin | pre_install | notify | sfPluginManager | channel, plugin, is_package |
post_install | notify | sfPluginManager | channel, plugin | |
pre_uninstall | notify | sfPluginManager | channel, plugin | |
post_uninstall | notify | sfPluginManager | channel, plugin | |
request | filter_parameters | filter | sfWebRequest | path_info |
method_not_found | notifyUntil | sfRequest | method, arguments | |
response | method_not_found | notifyUntil | sfResponse | method, arguments |
filter_content | filter | sfResponse | - | |
routing | load_configuration | notify | sfRouting | - |
task | cache.clear | notifyUntil | sfCacheClearTask | app, type, env |
template | filter_parameters | filter | sfViewParameterHolder | - |
user | change_culture | notify | sfUser | culture |
method_not_found | notifyUntil | sfUser | method, arguments | |
change_authentication | notify | sfBasicSecurityUser | authenticated | |
view | configure_format | notify | sfView | format, response, request |
method_not_found | notifyUntil | sfView | method, arguments | |
view.cache | filter_content | filter | sfViewCacheManager | response, uri, new |
You are free to register event listeners on any of these events. Just make sure that listener callables return a boolean when registered on a notifyUntil
-type event, and that they return the filtered value when registered on a filter
-type event.
Note that the event namespaces don't necessarily match the class role. For instance, all symfony classes notify an application.log
event when they need something to appear in the log files (and in the web debug toolbar):
$dispatcher->notify(new sfEvent($this, 'application.log', array($message)));
Your own classes can do the same and also notify symfony events when it makes sense.
17.1.6. Where To Register Listeners?
Event listeners need to be registered early in the life of a symfony request. In practice, the right place to register event listeners is in the application configuration class. This class has a reference to the event dispatcher that you can use in the configure()
method. Listing 17-6 shows how to register a listener on one of the rest_request
events of the above examples.
Listing 17-6 - Registering a Listener in the Application Configuration Class, in apps/frontend/config/ApplicationConfiguration.class.php
class frontendConfiguration extends sfApplicationConfiguration
{
public function configure()
{
$this->dispatcher->connect('rest_request.method_not_found', array('sfRestRequestExtension', 'listenToMethodNotFound'));
}
}
Plug-ins (see below) can register their own event listeners. They should do it in the plug-in's config/config.php
script, which is executed during application initialization and offers access to the event dispatcher through $this->dispatcher
.