Trying to persist another entity in preUpdate - symfony

I'm trying to persist another entity in my preUpdate event listener, but it's not working...
Here is my code:
public function preUpdate(LifecycleEventArgs $args) {
$entity = $args->getEntity();
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$session = new Session();
$newLog = new Log();
$newLog->setDate(new DateTime());
if(!empty($this->toBePersisted))
{
array_push($toBePersisted, $historique);
}
else
{
$toBePersisted[0] = $historique;
}
}
public function postFlush(PostFlushEventArgs $event)
{
if(!empty($this->toBePersisted)) {
$em = $event->getEntityManager();
foreach ($this->toBePersisted as $element) {
$em->persist($element);
}
$this->toBePersisted = [];
$em->flush();
}
}
But my new log is not persisted...
Do you have any solution ?
Regards

You are using the wrong listeners to implement what you want to do. Quoting from Doctrine documentation for postFlush event:
postFlush is called at the end of EntityManager#flush(). EntityManager#flush() can NOT be called safely inside its listeners.
The right way to implement what you want is inside the onFlush event which is the more powerful doctrine event but is NOT a lifecycle callback. So you have to proper set up your listener in your services.yaml:
App\EventListener\LogListener:
tags:
- { name: doctrine.event_listener, event: onFlush }
and then in onFlush event:
class LogListener {
public function onFlush(OnFlushEventArgs $args) {
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof YourUpdatedEntity) {
return;
}
$newLog = new Log();
$newLog->setDate(new DateTime());
$em->persist($newLog);
$classMetadata = $em->getClassMetadata(Log::class);
$uow->computeChangeSet($classMetadata, $newLog);
}
}
}
The computeChangeSet function call is needed because as stated in the documentation for onFlush event:
If you create and persist a new entity in onFlush, then calling EntityManager#persist() is not enough. You have to execute an additional call to $unitOfWork->computeChangeSet($classMetadata, $entity).

Related

Creating preUpdate or preFlush event with Event Listener

