Events in CakePHP 2 - Using the Observer Pattern

Diagram of CakePHP's observer pattern
CakePHP's Observer Pattern (the Events System)

It seems that Events are an often ignored feature of the CakePHP framework. Introduced in version 2.1 the events system provides a means of applying logic that generally doesn’t belong in a model or controller. Developing in Cake is all about its fat models and skinny controllers, but there are some things that belong in neither, and shouldn’t really be placed in a model just to make it fatter!

If you’re not familiar with the idea of fat models and skinny controllers it is basically this: models contain the business logic, things like retrieving, saving and deleting data whilst the controllers prepare this data for displaying to the end user via the view. However, there is some code that doesn’t fit into this cosy MVC arrangement.

For example, imagine we have an User Model and we want to send an email to a new user after creating their record in the database. The sending of the email has nothing to do with the view, so doesn’t belong with the controller and isn’t directly related to working with the database (the business logic). It is a reaction to the business logic of creating a new record in the database, our new user. This is where events come in.

CakePHP’s Observer Pattern

You may have heard of CakePHP’s events system as the Observer pattern (common to the MVC design approach).

In the Observer pattern we have subjects which can either be models or controllers in Cake; these subjects raise events.

Then we have observers (referred to in Cake as listeners) that are ‘attached’ to our subjects and ‘listen’ for any events raised in the application.

By developing with this design pattern we can separate out application code. If developing Cake plugins this can help make them more easily extensible. Code held in an Event Listener can be easily extended or overridden in Cake due to the way it is attached using the App’s config.

Raising an Event

So continuing with our example of a new user registration, our User Model is our subject and we want to raise an event after the user is created. We can do this inside the afterSave callback when a new record/user is created:-

<?php

App::uses('CakeEvent', 'Event');

class User extends AppModel {

    public function afterSave($created, $options = array()) {

        parent::afterSave($created, $options);

        if ($created === true) {
            $Event = new CakeEvent('Model.User.created', $this, array(
                'id' => $this->id,
                'data' => $this->data[$this->alias]
            ));
            $this->getEventManager()->dispatch($Event);
        }

    }

}

First thing to note in this example is that we’ve used App::uses() to include CakeEvent which we create a new instance of in our afterSave() method. We’re naming our event 'Model.User.created', passing the subject ($this) and associating some data with the event (the third parameter of CakeEvent).

We’ve named the event 'Model.User.created', but this really could be anything you’d like; the important thing here is that this name will be used in our listener to trigger some code. However, I’d recommend using Cake’s pseudo name-spacing used here; this is basically the subject type (‘Model’), the subject object (‘User’) and finally a descriptive event name (‘created’).

Finally the dispatch() method notifies all the attached listeners of our event. We’ll look at how to attach listeners in a bit.

As you can see our User Model has no reference to sending an email to the new user. As stated earlier, this has nothing to do with the interaction with our database. Instead we are going to use a listener to observe when the new user is created thanks to our newly raised event.

The Listener

The recommended place to keep our event listeners is app/Lib/Event, although Cake’s default file structure doesn’t come setup for this so you will need to create the Event directory yourself if it doesn’t already exist.

Unlike models and controllers, Cake doesn’t prescribe a naming convention on listeners. Personally I’d recommend using singular CamelCased class names ending in ‘Listener’ (and name the file accordingly). For our example we’ll create an UserListener and save it to app/Lib/Event/UserListener.php.

CakePHP Event Listeners implement the CakeEventListener interface which requires the presence of an implementedEvents() method. So our listener needs at a minimum to look something like this:-

<?php // In app/Lib/Event/UserListener.php

App::uses('CakeEventListener', 'Event');

class UserListener implements CakeEventListener {

    public function implementedEvents() {

    }
}

The implementedEvents() method is where we’ll map our event names to methods that the listener will be observing. For our example we’ve got a ‘Model.User.created’ event that we want to listen out for and send a confirmation email to the new user when it triggers:-

public function implementedEvents() {
    return ['Model.User.created' => 'sendConfirmationEmail'];
}

Now we need to create the sendConfirmationEmail() method in our listener:-

public function sendConfirmationEmail(CakeEvent $Event) {

    $Email = new CakeEmail();

    $Email->from(array(
        'no-reply@example.com' => 'Your Site'
    ));
    $Email->to($Event->data['user']['email']);
    $Email->subject('Your account has been created');
    $Email->template('new_user');
    $Email->viewVars(array(
        'data' => $Event->data['data']
    ));
    $Email->send();

    return;

}

As we’re using CakeEmail we also need to include it at the top of the listener file:-

App::uses('CakeEmail', 'Network/Email');

The methods mapped to in implementedEvents() must be methods of the current listener class; so sendConfirmationEmail() is a method of our UserListener. The methods take a single parameter that is a CakeEvent. As you can see in the example code above, this contains any data we passed when raising the event in our Model. In our case we will have $Event->data['id'] and $Event->data['data']. Any data modified in $Event->data will be accessible in our subject (e.g. the Model) after the event was raised.

Another thing to note about Listeners is that they are not directly associated with a Model. So although our UserListener is being attached to our User model it doesn’t load the Model. If you want to use a Model inside a listener you will need to use ClassRegistry::init().

Attaching the Listeners

The final thing we need to do is ‘attach’ our listeners to something so that our events trigger. I have seen people attach their listeners directly from the model/controller, but a better approach is to do the attachment from our app’s Config by creating an events.php file:-

// In app/Config/events.php

App::uses('ClassRegistry', 'Utility');
App::uses('UserListener', 'Lib/Event');
$User = ClassRegistry::init('User');
$User->getEventManager()->attach(new UserListener());

What this is doing is loading the listener we’ve created and then attaching it to the User Model’s event manager.

It shouldn’t be necessary for the User example being described here, but it is also possible (and in some cases desirable) to globally attach listeners in our app:-

// In app/Config/events.php

App::uses('CakeEventManager', 'Event');
App::uses('UserListener', 'Lib/Event');
CakeEventManager::instance()->attach(new UserListener());

Lastly, we need to make sure Cake knows about our new events config file, so we need to load it in our app’s bootstrap file (somewhere towards the end of the file should be fine):-

// In app/Config/bootstrap.php

// Load the global event listeners.
require_once APP . 'Config' . DS . 'events.php';

Now when a new user is created Cake will call the method defined in our listener and send the welcome email.

Final Words

You may be like one of the many that have come across the official CakePHP Events System documentation and been scared off events due to the confusing explanations. I hope this has better explained what Cake’s Event System is and how it works; and perhaps has got you motivated to getting started with it.

Be aware that working with events can make debugging a little more tricky, so don’t go overboard using them. However, when used right can lead to better and more reusable code. Events can do much much more than described here (see the official documentation) so go on and check out what they can do for you!


Danilo Santos has translated this article into Portuguese: Eventos no CakePHP 2 — Usando o Observer Pattern (Padrão Observador).

Related Content

Published on


Comments

  1. Amirreza |

    tanx for tutorial

  2. Patrick |

    Hey! Thanks for giving the CakePHP 2 event system some light. It seems ridiculously ignored by devs.

    Could you give an example as to why it may be desirable to attach listeners globally in specific situations?

    Cheers.

  3. Danilo Santos |

    Hey Andy, i’m from Brazil and like so much of your article.

    In the official documentation, the explanation is complicated, but here you easily do it.

    The events it’s a very powerfull tool.

    Thanks.

Leave a Comment
  • You will need to preview your comment before you can submit it.