Events System
Creating maintainable applications is both a science and an art. It iswell-known that a key for having good quality code is making your objectsloosely coupled and strongly cohesive at the same time. Cohesion means thatall methods and properties for a class are strongly related to the classitself and it is not trying to do the job other objects should be doing,while loosely coupling is the measure of how little a class is “wired”to external objects, and how much that class is depending on them.
There are certain cases where you need to cleanly communicate with other partsof an application, without having to hard code dependencies, thus losingcohesion and increasing class coupling. Using the Observer pattern, which allowsobjects to notify other objects and anonymous listeners about changes isa useful pattern to achieve this goal.
Listeners in the observer pattern can subscribe to events and choose to act uponthem if they are relevant. If you have used JavaScript, there is a good chancethat you are already familiar with event driven programming.
CakePHP emulates several aspects of how events are triggered and managed inpopular JavaScript libraries such as jQuery. In the CakePHP implementation, anevent object is dispatched to all listeners. The event object holds informationabout the event, and provides the ability to stop event propagation at anypoint. Listeners can register themselves or can delegate this task to otherobjects and have the chance to alter the state and the event itself for the restof the callbacks.
The event subsystem is at the heart of Model, Behavior, Controller, View andHelper callbacks. If you’ve ever used any of them, you are already somewhatfamiliar with events in CakePHP.
Example Event Usage
Let’s suppose you are building a Cart plugin, and you’d like to focus on justhandling order logic. You don’t really want to include shipping logic, emailingthe user or decrementing the item from the stock, but these are important tasksto the people using your plugin. If you were not using events, you may try toimplement this by attaching behaviors to models, or adding components to yourcontrollers. Doing so represents a challenge most of the time, since youwould have to come up with the code for externally loading those behaviors orattaching hooks to your plugin controllers.
Instead, you can use events to allow you to cleanly separate the concerns ofyour code and allow additional concerns to hook into your plugin using events.For example, in your Cart plugin you have an Orders model that deals withcreating orders. You’d like to notify the rest of the application that an orderhas been created. To keep your Orders model clean you could use events:
- // Cart/Model/Table/OrdersTable.php
- namespace Cart\Model\Table;
- use Cake\Event\Event;
- use Cake\ORM\Table;
- class OrdersTable extends Table
- {
- public function place($order)
- {
- if ($this->save($order)) {
- $this->Cart->remove($order);
- $event = new Event('Model.Order.afterPlace', $this, [
- 'order' => $order
- ]);
- $this->getEventManager()->dispatch($event);
- return true;
- }
- return false;
- }
- }
The above code allows you to notify the other parts of the applicationthat an order has been created. You can then do tasks like send emailnotifications, update stock, log relevant statistics and other tasks in separateobjects that focus on those concerns.
Accessing Event Managers
In CakePHP events are triggered against event managers. Event managers areavailable in every Table, View and Controller using getEventManager()
:
- $events = $this->getEventManager();
Each model has a separate event manager, while the View and Controllershare one. This allows model events to be self contained, and allow componentsor controllers to act upon events created in the view if necessary.
Global Event Manager
In addition to instance level event managers, CakePHP provides a global eventmanager that allows you to listen to any event fired in an application. This isuseful when attaching listeners to a specific instance might be cumbersome ordifficult. The global manager is a singleton instance ofCake\Event\EventManager
. Listeners attached to the globaldispatcher will be fired before instance listeners at the same priority. You canaccess the global manager using a static method:
- // In any configuration file or piece of code that executes before the event
- use Cake\Event\EventManager;
- EventManager::instance()->on(
- 'Model.Order.afterPlace',
- $aCallback
- );
One important thing you should consider is that there are events that will betriggered having the same name but different subjects, so checking it in theevent object is usually required in any function that gets attached globally inorder to prevent some bugs. Remember that with the flexibility of using theglobal manager, some additional complexity is incurred.
Cake\Event\EventManager::dispatch()
method accepts the eventobject as an argument and notifies all listener and callbacks passing thisobject along. The listeners will handle all the extra logic around theafterPlace
event, you can log the time, send emails, update user statisticspossibly in separate objects and even delegating it to offline tasks if you havethe need.
Tracking Events
To keep a list of events that are fired on a particular EventManager
, youcan enable event tracking. To do so, simply attach anCake\Event\EventList
to the manager:
- EventManager::instance()->setEventList(new EventList());
After firing an event on the manager, you can retrieve it from the event list:
- $eventsFired = EventManager::instance()->getEventList();
- $firstEvent = $eventsFired[0];
Tracking can be disabled by removing the event list or callingCake\Event\EventList::trackEvents(false)
.
Core Events
There are a number of core events within the framework which your applicationcan listen to. Each layer of CakePHP emits events that you can use in yourapplication.
Registering Listeners
Listeners are the preferred way to register callbacks for an event. This is doneby implementing the Cake\Event\EventListenerInterface
interfacein any class you wish to register some callbacks. Classes implementing it needto provide the implementedEvents()
method. This method must return anassociative array with all event names that the class will handle.
To continue our previous example, let’s imagine we have a UserStatistic classresponsible for calculating a user’s purchasing history, and compiling intoglobal site statistics. This is a great place to use a listener class. Doing soallows you to concentrate the statistics logic in one place and react to eventsas necessary. Our UserStatistics
listener might start out like:
- use Cake\Event\EventListenerInterface;
- class UserStatistic implements EventListenerInterface
- {
- public function implementedEvents()
- {
- return [
- 'Model.Order.afterPlace' => 'updateBuyStatistic',
- ];
- }
- public function updateBuyStatistic($event)
- {
- // Code to update statistics
- }
- }
- // Attach the UserStatistic object to the Order's event manager
- $statistics = new UserStatistic();
- $this->Orders->getEventManager()->on($statistics);
As you can see in the above code, the on()
function will accept instancesof the EventListener
interface. Internally, the event manager will useimplementedEvents()
to attach the correct callbacks.
Registering Anonymous Listeners
While event listener objects are generally a better way to implement listeners,you can also bind any callable
as an event listener. For example if wewanted to put any orders into the log files, we could use a simple anonymousfunction to do so:
- use Cake\Log\Log;
- $this->Orders->getEventManager()->on('Model.Order.afterPlace', function ($event) {
- Log::write(
- 'info',
- 'A new order was placed with id: ' . $event->getSubject()->id
- );
- });
In addition to anonymous functions you can use any other callable type that PHPsupports:
- $events = [
- 'email-sending' => 'EmailSender::sendBuyEmail',
- 'inventory' => [$this->InventoryManager, 'decrement'],
- ];
- foreach ($events as $callable) {
- $eventManager->on('Model.Order.afterPlace', $callable);
- }
When working with plugins that don’t trigger specific events, you can leverageevent listeners on the default events. Lets take an example ‘UserFeedback’plugin which handles feedback forms from users. From your application you wouldlike to know when a Feedback record has been saved and ultimately act on it. Youcan listen to the global Model.afterSave
event. However, you can takea more direct approach and only listen to the event you really need:
- // You can create the following before the
- // save operation, ie. config/bootstrap.php
- use Cake\ORM\TableRegistry;
- // If sending emails
- use Cake\Mailer\Email;
- TableRegistry::getTableLocator()->get('ThirdPartyPlugin.Feedbacks')
- ->getEventManager()
- ->on('Model.afterSave', function($event, $entity)
- {
- // For example we can send an email to the admin
- $email = new Email('default');
- $email->setFrom(['info@yoursite.com' => 'Your Site'])
- ->setTo('admin@yoursite.com')
- ->setSubject('New Feedback - Your Site')
- ->send('Body of message');
- });
You can use this same approach to bind listener objects.
Interacting with Existing Listeners
Assuming several event listeners have been registered the presence or absenceof a particular event pattern can be used as the basis of some action.:
- // Attach listeners to EventManager.
- $this->getEventManager()->on('User.Registration', [$this, 'userRegistration']);
- $this->getEventManager()->on('User.Verification', [$this, 'userVerification']);
- $this->getEventManager()->on('User.Authorization', [$this, 'userAuthorization']);
- // Somewhere else in your application.
- $events = $this->getEventManager()->matchingListeners('Verification');
- if (!empty($events)) {
- // Perform logic related to presence of 'Verification' event listener.
- // For example removing the listener if present.
- $this->getEventManager()->off('User.Verification');
- } else {
- // Perform logic related to absence of 'Verification' event listener
- }
Note
The pattern passed to the matchingListeners
method is case sensitive.
Establishing Priorities
In some cases you might want to control the order that listeners are invoked.For instance, if we go back to our user statistics example. It would be ideal ifthis listener was called at the end of the stack. By calling it at the end ofthe listener stack, we can ensure that the event was not cancelled, and that noother listeners raised exceptions. We can also get the final state of theobjects in the case that other listeners have modified the subject or eventobject.
Priorities are defined as an integer when adding a listener. The higher thenumber, the later the method will be fired. The default priority for alllisteners is 10
. If you need your method to be run earlier, using any valuebelow this default will work. On the other hand if you desire to run thecallback after the others, using a number above 10
will do.
If two callbacks happen to have the same priority value, they will be executedwith a the order they were attached. You set priorities using the on()
method for callbacks, and declaring it in the implementedEvents()
functionfor event listeners:
- // Setting priority for a callback
- $callback = [$this, 'doSomething'];
- $this->getEventManager()->on(
- 'Model.Order.afterPlace',
- ['priority' => 2],
- $callback
- );
- // Setting priority for a listener
- class UserStatistic implements EventListenerInterface
- {
- public function implementedEvents()
- {
- return [
- 'Model.Order.afterPlace' => [
- 'callable' => 'updateBuyStatistic',
- 'priority' => 100
- ],
- ];
- }
- }
As you see, the main difference for EventListener
objects is that you needto use an array for specifying the callable method and the priority preference.The callable
key is a special array entry that the manager will read to knowwhat function in the class it should be calling.
Getting Event Data as Function Parameters
When events have data provided in their constructor, the provided data isconverted into arguments for the listeners. An example from the View layer isthe afterRender callback:
- $this->getEventManager()
- ->dispatch(new Event('View.afterRender', $this, ['view' => $viewFileName]));
The listeners of the View.afterRender
callback should have the followingsignature:
- function (EventInterface $event, $viewFileName)
Each value provided to the Event constructor will be converted into functionparameters in the order they appear in the data array. If you use an associativearray, the result of array_values
will determine the function argumentorder.
Note
Unlike in 2.x, converting event data to listener arguments is the defaultbehavior and cannot be disabled.
Dispatching Events
Once you have obtained an instance of an event manager you can dispatch eventsusing Event\EventManager::dispatch()
. This method takes aninstance of the Cake\Event\Event
class. Let’s look at dispatchingan event:
- // An event listener has to be instantiated before dispatching an event.
- // Create a new event and dispatch it.
- $event = new Event('Model.Order.afterPlace', $this, [
- 'order' => $order
- ]);
- $this->getEventManager()->dispatch($event);
Cake\Event\Event
accepts 3 arguments in its constructor. Thefirst one is the event name, you should try to keep this name as unique aspossible, while making it readable. We suggest a convention as follows:Layer.eventName
for general events happening at a layer level (e.g.Controller.startup
, View.beforeRender
) and Layer.Class.eventName
forevents happening in specific classes on a layer, for exampleModel.User.afterRegister
or Controller.Courses.invalidAccess
.
The second argument is the subject
, meaning the object associated to theevent, usually when it is the same class triggering events about itself, using$this
will be the most common case. Although a Component could triggercontroller events too. The subject class is important because listeners will getimmediate access to the object properties and have the chance to inspect orchange them on the fly.
Finally, the third argument is any additional event data. This can be any datayou consider useful to pass around so listeners can act upon it. While this canbe an argument of any type, we recommend passing an associative array.
The Event\EventManager::dispatch()
method accepts an eventobject as an argument and notifies all subscribed listeners.
Stopping Events
Much like DOM events, you may want to stop an event to prevent additionallisteners from being notified. You can see this in action during model callbacks(e.g. beforeSave) in which it is possible to stop the saving operation ifthe code detects it cannot proceed any further.
In order to stop events you can either return false
in your callbacks orcall the stopPropagation()
method on the event object:
- public function doSomething($event)
- {
- // ...
- return false; // Stops the event
- }
- public function updateBuyStatistic($event)
- {
- // ...
- $event->stopPropagation();
- }
Stopping an event will prevent any additional callbacks from being called.Additionally the code triggering the event may behave differently based on theevent being stopped or not. Generally it does not make sense to stop ‘after’events, but stopping ‘before’ events is often used to prevent the entireoperation from occurring.
To check if an event was stopped, you call the isStopped()
method in theevent object:
- public function place($order)
- {
- $event = new Event('Model.Order.beforePlace', $this, ['order' => $order]);
- $this->getEventManager()->dispatch($event);
- if ($event->isStopped()) {
- return false;
- }
- if ($this->Orders->save($order)) {
- // ...
- }
- // ...
- }
In the previous example the order would not get saved if the event is stoppedduring the beforePlace
process.
Getting Event Results
Every time a callback returns a non-null non-false value, it gets stored in the$result
property of the event object. This is useful when you want to allowcallbacks to modify the event execution. Let’s take again our beforePlace
example and let callbacks modify the $order
data.
Event results can be altered either using the event object result propertydirectly or returning the value in the callback itself:
- // A listener callback
- public function doSomething($event)
- {
- // ...
- $alteredData = $event->getData('order') + $moreData;
- return $alteredData;
- }
- // Another listener callback
- public function doSomethingElse($event)
- {
- // ...
- $event->setResult(['order' => $alteredData] + $this->result());
- }
- // Using the event result
- public function place($order)
- {
- $event = new Event('Model.Order.beforePlace', $this, ['order' => $order]);
- $this->getEventManager()->dispatch($event);
- if (!empty($event->getResult()['order'])) {
- $order = $event->getResult()['order'];
- }
- if ($this->Orders->save($order)) {
- // ...
- }
- // ...
- }
It is possible to alter any event object property and have the new data passedto the next callback. In most of the cases, providing objects as event data orresult and directly altering the object is the best solution as the reference iskept the same and modifications are shared across all callback calls.
Removing Callbacks and Listeners
If for any reason you want to remove any callback from the event manager justcall the Cake\Event\EventManager::off()
method using asarguments the first two parameters you used for attaching it:
- // Attaching a function
- $this->getEventManager()->on('My.event', [$this, 'doSomething']);
- // Detaching the function
- $this->getEventManager()->off('My.event', [$this, 'doSomething']);
- // Attaching an anonymous function.
- $myFunction = function ($event) { ... };
- $this->getEventManager()->on('My.event', $myFunction);
- // Detaching the anonymous function
- $this->getEventManager()->off('My.event', $myFunction);
- // Adding a EventListener
- $listener = new MyEventLister();
- $this->getEventManager()->on($listener);
- // Detaching a single event key from a listener
- $this->getEventManager()->off('My.event', $listener);
- // Detaching all callbacks implemented by a listener
- $this->getEventManager()->off($listener);
Events are a great way of separating concerns in your application and makeclasses both cohesive and decoupled from each other. Events can be utilized tode-couple application code and make extensible plugins.
Keep in mind that with great power comes great responsibility. Using too manyevents can make debugging harder and require additional integration testing.