Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
The Events System of CakePHP
Clone this wiki locally
The Events System of CakePHP
The Events system of CakePHP was introduced in CakePHP 2.1 and is therefore relatively new. The CakePHP 2.1 Migration Guide lists the following description of it:
- A new generic events system has been built and it replaced the way callbacks were dispatched. This should not represent any change to your code.
- You can dispatch your own events and attach callbacks to them at will, useful for inter-plugin communication and easier decoupling of your classes.
The second bullet lists the main functionality that the Events subsystem provides and the motivation for its introduction. The Events system allows one to apply the Observer design pattern to avoid highly coupled objects. The Cookbook clearly explains how Events can be used in an application for this purpose.
However, the focus of this document is on the first bullet, namely that the Events system replaced the way callbacks were dispatched. CakePHP has a lot of default callbacks for Model, View, Controller, Behavior, Helper and Component classes, which are now triggered by Events. However, there are concerns that this change has led to a decrease in performance and that the current way is inefficient. In this document, we try to indentify the problems and explore some possible solutions.
Firstly, we will give a short overview of the functionality of Events. Then we compare the way that the callbacks used to be triggered in previous versions to the current approach using Events. After that we present some profiling results. Then we give an overview of the problems and we discuss some solutions. We end with our conclusion.
Dispatching Events and Listening
The Cookbook already explains in detail how Events are used in the general way, but we will give a quick overview of the things that matter to understand the rest of the document.
The most important class is the CakeEventManager. In short, there are two things one can do with it:
One can dispatch an event through
The CakeEvent class provides an abstraction of an event. An event is created using three parameters. An event can be identified by the parameter name. The parameter subject refers to the object executing the particular event. Additional data for the event can be passed through the third parameter payload.
One can attach an object that implements the CakeEventListener interface through
The object is then subscribed to the events that are generated by the instance of CakeEventManager. The implementation should contain a method
implementedEvents()that provides a mapping of names of events to methods that should be called when the event is triggered. The CakeEventManager keeps track of all attached listeners.
Callbacks - before and after
Models, Views and Controllers have a number of callbacks. These are empty methods by default, but when those classes are extended by an application, the methods can be overridden. For example, a Controller has a callback called
beforeFilter(). A Component for Controllers additionaly has callbacks called
startup(). A Model has many callbacks, such as
afterDelete(), et cetera. These can be seen as hooks to add additional business logic.
Callbacks used to be called directly in most cases, but with the advent of the Event system in CakePHP 2.1, the callbacks are triggered when certain events are dispatched. To that end, the classes providing callbacks have become event listeners. Below, we compare the new situation to the old one. In the comparison, Models and Behaviors are used as examples, but keep in mind that the same applies to Views and Controllers with their Components and Helpers.
Before (CakePHP 2.0)
The sequence diagram below gives an example of how the callback
Model.afterDelete() is triggered. The callback is first executed on each Behavior, if there are any, and then on the Model itself.
- Note that a Model always has a BehaviorCollection, but it can be empty. Therefore, the BehaviorCollection is always triggered.
Behavior.afterDelete()are empty callbacks by default that can be overridden by subclasses.
Model.afterDelete()will always be called, even it is empty. The call happens via a direct invocation.
- The call to
Behavior.afterDelete()does not happen via a direct invocation, but via
call_user_func_array(). According to comments in the PHP manual, this function is slow. However, it will only be called if there actually is a Behavior.
After (CakePHP ≥ 2.1)
From CakePHP 2.1 onwards, Model, View, Controller classes and their corresponding BehaviorCollection, ComponentCollection and HelperCollection classes have all become event listeners. They all implement
implementedEvents(), which maps event names to the actual callback methods. For example, there is a mapping from the event name "afterDelete" to the method
Model.afterDelete(). Just as before, these callbacks exist by default and may be empty.
As explained, listeners need to be attached to the CakeEventManager. Therefore, each Model passes through the following initialization phase:
Now we return to the example scenario, where the
delete() method is called. In the new situation, the following happens:
- Instead of immediately triggering the BehaviorCollection and calling Model.afterDelete() directly, a CakeEvent with the name "afterDelete" is constructed, which is then dispatched.
- The CakeEventManager knows that there are two listeners, the Model itself and the BehaviorCollection, and queries their
implementedMethods(). It then calls the callback methods that belong to the "afterDelete" event.
Model.afterDelete()is no longer called directly, but via
call_user_func_array(). This also goes for
Now, consider a Model that has no Behaviors and has not implemented any callbacks (in other words, their bodies are empty). In this case, only the rightmost lifeline and the corresponding loop in the sequence diagram is gone. This means that the notorious
call_user_func_array() is still called twice, whereas it would not be used in the situation before. Furthermore, note that this would not only happen for afterDelete(), but also for beforeDelete(), beforeSave(), et cetera.
So, a very simple application with Model classes with no Behaviors and empty callbacks, would make calls to
call_user_func_array(), whereas it would do none in the previous situation. If
call_user_func_array() is indeed as slow as mentioned by others, then this might be a source of problems.
We performed a number of experiments to compare the performance between CakePHP 2.0 and 2.1 with respect to the callbacks. We used a simple application with only a Controller and a View and another application that also included Components. Finally, we upgraded an actual CakePHP application with many Behaviors, Components and overridden callbacks from version 2.0 to 2.1 and compared the performance before and after.
We expected to see more calls to
call_user_func_array() and, partly because of that, we expected to see a significant decrease of the performance.
Our full test setup and the experimental results are described in Results profiling CakePHP event system. Here, we list our main findings:
- In CakePHP ≥ 2.1,
CakeEventManager->dispatch()is among the most time consuming methods. However, in CakePHP 2.0,
ObjectCollection->trigger()is among the most time consuming methods. It consumes less time than
CakeEventManager->dispatch()in the new version, but the difference is not significant.
call_user_func_array()is performed much more often (twice as much in the complex application), but again, the difference in performance is not significant.
- In the complex application that had quite some callbacks,
ObjectCollection->trigger()did not perform more time in CakePHP 2.1 than in version 2.0.
Therefore, we conclude from our experiments that the change from callbacks to events between version 2.0 and 2.1 did not decrease the performance significantly. However, this does not imply that there are no performance issues. Though, it implies that, if the Events system is currently slow, then the old implementation of callbacks was also slow.
A second conlusion is that
call_user_func_array() did not negatively impact the performance very much, in contrast to what we presumed. Still, a more natural way of invoking the callback methods should be preferred, because
call_user_func_array() does not really fit in with Object Oriented Programming.
So, the good news is that the Events system has not made CakePHP slower than it was. The bad news is that apparently it was already slow in the old situation. We identified a number of problems that are common to both versions:
- ObjectCollections such as BehaviorCollection, ComponentCollection and HelperCollection are always attached to the CakeEventManager, even if they are empty. This results in many unneccesary function calls.
- Callbacks are always triggered, even if they are empty, which is the default. Considering the fact that there are so many callbacks (especially for Models) and that most applications only use a subset of them, this also results in many unnecessary function calls.
An alternative is to attach the Objects instead of the ObjectCollections. The following sequence diagram shows this. The number of calls to
call_user_func_array()is decreased by 1 per Model, because the collection is no longer an event listener.
A problem inherent to this solution is that the
CakeEventManager->dispatch()function has to call more listeners. But the
ObjectCollection->trigger()function is eliminated the same number of times, which will save time.
Another problem is that the ObjectCollection->trigger() function has some edge cases: the signatures of the behavior and component classes differ from the callback of the corresponding model and controller classes. For example in the Model
model.afterFind($query)is called, while in a Behavior
behavior.afterFind($model, $query)is called. This is implemented in the ObjectCollection.trigger() function in the following way. If the callback is an instance of CakeEvent, ObjectCollection.trigger() will prepend the subject to the list of arguments, such that the model is passed to the behaviors. However, the callback functions of the
Helperclasses don't have the subject as argument in contrast to the
Componentclasses. That is why the HelperCollection.trigger() overrides the ObjectCollection.trigger() function, and adds an attribute 'omitSubject' to the event. This causes that the subject is not passed as argument to helper classes.
If we would adopt this behavior in the dispatch() function, we should have to take into account which listeners we are dealing with to determine their signature. A solution to the complex behavior of ObjectCollection->trigger() should be found, to prevent the CakeEventManager->dispatch() function to become overly complex. We consider this in the next bullet point.
When an event is dispatched, listeners receive a proper
CakeEventobject as an attribute. So in the ideal case, the signature of, for example, the afterFind callback would look like
However, the callback had always looked like
afterFind(array $results, boolean $primary = false). To make callbacks compatible with the Events system without breaking backwards compatibility, the following solution was introduced: when implemented methods are attached to the CakeEventManager, one can specify the option
'passParams' => true. This has the result that, when the listener is called, the data from the event object are passed as attributes instead of the event object itself.
In the previous bullet point, we had the problem that callbacks between Models and Behaviors are different, while they correspond to the same event. In this case, it would be
afterFind(Model $Model, array $results, boolean $primary = false)for the behavior.
A potential solution for this problem, that is in the spirit of the passParams solution, is to introduce yet another option, such as
passSubject => truein
Behavior.implementedMethods()that denotes that the subject (in this case the Model) should be appended to the passed attributes. However, this is not a very object oriented manner and in this way, everything becomes a bit of a hack. In our opinion, adding all these extra checks for the sake of backwards compatiblity has an impact on maintainability. So there is a trade-off to make.
We would recommend to remove the
passParamsoption altogether, so that callbacks only receive a CakeEvent object. This makes callbacks compliant with the general event system and reduces all the checks for edge cases. This decision could be made for the next major version, version 3.0. The downside of this move would be that it breaks all existing plug-ins. However, postponing this decision would only increase the technical debt.
The Behaviors, Components and Helpers that are used are stored as an array of strings in the Models and Controllers. They actual Behavior/Component/Helper objects are loaded via
ObjectCollection->load(String $name). This creates an instance of the class if it is requested for the first time and returns it on subsequent calls. In this way, the loading of Behaviors, Components and Helpers is deferred. We investigated if this reduces the number of objects that are loaded and found the following:
Dispatcher.invoke()is called on a user request, then the Controller is initialized and all the Components in the ComponentCollection are loaded.
- All Behaviors in the BehaviorCollection are loaded from the constructor of the
- All Helpers in the HelperCollection are only loaded when a View is rendered.
So, in practice Components, Behaviors and Helpers are always loaded eventually, except for the rare case where a View is not rendered. Therefore,
ObjectCollection may be simplified by always loading its objects.
A loaded object can be either enabled or disabled. A disabled object should not respond to events. However, only the
AuthComponent appears to be disabled sometimes.
The ObjectCollection violates the Single Responsibility Principle since it has too many responsibilities: creating the objects, maining states (enabled/disabled), relaying events (ObjectCollection.trigger()). The event relaying should be taken out as recommended under Solutions. Perhaps an Abstract Factory Pattern can be applied so that there is a Factory, instead of the ObjectCollection, responsible for the creation of Behavior, Component and Helpers.
In this document we gave an overview of how the callback methods in CakePHP have become part of the event system and we analysed the differences. Although we found that after the change, CakePHP has not not really performed worse than before, we still found some problems that we pointed out. Solving these could potentially improve the performance, but most of all it would make the code more clean and increase maintainability, because right know there are many different checks in the code for handling different kinds of callbacks.
We gave some pointers to possible solutions. Though we realise that our suggestions are somewhat shallow and that an actual refactoring would require quite some more work, we hope that this document at least makes it easier to discuss the problems, that it may serve as a reference regarding the problems that currently exist and that it may be a starting point for working towards an actual solution for the new major version of CakePHP.