Simpler CakePHP Events
This tutorial assumes you are using the FriendsOfCake/app-template project with Composer. Please see this post for more information.
There is always a lot of boilerplate involved in creating a proper event listening setup. Martin Bean wrote a small tutorial on it’s usage, and I’d like to propose a few small changes:
Centralized Event Dispatching
One of my complaints with dispatching events is all the complications with figuring out how to dispatch. Several classes are involved and I am lazy. Instead, we’ll create an app/Lib/Event/AppEventDispatcher.php
:
<?php
App::uses('CakeEvent', 'Event');
class AppEventDispatcher {
public static function dispatch($name, $subject, $data = null) {
$manager = $subject->getEventManager();
$event = new CakeEvent($name, $subject, $data);
return $manager->dispatch($event);
}
}
?>
Our sole requirement is that the subject of the event must also have a method getEventManager
that returns a CakeEventManager
instance. Now the interface becomes:
<?php
App::uses('AppEventDispatcher', 'Lib/Event');
class User extends AppModel {
public function afterSave($created, $options = array()) {
if ($created) {
AppEventDispatcher::dispatch('Model.User.created', $this, array(
'id' => $this->id,
'data' => $this->data[$this->alias]
));
}
}
}
?>
Not a big change, but something that removes some complication for me.
Annotations for custom CakeEventListener
This section requires Traits, which are only available in 5.4
I absolutely hate defining a new method in my listeners to return implemented events. Instead, I’ll use annotations. Let’s install an annotations library using composer
:
composer require minime/annotations:~1.1
And now we’ll define an AppEventListener
that all our classes will inherit from:
<?php
App::uses('CakeEventListener', 'Event');
class AppEventListener implements CakeEventListener {
use Minime\Annotations\Traits\Reader;
public function implementedEvents() {
$methods = get_class_methods($this);
$events = array();
foreach ($methods as $method) {
$annotations = $this->getMethodAnnotations($method);
if (!$annotations->get('CakeEvent')) {
continue;
}
$events[$annotations->get('CakeEvent')] = $method;
}
return $events;
}
}
?>
Now we can define a new listener using annotations:
<?php
App::uses('AppEventListener', 'Event');
class UserListener extends AppEventListener {
/**
* @CakeEvent Model.User.created
*/
public function sendActivationEmail(CakeEvent $event) {
// TODO
}
}
?>
Whenever this listener is used, we iterate over every method and check for the @CakeEvent
annotation. If it exists, we attach the method to the specified event name.
This may break if CakePHP ever uses @CakeEvent internally for phpdocs, but that is unlikely as CakePHP doesn’t use annotations for code anywhere, nor does it use non-standard annotations for docblocks.
If you are worried about performance hits because of the above method - and you should always be wary when magic is involved - you can cache the implemented events using somthing like APC or a local Memcache instance.
Properly attaching your listeners
The annoying bit of attaching listeners is that you don’t know where they should be attached. Attaching in bootstrap for models that aren’t going to be used in the request is annoying. Methods I use are as follows:
Attaching in the constructor
Simply never attach the listener unless the object is constructed. For instance, here is what it would look like in our app/Model/User.php
model class:
<?php
App::uses('UserListener', 'Event');
class User extends AppModel {
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->getEventManager()->attach(new UserListener());
}
}
?>
All you need to do is remember to call the parent::__construct()
method with the proper arguments.
Attaching on the fly
My AppEventDispatcher
normally has the following method:
<?php
App::uses('CakeEvent', 'Event');
class AppEventDispatcher {
public static function attach($subject, $listenerClass) {
return $subject->getEventManager()->attach(new $listenerClass);
}
}
?>
Before I expect my code to be called, I would call the following:
<?php
class User extends AppModel {
public function afterSave($created, $options = array()) {
if ($created) {
AppEventDispatcher::attach($this, 'UserListener');
AppEventDispatcher::dispatch('Model.User.created', $this, array(
'id' => $this->id,
'data' => $this->data[$this->alias]
));
}
}
}
?>
Note, this is quite messy as nor you are attaching listeners in random places.
Combine the listener with your class
This one is a combination of the above methods. We’ll use the User model as an example. Lets setup our AppModel scaffolding:
<?php
App::uses('AppEventDispatcher', 'Event');
class AppModel extends Model implements CakeEventListener {
use Minime\Annotations\Traits\Reader;
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->getEventManager()->attach($this);
}
}
?>
We’ve made our model implement the CakeEventListener
, as well as included the Minime\Annotations\Traits\Reader
trait. Now lets implement the interface:
<?php
public function implementedEvents() {
$methods = $this->getClassMethods($this);
$events = array();
foreach ($methods as $method) {
$annotations = $this->getMethodAnnotations($method);
if (!$annotations->get('CakeEvent')) {
continue;
}
$events[$annotations->get('CakeEvent')] = $method;
}
return array_merge(parent::implementedEvents(), $events);
}
public function getClassMethods() {
$class = get_class($this);
$classMethods = get_class_methods($class);
if ($parentClass = get_parent_class($class)) {
$parentMethods = get_class_methods('Model');
$readerMethods = get_class_methods('Minime\Annotations\Traits\Reader');
return array_diff($classMethods, $parentMethods, $readerMethods, array(
'implementedEvents',
'getClassMethods'
));
}
return (array) $classMethods;
}
?>
We have customized our call to get the class methods in order to remove all of the parent methods from the Model as well as the trait that is in use. Note that this allows us to annotate methods in our AppModel. Now we add our event to the app/Model/User.php
class:
<?php
class User extends AppModel {
/**
* @CakeEvent Model.User.created
*/
public function sendActivationEmail(CakeEvent $event) {
// TODO
}
?>
And we’re done. To implement more events, you simply add the method to your model and annotate it as we did before. You can still attach new listeners, and we are respecting all core events.
Going further
There are a bunch of small ways you can improve your CakePHP experience. You should not ignore features you want only because CakePHP does not include some library, pattern, or methodology in it’s core. We’ve implemented annotations in a clean way, trimmed the fat off creating new events, and exposed the power of extending CakePHP!