I have a onFlush() event which works fine but what I need to do is to turn that into preFlush() or preUpdate() both acceptable. I did preFlush() but for some reason it doesn't do anything. Not even an error. What am I missing?
TEST: I placed exit in preFlush() to see if it is being called at all or not. Outcome is: 1 so foreach() is never run! It is an empty array. I also tested preUpdate() and all the lines in that get runed but no data inserted.
public function preFlush(PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
echo '1';
foreach ($uow->getScheduledEntityUpdates() as $entity) {
echo '2';
if ($entity instanceof User) {
echo '3';
}
}
exit;
}
I created them after reading the documentation.
service.yml
services:
entity.event_listener.user:
class: Site\FrontBundle\EventListener\Entity\UserListener
tags:
- { name: doctrine.event_listener, event: preUpdate }
- { name: doctrine.event_listener, event: onFlush }
- { name: doctrine.event_listener, event: preFlush }
Working onFlush() example:
class UserListener
{
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof User) {
$userLog = new UserLog();
$userLog->setDescription($entity->getId() . ' being updated.');
$em->persist($userLog);
// Instead of $em->flush() cos we're already in flush process
$userLogMetadata = $em->getClassMetadata(get_class($userLog));
$uow->computeChangeSet($userLogMetadata, $userLog);
}
}
}
}
Not working preFlush() example:
class UserListener
{
public function preFlush(PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof User) {
$userLog = new UserLog();
$userLog->setDescription($entity->getId() . ' being updated.');
$em->persist($userLog);
// Instead of $em->flush() cos we're already in flush process
$userLogMetadata = $em->getClassMetadata(get_class($userLog));
$uow->computeChangeSet($userLogMetadata, $userLog);
}
}
}
}
Not working preUpdate() example
class UserListener
{
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
if ($entity instanceof User) {
$userLog = new UserLog();
$userLog->setDescription($entity->getId() . ') been updated.');
$em = $args->getEntityManager();
$em->persist($userLog);
$userLogMetadata = $em->getClassMetadata(get_class($userLog));
$uow->computeChangeSet($userLogMetadata, $userLog);
}
}
}
SOLUTION:
The trick is, persisting after preUpdate() within postFlush() event.
Note: Although this might not be the best solution, it answers the question however it could be done with an Event Subscriber or simple onFlush() -> $uow->getScheduledEntityUpdates() in an Event Listener.
Service.yml
services:
entity.event_listener.user_update:
class: Site\FrontBundle\EventListener\Entity\UserUpdateListener
tags:
- { name: doctrine.event_listener, event: preUpdate }
- { name: doctrine.event_listener, event: postFlush }
Event Listener
<?php
namespace Site\FrontBundle\EventListener\Entity;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Site\FrontBundle\Entity\User;
use Site\FrontBundle\Entity\UserLog;
class UserUpdateListener
{
private $log = array();
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// False check is compulsory otherwise duplication occurs
if (($entity instanceof User) === false) {
$userLog = new UserLog();
$userLog->setDescription($entity->getId() . ' being updated.');
$this->log[] = $userLog;
}
}
public function postFlush(PostFlushEventArgs $args)
{
if (! empty($this->log)) {
$em = $args->getEntityManager();
foreach ($this->log as $log) {
$em->persist($log);
}
$em->flush();
}
}
}
Reading the docs,
http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#onflush
it doesn't mention that preFlush has the infor about changes(I mean the entityManager)
if you look at Doctrine\ORM\UnitOfWork you see that change sets are computed after preFlush event so you should use onFlush if you want to interact with changed entities
// Raise preFlush
if ($this->evm->hasListeners(Events::preFlush)) {
$this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
}
// Compute changes done since last commit.
if ($entity === null) {
$this->computeChangeSets();
} elseif (is_object($entity)) {
$this->computeSingleEntityChangeSet($entity);
} elseif (is_array($entity)) {
foreach ($entity as $object) {
$this->computeSingleEntityChangeSet($object);
}
}
Dear BentCoder:
I'm using Symfony 2.7 version. When using $em->flush in event listener like your post, this error was occur:
click-here-to-see-bug-description
And, this is my solution:
Service.yml
services:
app.listener:
class: AppBundle\EventListener\DoctrineListener
arguments: ["#service_container"]
tags:
- { name: doctrine.event_listener, event: preUpdate, method: preUpdate }
- { name: doctrine.event_listener, event: postUpdate, method: postUpdate }
Event Listener
namespace AppBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity;
/**
* Log activity on website
* Class DoctrineListener
* #package AppBundle\EventListener
*/
class DoctrineListener
{
/**
* #var ContainerInterface
*/
private $_container;
/**
* #var Array
*/
private $_activities;
/**
* DoctrineListener constructor.
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->_container = $container;
}
/**
* #param LifecycleEventArgs $args
*/
public function preUpdate(LifecycleEventArgs $args)
{
$entityManager = $args->getEntityManager();
$entity = $args->getEntity();
$activityEntity = new Entity\Activity();
$activityEntity->setAction(Entity\Activity::ACTION_EDIT);
$activityEntity->setActionAt(new \DateTime());
$activityEntity->setIpAddress($this->_container->get('request')->getClientIp());
switch (true) {
case $entity instanceof Entity\Goods:
$repository = $entityManager->getRepository('AppBundle:Goods');
$activityEntity->setType(Entity\Activity::TYPE_GOODS);
$message = 'User: <strong>%s</strong> sửa mẫu hàng hóa <strong>%s</strong>';
break;
default:
return;
}
if (isset($repository) && $args->getEntityChangeSet()) {
$user = $this->_container->get('security.context')->getToken()->getUser();
$recordBefore = clone $entity;
foreach ($args->getEntityChangeSet() as $key => $value) {
$method = 'set'.ucfirst($key);
$recordBefore->$method($value[0]);
}
$activityEntity->setFosUser($user);
$activityEntity->setRecordBefore(serialize($recordBefore));
$activityEntity->setRecordAfter(serialize($entity));
$activityEntity->setMessage(
sprintf(
$message,
$user->getUserProfile()->getFullName(),
(string) $recordBefore
)
);
$this->_activities[] = $activityEntity;
}
return;
}
/**
* #param LifecycleEventArgs $args
*/
public function postUpdate(LifecycleEventArgs $args)
{
if (sizeof($this->_activities)) {
$entityManager = $args->getEntityManager();
foreach ($this->_activities as $activity) {
$entityManager->persist($activity);
}
$entityManager->flush();
}
}
}
Hope this will help someone!

Checking which event was fired in Event Subscriber

When I insert data into User entity, subscriber below gets fired for pre and post events so I get two records inserted into Dummy entity which is fine up to here. What I need to know is, how can I check which event was fired so that I can use it in setHow() method?
$dummy->setHow(......);
Expected result in Dummy table:
id createdOn how
1 2014-10-16 12:12:00 prePersist
2 2014-10-16 12:12:01 postPersist
Subscriber:
class UserPost implements EventSubscriber
{
public function getSubscribedEvents()
{
return array('prePersist', 'postPersist');
}
public function prePersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$entity = $args->getEntity();
if ($entity instanceof User) {
$dummy = new Dummy();
$dummy->setCreatedOn(new \DateTime('now'));
$dummy->setHow(.............);
$em->persist($dummy);
$em->flush();
}
}
}
Service:
Service:
entity.subscriber.user_post:
class: Site\MainBundle\EventSubscriber\Entity\UserPost
tags:
- { name: doctrine.event_subscriber }
You can just pass the name of the event in the method call like..
public function prePersist(LifecycleEventArgs $args)
{
$this->index($args, 'prePersist');
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args, 'postPersist');
}
public function index(LifecycleEventArgs $args, $event)
{
...
if ($entity instanceof User) {
$dummy = new Dummy();
$dummy->setCreatedOn(new \DateTime('now'));
$dummy->setHow($event);
...
}
}

