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);
...
}
}
Related
Changes to my Entities are logged using an EventSubscriber for Doctrine lifecycle events. I want to log a request id alognside the entity change log entries to see what's happened in one single user action.
Adding the request id is as easy as this:
class RequestIdSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => 'addRequestId'];
}
public function addRequestId(GetResponseEvent $event): void
{
$request = $event->getRequest();
$request->attributes->set('RequestId', Uuid::uuid1()->toString());
}
}
The problem is that the request does not seem to be available in the Doctrine EventSubscribers in a reliable way:
class EntityEventSubscriber implements EventSubscriber
{
public function __construct(DelayedEventDispatcher $dispatcher, RequestStack $requestStack, LoggerInterface $logger)
{
$this->dispatcher = $dispatcher;
$this->inventory = new EntityInventory();
$this->requestStack = $requestStack;
$this->logger = $logger;
}
public function getSubscribedEvents(): array
{
return [
Events::postUpdate,
];
}
public function postUpdate(LifecycleEventArgs $args): void
{
// works
$this->logger->debug($this->requestStack->getCurrentRequest()->get('RequestId'));
$entity = $args->getEntity();
$changes = $this->inventory->getChangeSet($entity);
$event = new EntityUpdatedEvent($entity, $changes);
$this->triggerAuditLogEvent($event);
}
public function triggerAuditLogEvent(EntityEvent $event): void
{
// request.CRITICAL: Uncaught PHP Exception Error: "Call to a member function get() on null"
$event->setRequestId($this->requestStack->getCurrentRequest()->get('RequestId'));
$this->dispatcher->dispatch(FhrEvents::GENERIC_ENTITY_EVENT, $event);
}
}
So what really bothers me is that the request seems to be available in one method and if I call the next one, it's already gone.
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).
I have a problem with my project :
Not called :
public function postRemove(LifecycleEventArgs $event)
{
$test = 1;
$test1 = 2;
}
Called :
public function preRemove(LifecycleEventArgs $event)
{
$test = 1;
$test1 = 2;
}
Strange that postRemove is not called. I confirm that the entity is deleted from database. Please give an idea.
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
'postRemove' // !!!
);
}
public function postUpdate(LifecycleEventArgs $args)
{
dump('Update');
}
public function postPersist(LifecycleEventArgs $args)
{
dump('Persist');
}
public function postRemove(LifecycleEventArgs $args)
{
dump('Remove');
}
when I register a new Plasmid Entity, I want give him an automatic name (like: p0001, p0002, p0003), to do this, I need to select in the database the last Plasmid entity for a specific User, get its autoName, and use this previous name to define the new one.
But, when I inject the token_storage in my listener, the token is null... In the controller, I can have the user, it's work.
The service.yml
app.event_listener.plasmid:
class: AppBundle\EventListener\PlasmidListener
arguments: ["#security.token_storage"]
tags:
- { name: doctrine.event_listener, event: prePersist }
And, the PlasmidListener
class PlasmidListener
{
private $user;
public function __construct(TokenStorage $tokenStorage)
{
$this->user = $tokenStorage->getToken()->getUser();
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// If the entity is not a Plasmid, return
if (!$entity instanceof Plasmid) {
return;
}
// Else, we have a Plasmid, get the entity manager
$em = $args->getEntityManager();
// Get the last plasmid Name
$lastPlasmid = $em->getRepository('AppBundle:Plasmid')->findLastPlasmid($this->user);
// Do something with the last plasmid in the database
}
}
If someone know why I can get the actual user in the Doctrine Listener ?
Thanks
I think that you should store pointer to tokenStorage class in your service instead of user object:
class PlasmidListener
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function prePersist(LifecycleEventArgs $args)
{
$user = $this->tokenStorage->getToken()->getUser();
//...
}
}
To avoid error in Symfony4 and above, use TokenStorageInterface instead of TokenStorage
For example
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
And in your constructor :
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
To get the user and its details in prePersist :
$user = $this->tokenStorage->getToken()->getUser();
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!