Stop prePersist LifeCycleEvent being recursive in Doctrine2 Entiy - php

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.

Related

Doctrine - Difference between injected entities in entity listener

As we can see in official documentation Doctrine implements entity listeners which are executed only when something happens on a specific entity.
However there is a different injection between lifecycle event listeners/subscribers and entity listeners. In fact for listeners/subscribers Doctrine injects only a LifecycleEventArgs object into defined callbacks
// Event listener/subscriber
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
}
which gives you anyway access to the entity, but then in an entity listener the entity is also injected explicitly as first argument
// Entity listener
public function postUpdate(object $entity, LifecycleEventArgs $args)
{
$entity2 = $args->getEntity();
}
and it's still available in $args. This is also reported in documentation:
An entity listener method receives two arguments, the entity instance
and the lifecycle event.
But then what's exactly the difference between $entity and $args->getEntity() in an entity listener?
It's the same entity.
As LifecycleEventArgs is injected even in "generic" doctrine listener, you need to retrieve entity object. On the others side the LifecycleEventArgs has a lot of things you can retrieve in both situation.
For this rease, they've used the same object for both operations: it's pretty common and acceptable from my POV.
You can verify it yourself by doing something like
// Entity listener
public function postUpdate(object $entity, LifecycleEventArgs $args)
{
$entity2 = $args->getEntity();
dump(spl_obj_hash($entity) == spl_obj_hash($entity)); // you can echo this, or log, or VarDump, or whatever
}

Symfony observer pattern - How to dispatch custom event when setting entity property?

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.

Doctrine LifecycleEventArgs return Gedmo\Loggable\Entity\LogEntry

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.

Best way to implement a log with doctrine and sonata-admin

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.

Prevent entity deletion in doctrine 2 listener

I need to make entities to change status except being deleted. For those I added a listener for on flush method. So, I can see all the entities, that being deleted, but cannot prevent them to be deleted. Is it possible?
Throwing a Exception in the event handler cut the transaction and rollback the changes.
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledCollectionDeletions() AS $col) {
throw new Exception('avoid delete');
}
}
Just install doctrine extension https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/softdeleteable.md or use it as an example to develop your own solution

Categories