In this tutorial I would like to show how you can add flexibility to your symfony applications using the symfony built-in event system. For this example let's imagine that we have a website where the user can rate pictures from other users in the system. Every time the user rates a picture, some actions should happen according to the application business rules:
- The user will gain points for his action
- The session should be updated indicating the amount of images rated by the user
- The view cache must be cleared for the template that shows the amount of user rated images
While we can override the Image::save()
method to add a hook, so every time the user rates an image we perform those three actions, this
can bring some unwanted consequences. To name a few: If the user uploads an image, when we store the object in database we will
unnecessarily clear the view cache and store data in the session. Also, if we have a backend to administer the images, every time a website
admin review an image the overridden save()
method will be called, with all the extra actions.
Symfony Event System to the rescue
Since version 1.1, symfony provides an event notification system. Basically, the framework classes dispatch events at certain moments of the application lifetime. We can register our own listeners to act upon event notifications. Also we can even create and dispatch our custom events.
To read more about the symfony event system, refer to the following links:
In our example we will fire a custom event after the user rates the image for which we will add listeners to act accordingly.
In the rateImage
action we will have the following code:
public function executeRateImage(sfWebRequest $request) { // code for processing the image rate here ... $this->dispatcher->notify(new sfEvent($this, 'images.image_rated')); }
This will fire an event with the name images.image_rated
that will have as subject the action where it was generated. We will use the
subject to later retrieve information related to the context where the event subject occurred.
The sfEvent
constructor expects up to three parameters in the following order: event subject, event name and an optional array of parameter
which can be used by the event handler. Because the sfEvent
implements the ArrayAccess
interface we can access the parameters with ease like
this: $event['my_parameter']
.
For more details about the
ArrayAccess
interface check the PHP documentation.
The next step is to actually listen to this event. We can add listeners in several places along a symfony application. For this example we will do it in the application configuration file. So, let's register the listeners:
class frontendConfiguration extends sfApplicationConfiguration { public function configure() { // ... // Register our listeners $this->dispatcher->connect('images.image_rated', array('UsersManager', 'addPointsToUser')); $this->dispatcher->connect('images.image_rated', array('UsersManager', 'updateSessionPoints')); $this->dispatcher->connect('images.image_rated', array('UsersManager', 'clearRatePartial')); } }
As you can see, the application configuration class has a member that keeps a reference to the symfony event dispatcher. There we tell it
that we want to be notified when an event of the type images.image_rated
is fired. Then, we will create a UsersManager
class that will
provide the methods to listen to the events. Before going into the UsersManager
class code we should talk about the sfEventDispatcher
class.
From the symfony book we know that this class provides the mechanisms to add and remove listeners and to notify events. Let's see some of
the sfEventDispatcher
public methods:
public function connect($name, $listener)
We used this method in the frontendConfiguration
class to register listeners to our custom event. The first parameter is an event name
(images.image_rated
in our case) and the second one a listener which should be a variable of the callback pseudo PHP type.
More on PHP callbacks in the PHP documentation.
public function disconnect($name, $listener)
The expected parameters are the same as the previous method, but this time we use it to remove a listener. If we have the case that a
specific module of our application doesn't need to listen to a particular event we can add a config.php
file inside the module config
folder with the following code:
$this->dispatcher->disconnect('images.image_rated', array('UsersManager', 'clearRatePartial'));
public function hasListeners($name)
This method will return true
if the provided event name has registered listeners.
public function getListeners($name)
If we need to do something with the registered listeners for a particular event we can also retrieve them by calling this method, providing the event name.
Now let's see the UsersManager
code:
class UsersManager { static public function addPointsToUser(sfEvent $event) { UserPeer::updateUserPoints($event->getSubject()->getUser()->getPoints() + sfConfig::get('app_points_for_rating')); } static public function updateSessionPoints(sfEvent $event) { $user = $event->getSubject()->getUser(); $user->setPoints($user->getPoints() + sfConfig::get('app_points_for_rating')); } static public function clearRatePartial(sfEvent $event) { if ($cache = $event->getSubject()->getContext()->getViewCacheManager()) { $cache->remove('user/points?id='.$event->getSubject()->getUser()->getId()); } } }
In the addPointsToUser()
method, we retrieve the actual user points from the session, we add more points, and then we store them in to the
database.
The updateSessionPoints()
follows a similar mechanism but update the user session.
If your are wondering how do we access the symfony session user, the answer is in the magic behind the symfony events system. If you recall from the code to notify in our action, we provided the current action instance as the subject of our custom event. In symfony, the action has access to the session user by calling
$this->getUser()
which is what we do in our example.
The last event listener that we registered was UsersManager::clearRatePartial()
, where we get an instance of the View Cache Manager and we
tell it to clear the required cache.
Conclusion
Symfony event system provides a lot of capabilities to your applications. It can be used to build highly decoupled systems, leaving away glue code. After you get used to this techniques, it will be easier to build slim controllers that maximize the power of MVC. Also keep in mind that, as explained on the symfony book, you can even adapt at run time the way the framework works, making it highly customizable.
This is simply great. I always wondered if I could do something like this. Your post just awakened me to a great symfony feature. Thanks.
little typo, "related to the context where the event occurred subject" should be "related to the context where the event subject occurred"
Great work, this sf feature is very useful!
This is a great feature which (as concluded) can help to slim controllers.
But i wonder about the moment in the filter chain when the code of the registered listeners is actually executed.
Indeed, very useful, thx.
Although it does slim controllers, there is a trade off of moving some important logic like incrementing the users points into the "background" hidden quite deep inside the app which is not always a good thing, especially when working in teams.
I think it's important to keep any interactions with your model in the controller for clarity.
The cache and session state are handy though.
Of course having the logic in one place has advantages when working in teams and without a given design model, but on the other side events are great to decouple some logic, that is not related to the core.
You for example might want to be able to activate/deactivate some features in your application like a facebok activity feed or something like that. But if you want to deactivate that feature due to too high perfomance load or bugs, the hardcoding it in the controller will force you to go directly to the controller and comment the feature logic. Using Events you could just disconnect the eventlistener. I like that idea very much!
Having read the last blog articles about the event system I added a feature suggestion to uservoice yesterday ;)
The event system is very usefull. However in this particular example, I can't see how this code is better than simply calling the 3 functions at the end of the rating action.
ex: although the code length is not the only aspect of it, it is actually longer, isn't it ? 3 connect + 1 notify = 4 calls ?
is it better in that the coupling is less tight ?
If you want to trigger an event from an Propel/doctrine class, I've added a snippet that might help you : http://snippets.symfony-project.org/snippet/322
This idea with sfEventDispatcher is only an implementation of famous design pattern called "observer pattern". The great feature for me is: the symfony core classes do fire events so I can observe it and add custom action. It makes symfony more flexible and extensible.
But take care with firing own events: If you fire over 100 events and you will lose the overview and you can make your errors untraceable. Don´t forget to keep records which events you did fire and which objects you passed into sfEvent.
@Éric Rogé: I wouldn't use sfEventDispatcher in model classes. A model should not know more like other such things. I would send such events in controller (an action / a component class).
@Fabian Spillner: But the issue is that an operation on the model can be done by many different ways.
Let's consider the case of the event fired when an article is deleted. Of course, you can notify the event in every action/component class that delete an article. But it goes against the DRY concept.
And sometime, there's hardly no other way to do it. In your application, your client wants that if an author is removed, all his articles are also deleted. Then how will you fire the article deletion event ? By notifying it also in every action/component that delete an author ? For me the good place for these event notification is the model classes.
I might be wrong, what's your opinion ?
@Fabian and Éric: I must agree with Éric, this definitely belongs to the model ("model" not only stands for data model but also for application model). One of the most important rules using the MVC pattern is: keep the controller as small as possible. Application logic always has to go to the model, not the controller or the view. Even if it is not for DRY, the maintainability is much better if you follow this rule (it took me several years on several projects in both Java and PHP to really understand and accept this though).
To the events system in general: I've been using the observer pattern quite extensively in Java and I love it - it is a really powerful thing to use (if used correctly) and it's great for keeping applications (and application models) separated. As with anything else there are ways to misuse it.