I have 2 entities, Invoice and Advance. Advances are related to Invoices, with ManyToOne relation. And when adding or editing and advance, I want the Invoice total to be edited accordingly. I have a listener set for Advances postPersist which looks like this :
class PostPersistListener
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$em = $args->getEntityManager();
// Advance Listener
if ($entity instanceof Advance) {
// Modification du total de la facture mère
$invoice = $entity->getInvoice();
echo '<pre>';
\Doctrine\Common\Util\Debug::dump($invoice);
echo '</pre>';
$newTotal = $invoice->getTotalMaster() - $entity->getTotalMaster();
$invoice->setTotalMaster($newTotal);
echo '<pre>';
\Doctrine\Common\Util\Debug::dump($invoice);
echo '</pre>';
$em->flush();
}
}
}
The event is well triggered. The first dump() displays something like this :
object(stdClass)#1379 (49) {
["__CLASS__"]=>
string(32) "Evo\BackendBundle\Entity\Invoice"
["id"]=>
int(1)
["totalMaster"]=>
float(250)
}
And the second dump() this :
object(stdClass)#1379 (49) {
["__CLASS__"]=>
string(32) "Evo\BackendBundle\Entity\Invoice"
["id"]=>
int(1)
["totalMaster"]=>
float(240)
}
So the "totalMaster" property is modified. But the $em->flush() doesn't seem to update the field in the database. Am i missing something ?
To start with, as it's name implies, PostPersist only get's triggered after the initial persist operation i.e. only for new objects. Editing an existing object will not trigger the event.
From: http://docs.doctrine-project.org/en/latest/reference/events.html#lifecycle-events
postPersist - The postPersist event occurs for an entity after the entity has been made persistent.
It will be invoked after the database **insert** operations.
Generated primary key values are available in the postPersist event.
There are also strict limits to what you can do with these events. Basically the flush operation is already underway so anything that updates an entity is problematical at best. Specifically:
From: http://docs.doctrine-project.org/en/latest/reference/events.html#postupdate-postremove-postpersist
The three post events are called inside EntityManager#flush().
Changes in here are not relevant to the persistence in the database,
but you can use these events to alter non-persistable items, like non-mapped fields, logging or
even associated classes that are directly mapped by Doctrine.
It's barely possible you might get this to work using prePersist and preUpdate but those have their own issues.
#kix has the correct approach. Updating an invoice total is a domain function and should be handled using a domain event.
P.S. You don't need to call persist on existing objects retrieved through Doctrine.
You've missed the $em->persist($invoice) line.
Also, I'd suggest implementing this logic with Symfony's built-in events, not with prePersist listeners. The simple reason behind this is that your code is dealing with business logic and not persistence logic. You should dispatch an AdvanceModified event with the event_dispatcher service (it should be available to your controller) and then process it in a listener.
Related
I have a method to get a change set of Entity from the unit of work (entity manager) in Symfony and I would like it to receive all Entities (Post, User...) instead of specific Entity.
public function getChanges(Post $event): array
{
$uow = $this->entityManager->getUnitOfWork();
$uow->computeChangeSets();
return $uow->getEntityChangeSet($event);
}
Do you have any idea to do it?
One solution is getting the object as the argument but I prefer to get only the Symfony Entity objects in the function.
Look for doctrine entity listener.
https://symfony.com/doc/current/doctrine/events.html#doctrine-entity-listeners
And do not filter entity inside it, remove this part from the example:
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof Product) {
return;
}
if (!$this->entityManager->contains($entity)) {
throw new Exception('The given object must be doctrine entity');
}
In my application, i have "users".
One user can have several "accounts"
I have a listener on my "account" entity.
It is declared on "service.yml" file like this :
account_listener:
class: AppBundle\EventListener\AccountListener
arguments:
- '#service_container'
tags:
- {name: doctrine.event_listener, event: preUpdate}
In my service, the method preUpdate :
public function preUpdate(PreUpdateEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if (!$entity instanceof Account) {
return;
}
$this->container->get('notification_manager')->sendNotification();
}
the sendNotification method call a function that try to create an entity "notification"
public function sendNotification()
{
$notification = new Notification();
$data = array(
'label' => 'Hello'
)
$form_notif = $this->formFactory->create(NotificationType::class, $notification, ['method' => 'POST']);
$form_notif->submit($data,($method === 'POST'));
if ($form_notif->isValid())
{
$this->em->persist($notification);
$this->em->flush();
} else {
return $form_notif;
}
return $notification;
}
The problem :
The notification is not created and php is stuck in an infinite loop.
To prevent this, i added this at the beggining of sendNotification method :
$eventManager = $this->em->getEventManager();
$eventManager->removeEventListener(['preUpdate'],$this->container->get('account_listener'));
With this, it works. But i think there is a better way.
Can you help me ?
Thanks
If you call a service which call a flush, i think the removeEventListener method is the not a bad way to avoid infinite loop.
If you really don't want to call removeEventListener, you have to change your pattern and not call flush in a doctrine event.
One alternative is to use a third service with the collection of object you want to flush (in your case, a NotificationStack class with a single collection and few getter/setter).
Your sendNotification method will add element to this collection (without flushing them).
Then, you can flush all that collection on the kernel.response event (and/or console.terminate if needed).
Also, inject the container in a service is a bad pratice, you should inject only needed services and or parameters.
Hope it will helps
the reason for php stuck is as below.
In your code you call the preUpdate(), which called when you insert or update the entity....
Now when your sendNotification action will called and you save Notification() at that time eventlistener will called, and from eventlistener it again call sendNotification method and so on..... This will create recursive loop, that's why ypur php stuck.
Hope it will help you.
I had things set up that through a form you could add an Alert to my database. When this Alert was added, a postFlush event took the Alert and performed a task on it. This all worked perfectly.
I have now changed things. Instead of adding an Alert through a form, I display all possible Alerts they can have. They can then select whatever alerts they want an add these to the database. So whereas before only one Alert was added at a time, now multiple Alerts can be added.
So the problem now is that in my Controller, an Alert is added using a foreach
foreach ($data as $flightNum=>$seat) {
$alert = new Alert();
$alert->setLastUpdated();
$alert->setIsDeleted(0);
$alert->setAlertStatus('Active');
$flightNumber = new FlightNumbers();
$flightNumber->setAvailabilityAlert($alert);
$flightNumber->setFlightNumber($flightNum);
$alert->addFlightNumber($flightNumber);
$em->persist($flightNumber);
$em->persist($alert);
$em->flush();
}
The problem with this is that my postFlush only seems to execute now for the first Alert that is added. So if I choose three Alerts, the first one has the additional postFlush action performed on it but the other two do not.
I have played about with my postFlush to try using an Array instead, does not seem to work though
class AlertListener
{
protected $api_service;
protected $alertEntity = array();
protected $em = null;
public function __construct(UapiService $api_service)
{
$this->api_service = $api_service;
}
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof Alert) {
array_push($this->alertEntity, $entity);
var_dump("TEST");
}
}
public function postFlush(PostFlushEventArgs $args)
{
$this->em = $args->getEntityManager();
$eventManager = $this->em->getEventManager();
$eventManager->removeEventListener('postFlush', $this);
\Doctrine\Common\Util\Debug::dump($this->alertEntity);
if (!empty($this->alertEntity)) {
foreach($this->alertEntity as $alert) {
$this->api_service->addFlightsAction($alert);
}
}
}
}
Interesting thing is, the var_dump outputs twice if I choose 2 Alerts, which would leave me to believe the correct number of Alerts are added to the Array.
However, the Symfony2 dump of the Object only outputs one Alert, so apparently not. Interesting thing is that if I dump the array in the postPersist function I get loads of Alerts listed. If I dump in the postFlush function, only one Alert is outputted.
How can I handle multiple Alerts here?
Thanks
First of all you shouldn't flush inside a loop, for performance sake flush outside, then get an array of your Alerts in your Postflush event from PostFlushEventArgs , then use them for whatever you want.
I have a reletivly simple change logging class that stores the date, an integer to indicate the type of change and 2 varchar(50)s that hold the old and new data for the change.
I can create and populate an instance of the class but when I come to flush it I get an "Error: Maximum function nesting level of '200' reached, aborting!" error.
I've read about the Xdebug issue and configured the max nests up to 200 but as you can see this isn't enough. The save process should be very simple and there should be no need for so many nested functions, so increasing it further will just hide the problem, whatever it is. I have far more complicated classes in this app that persisit and flush without a problem.
The issue is always at
NormalizerFormatter ->normalize ()
in app/cache/dev/classes.php at line 4912 -
Having looked at this a bit more I think the issue may be that the change instance is created and saved during the preUpdate event of another class:
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof Property) {
$entityManager = $eventArgs->getEntityManager();
$changeArray = $eventArgs->getEntityChangeSet();
foreach ($changeArray as $field => $values) {
$eventType = "";
switch ($field) {
case 'price' :
$eventType = PropertyEvent::EVENTTYPE_PRICE;
BREAK;
case 'status' :
$eventType = PropertyEvent::EVENTTYPE_STATUS;
BREAK;
}
if ($eventType != "") {
$event = new PropertyEvent($entity->getID(), $eventType, $values[0], $values[1]);
$entityManager->persist($event);
$entityManager->flush();
}
}
$entity->setUpdatedDate();
}
}
Why would that be an issue?
Doctrine only has one lifecycle event process so regardless of the entity you're using adding a flush within a lifecycle event will send you back round the loop and into your event handler again... and again ... and...
Have a look at this blog post
http://mightyuhu.github.io/blog/2012/03/27/doctrine2-event-listener-persisting-a-new-entity-in-onflush/
Basically the answer is to use
$eventManager -> removeEventListener('preUpdate', $this);
to remove the event. Then create the new entity, persist, flush and finally re-attach the event you are in.
$eventManager -> addEventListener('preUpdate', $this);
I think your issue is the $entity->setUpdatedDate(); call, which probably calls another update event and re-calls your handler.
Hi I using Doctrine 2 listener to check if user group was change.
So I have
- {name: doctrine.event_listener, event: preUpdate } i my service.yml
The method is executed and Im doing such thing
$user = $args->getEntity();
$em = $args->getEntityManager();
if($user instanceof \iTracker\UserBundle\Entity\User) {
$u = $em->getRepository('UserBundle:User')->find($user->getId());
var_dump($u->getUserGroup());
var_dump($user->getUserGroup());
}
And both object are this same. Should object $u with old user group and $user with new group ?? Event is preUpdate so before update object should be different.
Have a look at the Doctrine documentation to see what you can get in the preUpdate lifecycle callback: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#preupdate
You have direct access to the original and changed values, so you don't have to query the database.
And to answer your question, why the two values are the same: I'm not 100% sure on that but most probably the EntityManager actually understands that you want to retrieve the same object as you already have, so it returns it without querying the database. To actually query the DB again you would have to somehow refresh the object in the EM (which will probably end up in loosing your changes).