Symfony2 - PostFlush Event - symfony

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.

Related

Doctrine EventListener onFlush does not save original entity

I've been struggling with an annoying issue for a while. I'm trying to create Notification entities associated to InventoryItems that expire.
These InventoryItems are generated automatically for users, but users can edit them and set expiry date on them individually. Upon saving, if an InventoryItem has an expiry date, a Notification entity is generated and associated to it. So these Notification entities are created when an entity is updated and hence onPersist event is not going to work.
Everything seems to work fine and Notifications are generated upon saving InventoryItems as expected. Only problem is, when a Notification is created for the first time, even though it's saved properly, changes to the InventoryItem are not saved. That is a Notification with correct expiry date is created, but the expiry date is not saved on the InventoryItem.
Here's my onFlush code:
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof NotificableInterface) {
if ($entity->generatesNotification()){
$notification = $this->notificationManager->generateNotificationForEntity($entity) ;
if ( $notification ) {
$uow->persist($notification) ;
}
$entity->setNotification($notification) ;
$uow->persist($entity);
$uow->computeChangeSets();
}
}
}
}
The problem only occurs the first time a Notification is associated to an entity, i.e. the first time an expiry date is set on an InventoryItem. In later instances when expiry date is updated, the update is reflected correctly on both the Notification and InventoryItem.
Any ideas or suggestions would be appreciated.
Thanks
You need to call computeChangeset specifically on your newly created or updated entity. Just calling computeChangeSets is not enough.
$metaData = $em->getClassMetadata('Your\NameSpace\Entity\YourNotificationEntity');
$uow->computeChangeSet($metaData, $notification);
Thanks Richard. You pointed me to the right direction. I needed to recompute the change set on the parent entity (InventoryItem) to get things working properly. Additionally I had to call computeChangeSets on the unit of work to get rid of the invalid parameter number error (such as the one explained symfony2 + doctrine: modify a child entity on `onFlush`: "Invalid parameter number: number of bound variables does not match number of tokens")
Note I ended up also removing:
if ( $notification ) {
$uow->persist($notification) ;
}
Which never made sense, since I had set cascade persist on the association in my entity and should have cascaded down automatically.
My final solution is:
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof NotificableInterface) {
if ($entity->generatesNotification()){
$notification = $this->notificationManager->generateNotificationForEntity($entity) ;
$entity->setNotification($notification) ;
// if ( $notification ) {
// $uow->persist($notification) ;
// }
$metaData = $em->getClassMetadata(get_class($entity));
$uow->recomputeSingleEntityChangeSet($metaData, $entity);
$uow->computeChangeSets();
}
}
}
}

Symfony2 error Flushing an entitiy within the preUpdate event of another entiity

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.

postPersist event not updating database

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.

Event Subscriber postUpdate query is not updated

I'm trying to compare data on the database before and after an upgrade, in order to perform the operations.
Specifically:
Association: Artist ManyToMany Soundtrack.
When I remove an artist by a record soundtrack, I check if that artist has been without associations, if true removes the artist.
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$em = $args->getEntityManager();
if ($entity instanceof Soundtrack) {
//$this->difference contains the difference between the artists before and the artists after the change.
if(!empty($this->difference)) {
foreach($this->difference as $m) {
$art = $em->getRepository('AcmeUserBundle:Artist')->findArtistByIdJoinSoundtrack($m);
var_dump($art->getId());
}
}
}
}
query findArtistByIdJoinSoundtrack():
public function findArtistByIdJoinSoundtrack($id) {
$qb = $this->createQueryBuilder('a');
$query = $qb->select(array('partial a.{id}'))
->innerJoin('a.soundtrack', 's')
->where('a.id = :id')
->setParameter('id', $id)
->setMaxResults(1)
->getQuery();
return $query->getSingleResult();
}
The problem comes when I run a query to see if the artist has been without associations, in fact querying in postUpdate:
$em->getRepository('AcmeUserBundle:Artist')->findArtistByIdJoinSoundtrack($m);
for example, if an artist had only one association with the soundtrack with id 71
and I just removed from the soundtrack with id 71 that artist, the query does not return an empty response but returns an element soundtrack with just 71 id.
In other words it is as if the database is not updated despite having used the event postUpdate that is invoked after the flush().
Why does this happen?
I also cleared the cache several times to make sure it was not that's the problem!
Please have a look at my answer here. Generally changing related entities is not allowed using a preUpdate listener. Your thought ...
[...] event postUpdate [...] is invoked after the flush().
... is not exactly correct - postUpdate is not invoked after flush() but inside.
The three post [postUpdate, postRemove, postPersist] events are called inside EntityManager#flush(). Changes
in here are not relevant to the persistence in the database.
(documentation)

How to customize symfony2 forms upon retrieval?

I have a case where we create registration for sports events.
The registration contains some fields specific to each sport. Some of which will be named similarly although they will be different for each sport. Example: "favorite position on the field":
For Basketball it would be a choice field between:
Point guard
Shooting guard
etc...
For baseball, it would be the same choice field but with some different choices available:
Pitcher
Infield
Outfield
...
When first creating the form (for display), the sport is passed as part of the data in the registration:
$registration = new Registration;
$registration->setEvent($event);
and $event->getSport(); would return the sport for that event.
So far so good, and adding a listener to the generation of my form, I can set only the fields specific to that sport:
public static function getSubscribedEvents()
{
return [FormEvents::POST_SET_DATA => 'preSetData'];
}
/**
* #param event DataEvent
*/
public function preSetData(DataEvent $event)
{
$form = $event->getForm();
if (null === $event->getData()) {
return;
}
// (The get event here means the real life sports gathering)
$sport = $event->getData()->getEvent()->getSport();
/**
* Then I customize the fields depending on the current sport
*/
}
The problem comes when the user submits this form back. In this case, $event->getData()->getEvent() is null.
The "event" (real life one) is a document_id field in the registration form (using MongoDB here).
If I listen to the ::BIND event instead of ::PRE_SET_DATA, then I can access everything, but it's too late to customize the form as it is already bound. ::PRE_BIND does the same as ::PRE_SET_DATA.
How can I correctly retrieve my Event and Sport Documents here in order to customize my form and validate it appropriately?
Why would you need an event to do such task? You can define the fields in the buildForm() action of the form class. To access the event object simply use $options['data']->getEvent()
So ... Finally found how to do this properly.It requires subscribing to two different events.
First time the form is built, some data is passed to it, therefore, the PRE_SET_DATA event contains that data and everything works fine as explained in the question.
On the moment the form is submitted, it is first created with NO data, therefore the data accessed in PRE_SET_DATA will be null. In this case we skip over the form customization:
public function preSetData(DataEvent $event)
{
$myEvent = $event->getData()->getEvent();
if (null === $myEvent) {
return;
}
$this->customizeForm();
}
This ensures that we don't run into issues when submitting the form and no data is passed, however getData() will return an empty object and not NULL.
Now, when the form is submitted, we will bind it to the data received. That's when we want to interfere. So we'll also subscribe to the PRE_BIND event:
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_BIND => 'preBind',
FormEvents::PRE_SET_DATA => 'preSetData',
];
}
In pre-bind, the data we receive is only an array of values and not an object graph.
But if we injected the object manager in our listener, then we can find our objects and work with them:
public function preBind(DataEvent $event)
{
$data = $event->getData();
$id = $data['event'];
$myEvent = $this->om
->getRepository('Acme\DemoBundle\Document\Event')
->find(new \MongoId($id));
if($myEvent === null){
$msg = 'The event %s could not be found';
throw new \Exception(sprintf($msg, $id));
}
$this->customizeForm();
}

Resources