Very simple example of event listener on preUpdate() event of an object hangs/fails

Event Listener example below works fine for prePersist() and postPersist() however browser times out for preUpdate() and postUpdate(). Anyone knows why this is happening?
Note: Event listener is the one which cause problem because controller works fine when used on its own. I checked the database.
Error:
Maximum execution time of 30 seconds exceeded in /var/www/html/local/listener/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 685
Event listener
class UserListener
{
//public function postUpdate(LifecycleEventArgs $args)
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof User) {
$userLog = new UserLog();
$userLog->setDescription('User Update.');
$em = $args->getEntityManager();
$em->persist($userLog);
$em->flush();
}
}
}
Controller:
public function updateUser()
{
$repo = $this->getDoctrine()->getRepository('SiteFrontBundle:User');
$user = $repo->findOneBy(array('id' => 1));
$user->setLock(true);
$em = $this->getDoctrine()->getManager();
$em->flush();
}
Service.yml
services:
entity.event_listener.user:
class: Site\FrontBundle\EventListener\Entity\UserListener
tags:
- { name: doctrine.event_listener, event: preUpdate }
preUpdate is very limited.
From: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#preupdate
Any calls to EntityManager#persist() or EntityManager#remove(),
even in combination with the UnitOfWork API are strongly discouraged and
don’t work as expected outside the flush operation.
Probably need to use onFlush, http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#onflush
Might also want to take look at how these guys do it: https://github.com/Atlantic18/DoctrineExtensions/tree/master/lib/Gedmo/Loggable
Or just setup a logging entity manager.

OneToMany related entity not updating with preUpdate listener

I have set up an event listener with a preUpdate function, and I am trying to use this to update an address entity related through a OneToMany relationship. The function runs, and the object is updated, but this is not then persisted tot he database.
You can see the code below, does anyone have any ideas?
public function preUpdate(LifecycleEventArgs $args){
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Company) {
$key = 0;
$areaCode = explode(' ', $this->parsePostcode($entity->getAddresses()[$key]->getPostCode() ));
$geo = $entityManager->getRepository('AlphaRMSBundle:SelGeo')->findOneByPostcode($areaCode[0]);
if ($geo instanceof SelGeo) {
$entity->getAddresses()[$key]->setEastings($geo->getEastings());
$entity->getAddresses()[$key]->setNorthings($geo->getNorthings());
$entity->getAddresses()[$key]->setLatitude($geo->getLatitude());
$entity->getAddresses()[$key]->setLongitude($geo->getLongitude());
$entity->getAddresses()[$key]->setMunicipality($geo->getTownCity());
$entity->getAddresses()[$key]->setRegion($geo->getRegion());
$entity->getAddresses()[$key]->setCountry($geo->getCountryString());
$entity->getAddresses()[$key]->setCountryUkCode($geo->getCountryUkCode());
//die($entity->getAddresses()[$key]->getMunicipality());
}
}
}

Get scheduled extra updates from Doctrine UnitOfWork

I have a Doctrine Event Listener that listens to the onFlush event. I use it to update an eTag on an entity when saved.
I need to get access to the entities scheduled for deletion, so I can access their associated object, however:
I'm using a soft-deletable filter, so the entities aren't actually in $uow->getScheduledEntityDeletions(), they're in $uow->extraUpdates marking the deleted flag changed instead.
This variable is private and I don't know of any programatic way to get notified of this change. Any ideas?
private function updateIfRequired(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// Entities either updated or just inserted
$upsertedEntities = array_merge(
$uow->getScheduledEntityUpdates(),
$uow->getScheduledEntityInsertions()
);
foreach ($upsertedEntities as $entity) {
if ($entity instanceof ETaggableInterface || $entity instanceof ETagRelatedInterface) {
$this->updateETag($entity);
}
}
// When soft-deleted, this and getScheduledEntityUpdates are both empty!
$deletedEntities = $uow->getScheduledEntityDeletions();
foreach ($deletedEntities as $entity) {
$this->deletedEntities[spl_object_hash($entity)] = $entity;
$this->updateETag($entity);
}
}
So, the detailed answer for this question would be something like this:
Listen to the preSoftDelete event (Symfony2 DoctrineExtensions preSoftDelete event call):
tags:
- { name: doctrine.event_listener, event: preSoftDelete }
Then in your listener class, add the following method:
public function preSoftDelete(LifecycleEventArgs $args){
$entity = $args->getEntity();
$em = $args->getEntityManager();
if ($entity instanceof ETaggableInterface || $entity instanceof ETagRelatedInterface) {
$entity->updateEtag('foo'); // Was not clear what $this->updateEtag() do
$em->persist($entity);
$classMetadata = $em->getClassMetadata(get_class($entity));
$em->getUnitOfWork()->computeChangeSet($classMetadata, $entity);
}
}
This will update the entity, tell to persist it, calculate the changes.
Maybe you should listen to preSoftDelete events: https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php#L74

Resources