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.
Related
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.
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 }
I'm trying to work with doctrine event in a symfony project,
following the symfony doc I have this code
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Rubrique) {
return;
}
$entityManager = $args->getEntityManager();
// do some stuff
}
The problem is that $entity is not the expected object Rubrique but an instance of Gedmo\Loggable\Entity\LogEntry maybe because Rubrique is Loggable. How can I access to my entity for manipulate it as I want ?
Thanks
This event listener is a "generic" one and not an doctrine entity listener
This means that the event is raised for each entity persisted: if you have a relation between Rubrique and LogEntry, than is possible that you're checking only for the "first" of them being "postPersisted".
If you need a specific listener only for that kind of entity, think about using doctrine entity listener (linked above).
Moreover remember that "generic" listener will listen (or will be subscribed) for events of every entity (so, basically, it could be invoked a lot of times) wheres doctrine entity listener not.
I am using a the prePersist LifeCycleEvent to update an Entity, updating this Entity creates a loop as the object is re-saved over and over again to the database.
public function doSomething(LifecycleEventArgs $event)
{
// Stuff here
$em = $event->getEntityManager();
$em->persist($entity);
$em->flush();
}
How can I have the Entity update itself, without causing his recursive loop?
As the prePersist event is triggered before the entity is actually scheduled for insertion in the unit of work, you can just change the entity state here without having to manually call persist/flush.
Simply do your stuff in your callback and don't bother about the entity manager.
I have an entity "Entity1", that have an OneToOne (one-direction) relation with another entity "Entity2". Entity2 is managed by a different EntityManager.
Here is part of the Entity1:
/**
* #ORM\OneToOne(targetEntity="Entity2")
* #ORM\JoinColumn(name="Entity2ID", referencedColumnName="ID")
**/
protected $entity2;
Entity2 already exists, Entity1 is a new entity. Here is part of the persisting logic:
$obj1 = new Entity1();
$obj1->setEntity2($this->entity2Manager
->getRepository('VendorBunlde:Entity2')
->find($entity2Id));
$this->entity1Manager->persist($obj1);
$this->entity1Manager->flush();
I got this error:
A new entity was found through the relationship 'MyBundle\\Entity\\Entity1#entity2' that was not configured to cascade persist operations for entity: VendorBundle\\Entity\\Entity2#0000000008fbb41600007f3bc3681401. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={\"persist\"}). If you cannot find out which entity causes the problem implement 'VendorBundle\\Entity\\Entity2#__toString()' to get a clue
How can I force Doctrine to skip persisting Entity2 and in the same time keep the relation? I tried "->merge($obj1)" it works but I get null when I call $obj1->getId()?!
I would try the detach method:
An entity is detached from an EntityManager and thus no longer managed
by invoking the EntityManager#detach($entity) method on it or by
cascading the detach operation to it. Changes made to the detached
entity, if any (including removal of the entity), will not be
synchronized to the database after the entity has been detached.
$obj1 = new Entity1();
$obj2 = $this->entity2Manager
->getRepository('VendorBunlde:Entity2')
->find($entity2Id);
$obj1->setEntity2($obj2);
$this->entity1Manager->detach($obj2);
$this->entity1Manager->persist($obj1);
$this->entity1Manager->flush();
Haven't tried though, please let me know if it works, thanks.
This solution works for me: Using Relationships with Multiple Entity Managers
I removed the relationship between entity1 & entity2.
Added #PostLoad to load entity2.