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.

Published in #Tutorials