Is there a way to dispatch a custom event every time a certain entity setter is called ?
I actually need to change some value of an unrelated entity, every time a certain entity property is changed. So in order to separate concerns and to decouple objects, I wanted to do this with the observer pattern. I don't want to do this in some doctrine event like 'preUpdate' or similar, as they only fire when the entity is flushed, but I need this value to change immediately to assure these two values are always in sync.
As it is bad practice to inject any service into the entity, I don't see how I could do that ?
Any suggestions ?
Using the event dispatcher:
The Event that will carry your information
class UpdateEntityEvent extends Event {
private $myEntity;
private $newValue;
public function __construct(Entity $entity, Whatever $newValue){
$this->myEntity = $entity;
$this->newValue = $newValue;
}
// [...] getters
}
Your Listener
class UpdateMyEntityEventListener
{
public function updateCertainProperty(UpdateMyEntityEvent $event)
{
// Do what you want here :D
}
}
Some configuration
kernel.listener.updateMyEntity:
class: Acme\AppBundle\EventListener\UpdateMyEntityEventListener
tags:
- { name: kernel.event_listener, event: updateMyEntity, method: updateCertainProperty }
We avoid using some hardcoded string, let's put the event name in a constant
class MyEntityEvents
{
const UPDATE = 'updateMyEntity';
}
Then in your Controller
public function updateAction()
{
// [...]
$event = new UpdateMyEntityEvent($entity, $whatever);
$dispatcher = $this->get('event_dispatcher')->dispatch( MyEntityEvents::UPDATE, $event);
If you wish to use the observer pattern, you will have to implement it yourself in some way. As you pointed out, Doctrine will compute the changeset of your entity only when a flush operation is triggered and not before. That being said, it happens that Doctrine proposes alternative tracking policies. The NOTIFY tracking policy behaviour relies exactly on what you wish to achieve.
I am not suggesting that you should change the tracking policy of your entity but you could take advantage of the existing interfaces to implement your observer pattern. To do so, as explained in this section of the documentation, your entity being observed needs to implement the NotifyPropertyChanged interface.
From there you could implement the PropertyChangedListener interface directly in the other entity (or use a specific service that would add itself as listener of your entity in the postLoad event for example ?). Here it mainly depends on the relation between your entities and how you can attach your listener to the entity implementing NotifyPropertyChanged.
Note that if you do this, the UnitOfWork of Doctrine will automatically hook itself as a listener of your entity but it will still rely on automatic changeset computation as long as you don't add the #ChangeTrackingPolicy("NOTIFY") annotation.
Related
I have a Symfony 3.4 app and a Composer package with an EntityChangeListener to log entity property changes. The package also contains a EntityListenerPass (compiler pass) which iterates over a list of class names defined in app config.yml while building the service container. It programmatically tags the entity classes like this to notify the listener on preUpdate events:
$listener = $container->getDefinition('entity_history.listener.entity_change');
$entities = $container->getExtensionConfig('entity_history')[0]['entities'];
foreach ($entities as $className) {
$listener->addTag('doctrine.orm.entity_listener', ['entity' => $className, 'event' => 'preUpdate']);
}
Adding those tags causes a lot of errors which appear unrelated. In example undefined index errors inside the Doctrine UnitOfWork for the entity states. Also related entities which are loaded from database suddenly are recognised as new by Doctrine. Even object comparison inside a switch statement started to fail with:
Fatal error: Nesting level too deep - recursive dependency?
But without those listeners, everything works fine and all tests pass. Is there an alternative/better way to programmatically set up Doctrine entity listeners?
Yes, you can attach entity listeners by acting directly on the class metadata. In my application (Symfony 2.8), I am doing this for some entities that are marked in my config by adding a listener that reacts to the loadClassMetadata event.
With this approach, you can hook your entity listeners when Doctrine loads for the first time the classmetada (by using addEntityListener). Thus, you only hook entity listeners that are needed for the current context, nothing more.
Here is a modified version of the listener I use to mirror how it could look in your particular case:
namespace AppBundle\Listener;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class MappingListener
{
private $listenerClassname;
private $entities;
public function __construct($listenerClassname, array $entities)
{
$this->entities = $entities;
$this->listenerClassname = $listenerClassname;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if(!in_array($classMetadata->name, $this->entities))
{
return;
}
// Hook the entity listener in the class metadata
// $classMetadata->addEntityListener( string $eventName, string $class, string $method )
$classMetadata->addEntityListener('preUpdate', $this->listenerClassName, 'preUpdate');
}
}
And then in your services.yml, something like this:
mapping.listener:
class: AppBundle\Listener\MappingListener
arguments: [ "%your_listener_classname%", "%your_entities_array%" ]
tags:
- { name: doctrine.event_listener, event: loadClassMetadata, lazy: true }
Is there a way to force a doctrine event ( like preUpdate ) on a parent associated entity ?
So for example: I have a order entity with one-to-many orderItem entities.
Now, I want to do a bunch of checkup's and possible changes to the order entity or even one of it's orderItem entities ( where I need to access many other services) whenever any of the orderItems change. But the doctrine events do not fire on the order entity when one of its orderItem entities changes.
Note: this post entirely focuses on the particular case of the preUpdate event. It is possible to dispatch an event manually by using the event manager. The problem lies in the fact that simply triggering the preUpdate event of an entity is not enough to have its new state persisted to the database if the preUpdate method modified something.
There are multiple ways to do this but none of them are really straightforward. Considering only the case of the preUpdate event, I had quite a lot of trouble to find how to do this in a clean way as association updates are simply not built in a way to handle such cases as discussed in the Doctrine documentation.
Either way, if you want to do this, among the solutions I found, there were many that suggested to directly mess up with the UnitOfWork of Doctrine. This can be quite powerful but then you have to be careful about what you use and when you use it as Doctrine might not be able to actually dispatch the event you want in some cases discussed below.
Anyway, I ended up implementing something that makes use of a change of tracking policy for the parent entity. By doing so, the parent entity preUpdate event can be triggered if one of its properties is modified or if one of its "children" was modified.
Main concerns with the UnitOfWork
If you wish to use the UnitOfWork (that you can retrieve by using $args->getEntityManager()->getUnitOfWork() with any type of arguments of lifecycle events), you can use the public method scheduleForUpdate(object $entity). However, if you wish to use this method, you will need to call it before the commit order is computed inside of the unit of work. Moreover, if you have a preUpdate event associated to the entity you scheduled for update, it will raise an error if your entity has an empty change set (which is exactly the case we are dealing with when the main entity is not modified but one of its related entities is).
Thus calling $unitOfWork->scheduleForUpdate($myParentEntity), in a preUpdate of a child entity is not an option as explained in the documentation (performing calls to the UnitOfWork API is strongly discouraged as it does not work as it would outside of the flush operation).
It should be noted that $unitOfWork->scheduleExtraUpdate($parentEntity, array $changeset) can be used in that specific context but this method is marked as "INTERNAL". The following solutions avoid using it but it might be a good approach if you know what you are getting into.
Possible solutions
Note: I did not test the implementation of the wanted behaviour with the onFlush event but it was often presented as the most powerful approach. For the other two possibilities listed here, I tried them successfully with a OneToMany association.
In the following section, when I'm talking about a parent entity, I refer to the entity that has the OneToMany association while children entities are refering to the entities that have the ManyToOne association (thus, the children entities are the owning side of the association).
1. Using onFlush event
You can try to work your way out of this by using the onFlush event however, in that case you have to deal with the UnitOfWork internals as suggested in the documentation. In that case, you can't do it within an Entity listener (introduced in 2.4) as the onFlush event is not among the possible callbacks. Some examples based on what's given by the official doc can be found on the web. Here is a possible implementation: Update associated entities in doctrine.
The main drawback here is that you don't really trigger the preUpdate event of your entity, you just handle the behaviour you wanted somewhere else. It seemed a bit too heavy handed for me, so I searched for other solutions.
2. Using the UnitOfWork in preFlush event of the child entities
One way to actually trigger the preUpdate event of the parent entity, is to add another entity listener to the child entity and to use the UnitOfWork. As explained before, you can't simply do this in the preUpdate event of the child entity.
In order for the commit order to be properly computed, we need to call scheduleForUpdate and propertyChanged in the preFlush event of the child entity listener as shown below:
class ChildListener
{
public function preFlush(Child $child, PreFlushEventArgs $args)
{
$uow = $args->getEntityManager()->getUnitOfWork();
// Add an entry to the change set of the parent so that the PreUpdateEventArgs can be constructed without errors
$uow->propertyChanged($child->getParent(), 'children', 0, 1);
// Schedule for update the parent entity so that the preUpdate event can be triggered
$uow->scheduleForUpdate($child->getParent());
}
}
As you can see, we need to notify the UnitOfWork that a property has changed so that everything works properly. It looks a bit sloppy but it gets the work done.
The important part is that we mark the children property (the OneToMany association of the parent) as changed so that the change set of the parent entity is not empty. A few important notes about the internals at stake with this propertyChanged call:
The method expects a persistent field name (non-persistent ones will be ignored), any mapped field will do, even associations, that is why using children works here.
The change set that is modified consecutively to this call does not have any side effects here as it will be recomputed after the preUpdate event.
The main problem of this approach is that the parent entity is scheduled for update even if it is not needed. As there is no direct way to tell if the child entity has changed in its preFlush event (you could use the UnitOfWork but it would become a bit redundant with its internals), you will trigger the preUpdate event of the parent at every flush where a child entity is being managed.
Moreover, with this solution, Doctrine will begin a transaction and commit even if there are no queries performed (e.g. if nothing was modified at all, you will still find in the Symfony Profiler, two consecutives entries "START TRANSACTION" and "COMMIT" in the Doctrine logs).
3. Change the tracking policy of the parent and handle the behaviour explicitly
Since I've been messing with the internals of the UnitOfWork quite a bit, I stumbled upon the propertyChanged method (that was used in the previous solution) and noticed that it was part of the interface PropertyChangedListener. It happens that this is linked to a documented topic: the tracking policy. By default, you can just let Doctrine detect the changes but you can also change this policy and manage everything manually as explained here, in the documentation.
After reading about this, I eventually came up with the following solution that cleanly handles the wanted behaviour, the cost being that you have to do some extra work in your entities.
Thus, to have exactly what I desired, my parent entity follows the NOTIFY tracking policy and children notify the parent when one of their properties is modified. As described in the official documentation, you have to implement the NotifyPropertyChanged interface and then notify the listeners of properties changes (the UnitOfWork automatically adds itself to the listeners if it detects that one of the managed entities implements the interface). After that, if the annotation #ChangeTrackingPolicy is added, at commit times, Doctrine will rely on the change set that was built via propertyChanged calls and not on an automatic detection.
Here is how you would do it for a basic Parent entity:
namespace AppBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
/**
* ... other annotations ...
* #ORM\EntityListeners({"AppBundle\Listener\ParentListener"})
* #ORM\ChangeTrackingPolicy("NOTIFY")
*/
class Parent implements NotifyPropertyChanged
{
// Add the implementation satisfying the NotifyPropertyChanged interface
use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;
/* ... other properties ... */
/**
* #ORM\Column(name="basic_property", type="string")
*/
private $basicProperty;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Child", mappedBy="parent", cascade={"persist", "remove"})
*/
private $children;
/**
* #ORM\Column(name="other_field", type="string")
*/
private $otherField;
public function __construct()
{
$this->children = new \Doctrine\Common\Collections\ArrayCollection();
}
public function notifyChildChanged()
{
$this->onPropertyChanged('children', 0, 1);
}
public function setBasicProperty($value)
{
if($this->basicProperty != $value)
{
$this->onPropertyChanged('basicProperty', $this->basicProperty, $value);
$this->basicProperty = $value;
}
}
public function addChild(Child $child)
{
$this->notifyChildChanged();
$this->children[] = $child;
$child->setParent($this);
return $this;
}
public function removeChild(Child $child)
{
$this->notifyChildChanged();
$this->children->removeElement($child);
}
/* ... other methods ... */
}
with the trait taken from the code given in the documentation:
namespace AppBundle\Doctrine\Traits;
use Doctrine\Common\PropertyChangedListener;
trait NotifyPropertyChangedTrait
{
private $listeners = [];
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listeners[] = $listener;
}
/** Notifies listeners of a change. */
private function onPropertyChanged($propName, $oldValue, $newValue)
{
if ($this->listeners)
{
foreach ($this->listeners as $listener)
{
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
}
and the following Child entity with the owning side of the association:
namespace AppBundle\Entity;
class Child
{
/* .. other properties .. */
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Parent", inversedBy="children")
*/
private $parentEntity;
/**
* #ORM\Column(name="attribute", type="string")
*/
private $attribute;
public function setAttribute($attribute)
{
// Check if the parentEntity is not null to handle the case where the child entity is created before being attached to its parent
if($this->attribute != $attribute && $this->parentEntity)
{
$this->parentEntity->notifyChildChanged();
$this->attribute = $attribute;
}
}
/* ... other methods ... */
}
And there it is, you have everything fully working. If, your child entity is modified, you explicitly call notifyChildChanged that will then notify the UnitOfWork that children field has changed for the parent entity thus cleanly triggering the update process and the preUpdate event if one is specified.
Unlike the solution #2, the event will be triggered only if something has changed and you can control with precision why it should be marked as changed. For example, you could mark the children as changed if only a certain set of attributes is changed and ignore other changes as you have full control other what is eventually notified to the UnitOfWork.
Note:
With the NOTIFY tracking policy, apparently, preFlush events won't be triggered in the Parent entity listener (preFlush event being triggered in computeChangeSet which is simply not called for entities using this policy).
It is necessary to track every "normal" property to trigger updates if normal properties are changed. One solution to do this without having to modify all your setters is to use magic calls as shown below.
It is safe to set a children entry in the change set as it will be simply ignored when the update query is created since the parent entity is NOT the owning side of the association. (i.e. it does not have any foreign keys)
Use of magic calls to handle notifications easily
In my application, I added the following trait
namespace AppBundle\Utils\Traits;
trait MagicSettersTrait
{
/** Returns an array with the names of properties for which magic setters can be used */
abstract protected function getMagicSetters();
/** Override if needed in the class using this trait to perform actions before set operations */
private function _preSetCallback($property, $newValue) {}
/** Override if needed in the class using this trait to perform actions after set operations */
private function _postSetCallback($property, $newValue) {}
/** Returns true if the method name starts by "set" */
private function isSetterMethodCall($name)
{
return substr($name, 0, 3) == 'set';
}
/** Can be overriden by the class using this trait to allow other magic calls */
public function __call($name, array $args)
{
$this->handleSetterMethodCall($name, $args);
}
/**
* #param string $name Name of the method being called
* #param array $args Arguments passed to the method
* #throws BadMethodCallException if the setter is not handled or if the number of arguments is not 1
*/
private function handleSetterMethodCall($name, array $args)
{
$property = lcfirst(substr($name, 3));
if(!$this->isSetterMethodCall($name) || !in_array($property, $this->getMagicSetters()))
{
throw new \BadMethodCallException('Undefined method ' . $name . ' for class ' . get_class($this));
}
if(count($args) != 1)
{
throw new \BadMethodCallException('Method ' . $name . ' expects 1 argument (' . count($args) . ' given)');;
}
$this->_preSetCallback($property, $args[0]);
$this->$property = $args[0];
$this->_postSetCallback($property, $args[0]);
}
}
which I could then use in my entities. Here is an example of my Tag entity whose preUpdate event needed to be called when one of its aliases was modified:
/**
* #ORM\Table(name="tag")
* #ORM\EntityListeners({"AppBundle\Listener\Tag\TagListener"})
* #ORM\ChangeTrackingPolicy("NOTIFY")
*/
class Tag implements NotifyPropertyChanged
{
use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;
use \AppBundle\Utils\Traits\MagicSettersTrait;
/* ... attributes ... */
protected function getMagicSetters() { return ['slug', 'reviewed', 'translations']; }
/** Called before the actuel set operation in the magic setters */
public function _preSetCallback($property, $newValue)
{
if($this->$property != $newValue)
{
$this->onPropertyChanged($property, $this->$property, $newValue);
}
}
public function notifyAliasChanged()
{
$this->onPropertyChanged('aliases', 0, 1);
}
/* ... methods ... */
public function addAlias(\AppBundle\Entity\Tag\TagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases[] = $alias;
$alias->setTag($this);
return $this;
}
public function removeAlias(\AppBundle\Entity\Tag\TagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases->removeElement($alias);
}
}
I can then reuse the same trait in my "child" entity named TagAlias:
class TagAlias
{
use \AppBundle\Utils\Traits\MagicSettersTrait;
/* ... attributes ... */
public function getMagicSetters() { return ['alias', 'main', 'locale']; }
/** Called before the actuel set operation in the magic setters */
protected function _preSetCallback($property, $newValue)
{
if($this->$property != $newValue && $this->tag)
{
$this->tag->notifyAliasChanged();
}
}
/* ... methods ... */
}
Note: If you chose to do this, you might encounter errors when Forms are trying to hydrate your entities as magic calls are disabled by default. Simply add the following to your services.yml to enable magic calls. (taken from this discussion)
property_accessor:
class: %property_accessor.class%
arguments: [true]
A more pragmatic approach is to version your parent entity. A simple example of this would be a timestamp (e.g. updated_at) that is updated when the collection of child entities is modified. This assumes you update all the child entities through its parent.
I have a case where I would like to log every change which happens on entities. I'm using sonata-admin with the doctrine admin bundle. I tried many things but I'm out of ideas what the best approach for this case would be.
The first try was creating a ChangeLog entity with the fields type (create / update), changes (array) and related entity class and id.
I setup a listener for the postUpdate und postPersist event:
appbundle.listen.ChangeLog:
class: AppBundle\Listener\ChangeLogListener
arguments: [#service_container]
tags:
- { name: doctrine.event_listener, event: postUpdate}
- { name: doctrine.event_listener, event: postPersist}
The related listener:
public function prePersist(LifecycleEventArgs $args)
{
$this->buildLog($args, ChangeLog::TYPE_CREATE);
}
public function preUpdate(LifecycleEventArgs $args){
$this->buildLog($args, ChangeLog::TYPE_UPDATE);
}
private function buildLog(LifecycleEventArgs $args, $type)
{
$entity = $args->getEntity();
$clHelper = ChangeLogHelper::getInstance();
if ($entity instanceof ChangeLog) return;
$em = $args->getEntityManager();
$changes = $em->getUnitOfWork()->getEntityChangeSet($entity);
$user = $this->container->get('security.context')->getToken()->getUser();
$cl = new ChangeLog();
$cl->setUser($user);
$cl->setDate(new \DateTime());
$cl->setChangeset($changes);
$cl->setType($type);
$cl->setEntityName(get_class($entity));
$cl->setDescription('');
$cl->setEntityId($entity->getId());
$cl->setRefGroup($clHelper->getRefId());
$em->persist($cl);
$em->flush();
}
That works for some entities but as soon as I have more relations i get the error:
Catchable Fatal Error: Argument 3 passed to Doctrine\ORM\Event\PreUpdateEventArgs::__construct() ....
I found no way to solve this problem but I have the feeling that it is caused because the listener will be called many times (for the entity itself and each relation) and it would be better to flush at the end of all listeners instead of every time it is called, but I don't see any way to do that with the post event listener setup.
After hours of debugging I thought that it probably would be better anyway if I would have a polymorphic relation on the ChangeLog entity to every other entity and I could just use the prePersist / preUpdate listeners so I don't have to persist the object myself and just set it as relation on the changed object with a proper cascade. And I'm trying to avoid the use of the entityManager in doctrine events anyway. Well hours later I'm still stuck, I couldn't find a way with doctrine to have this kind of relation. Basically One-To-Many with an extra column where the target entity is defined.
I tried to get it to work with doctrine inheritance (STI and CTI) but then my log fields are on the entity itself and not separated anymore which I don't want. I tried to solve this without the owning side on the ChangeLog entity, but then I can't set the ChangeLog entity on the changed entity because it will be ignored. But I don't know how to define the owning side with a reference to basically every other entity and I don't think it is possible with doctrine right now.
With a mapped super class and just a OneToMany relation I will get a join table or join column for every entity which is kind of messy as well.
Is it possible to setup an event listener (or do — something else?) to listen for all the events fired by a Symfony 2 AppKernel application for a particular request?
That is, I know I can browse an application with app_dev.php and use the profiler to view a list of all the listeners, but I'm interested in grabbing a list of every event that's been dispatched/fired. I know some event systems have a special global/all listener what would let me receive every event. I'm wondering if Symfony has something similar, or if there's another mechanism to get a list of all the available events on a particular page.
I also know I could add some temporary debugging code to one of the event dispatcher classes
Symfony/Component/EventDispatcher/EventDispatcher.php
Symfony/Component/HttpKernel/Debug/ContainerAwareTraceableEventDispatcher.php
Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php
but I'm looking for something that is less of a hack/less-destructive.
New to Symfony, but not new to programming. Apologies if this is a naive question, but googling about hasn't revealed what I'm after.
The clean way would be creating your own EventDispatcher which executes your logging or whatever you're trying to do if an event occurs. Have a look at the default one to get an idea of how it works.
Now first create the class
use Symfony\Component\EventDispatcher\EventDispatcher;
class MyDispatcher extends EventDispatcher
{
// sadly those properties aren't protected in EventDispatcher
private $listeners = array();
private $sorted = array();
public function dispatch($eventName, Event $event = null)
{
if (null === $event) {
$event = new Event();
}
$event->setDispatcher($this);
$event->setName($eventName);
// do something with the event here ... i.e. log it
if (!isset($this->listeners[$eventName])) {
return $event;
}
$this->doDispatch($this->getListeners($eventName), $eventName, $event);
return $event;
}
... then register your MyDispatcher as symfony's default one.
( by overwriting the original event_dispatcher service )
app/config/config.yml
services:
event_dispatcher:
class: Vendor\YourBundle\MyDispatcher
arguments: [#service_container]
... or even simpler just override the class parameter being used by symfony when creating the service.
parameters:
event_dispatcher.class: Vendor\YourBundle\MyDispatcher
I'm trying to use the Event System in CakePHP v2.1+
It appears to be quite powerful, but the documentation is somewhat vague. Triggering the event seems pretty straight-forward, but I'm not sure how to register the corresponding listener(s) to listen for the event. The relevant section is here and it offers the following example code:
App::uses('CakeEventListener', 'Event');
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'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->Order->getEventManager()->attach($statistics);
But it does not say where this code should reside. Inside a specific controller? Inside the app controller?
In case it's relevant, the listener will be part of a plugin which I am writing.
Update:
It sounds like a popular way to do this is by placing the listener registration code in the plugin's bootstrap.php file. However, I can't figure out how to call getEventManager() from there because the app's controller classes, etc aren't available.
Update 2:
I'm also told that listeners can live inside Models.
Update 3:
Finally some traction! The following code will successfully log an event when inside of my MyPlugin/Config/bootstrap.php
App::uses('CakeEventManager', 'Event');
App::uses('CakeEventListener', 'Event');
class LegacyWsatListener implements CakeEventListener {
public function implementedEvents() {
return array(
'Controller.Attempt.complete' => 'handleLegacyWsat',
);
}
public static function handleLegacyWsat($event) { //method must be static if used by global EventManager
// Code to update statistics
error_log('event from bootstrap');
}
}
CakeEventManager::instance()->attach(array('LegacyWsatListener', 'handleLegacyWsat'), 'Controller.Attempt.complete');
I'm not sure why, but I can't get errors when I try to combine the two App::uses() into a single line.
Events
Events are callbacks that are associated to a string. An object, like a Model will trigger an event using a string even if nothing is listening for that event.
CakePHP comes pre-built with internal events for things like Models. You can attach an event listener to a Model and respond to a Model.beforeSave event.
The EventManager
Every Model in Cake has it's own EventManager, plus there is a gobal singleton EventManager. These are not all the same instance of EventManager, and they work slightly differently.
When a Model fires an event it does so using the EventManager reference it has. This means, you can attach an event listener to a specific Model. The advantages are that your listener will only receive events from that Model.
Global listeners are ones attached to the singleton instance of EventManager. Which can be accessed anywhere in your code. When you attach a listener there it's called for every event that happens no matter who triggers it.
When you attach event listener in the bootstrap.php of an app or plugin, then you can use the global manager, else you have to get a reference to the Model you need using ClassRegistry.
What EventManager To Use?
If the event you want to handle is for a specific Model, then attach the listener to that Model's EventManager. To get a reference of the model you can call the ClassRegistry::init(...).
If the event you want to handle could be triggered anywhere, then attach the listener to the global EventManager.
Only you know how your listener should be used.
Inside A Listener
Generally, you put your business logic into models. You shouldn't need to access a Controller from an event listener. Model's are much easier to access and use in Cake.
Here is a template for creating a CakeEventListener. The listener is responsible for monitoring when something happens, and then passing that information along to another Model. You should place your business logic for processing the event in Models.
<?php
App::uses('CakeEventListener', 'Event');
class MyListener implements CakeEventListener
{
/**
*
* #var Document The model.
*/
protected $Document;
/**
* Constructor
*/
public function __construct()
{
// get a reference to a Model that we'll use
$this->Document = ClassRegistry::init('Agg.Document');
}
/**
* Register the handlers.
*
* #see CakeEventListener::implementedEvents()
*/
public function implementedEvents()
{
return array(
'Model.User.afterSave'=>'UserChanged'
);
}
/**
* Use the Event to dispatch the work to a Model.
*
* #param CakeEvent $event
* The event object and data.
*/
public function UserChanged(CakeEvent $event)
{
$data = $event->data;
$subject = $event->subject();
$this->Document->SomethingImportantHappened($data,$subject);
}
}
What I like to do is place all my Events into the Lib folder. This makes it very easy to access from anywhere in the source code. The above code would go into App/Lib/Event/MyListener.php.
Attaching The EventListeners
Again, it depends on what events you need to listen for. The first thing you have to understand is that an object must be created in order to fire the event.
For example;
It's not possible for the Document model to fire Model.beforeSave event when the Calendar controller is displaying an index, because the Calendar controller never uses the Document model. Do you need to add a listener to Document in the bootstrap.php to catch when it saves? No, if Document model is only used from the Documents controller, then you only need to attach the listener there.
On the other hand, the User model is used by the Auth component almost every. If you want to handle a User being deleted. You might have to attach an event listener in the bootstrap.php to ensure no deletes sneak by you.
In the above example we can attach directly to the User model like so.
App::uses('MyListener','Lib');
$user = ClassRegistry::init('App.User');
$user->getEventManager()->attach(new MyListener());
This line will import your listener class.
App::uses('MyListener','Lib');
This line will get an instance of the User Model.
$user = ClassRegistry::init('App.User');
This line creates a listener, and attaches it to the User model.
$user->getEventManager()->attach(new MyListener());
If the User Model is used in many different places. You might have to do this in the bootstrap.php, but if it's only used by one controller. You can place that code in the beforeFilter or at the top of the PHP file.
What About Global EventManager?
Assuming we need to listen for general events. Like when ever any thing is saved. We would want to attach to the global EventManager. It would go something like this, and be placed in the bootstrap.php.
App::uses('MyListener','Lib');
CakeEventManager::instance()->attach(new MyListener());
If you want to attach an event listener inside bootstrap.php file of your plugin, everything should work fine using the hints posted in the answers. Here is my code (which works properly):
MyPlugin/Config/bootstrap.php:
App::uses('CakeEventManager', 'Event');
App::uses('MyEventListener', 'MyPlugin.Lib/Event');
CakeEventManager::instance()->attach(new MyEventListener());
MyPlugin/Lib/Event/MyEventListener.php:
App::uses('CakeEventListener', 'Event');
class MyEventListener implements CakeEventListener {
...
}
Event listeners related to MyPlugin are being registered only when the plugin is loaded. If I don't want to use the plugin, event listeners are not attached. I think this is a clean solution when you want to add some functionality in various places in your app using a plugin.
Its' not important, where the code resides. Just make sure its being executed and your events are properly registered & attached.
We're using a single file where all events are attached and include it from bootstrap.php, this ensures that all events are available from all locations in the app.
The magic happens when you dispatch an event, like from an controller action.
$event = new CakeEvent('Model.Order.afterPlace', $this, array('some'=>'data') ));
$this->getEventManager()->dispatch($event);
However, you can dispatch events from anywhere you can reach the EventManager (in Models, Controller and Views by default)