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)
Related
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();
}
}
}
}
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 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.
Hi I have an action which adds people to a group.
In order to increase the usability the form for it removes the people that are already in the group.
My controller action looks like this:
public function addAction(UserGroup $userGroup)
{
$tempGroup = new UserGroup();
foreach ($userGroup->getUsers() as $user) {
$tempGroup->addUser($user);
}
$form = $this->container->get('form.factory')->create(new UserGroupQuickType(), $tempGroup);
$request = $this->container->get('request');
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
$group = $form->getData();
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
/** #var $em EntityManager */
$em = $this->container->get('em');
foreach ($group->getUsers() as $toInvite) {
$userGroup->addUser($toInvite);
}
$em->persist($userGroup);
$em->flush($userGroup);
}
}
return array(
'form' => $form->createView(),
'userGroup' => $userGroup
);
}
This code throws an exception:
A new entity was found through the relationship 'Strego\UserBundle\Entity\UserGroup#users'
that was not configured to cascade persist operations for entity: Degi.
To solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this
association in the mapping for example #ManyToOne(..,cascade={"persist"}).
The new found relation with was already there before. Meaning the user "Degi" was already in the group and is not a new entity.
I can avoid this error if I'll leave out the persist but then I'll get an exception: Entity has to be managed or scheduled for removal for single computation.
This is caused by the fact that my "usergroup" entity has the whole time a entity status of 3 (= detached)
I have used the same logic (with temp group etc.) for an entity that has 1 to 1 relationship to my usergroup and from there I can easily add people even to the group.
But not with this action, which is logically doing the exact same thing.
UPDATE:
My previous update was leading in the wrong direction. But here in comparison the (almost) same controller that works:
public function addAction(BetRound $betRound)
{
$userGroup = new UserGroup();
foreach ($betRound->getUsers() as $user) {
$userGroup->addUser($user);
}
$form = $this->createForm(new UserGroupQuickType(), $userGroup);
$request = $this->getRequest();
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
/** #var $betRoundManager BetRoundManager */
$betRoundManager = $this->container->get('strego_tipp.betround_manager');
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
$group = $form->getData();
foreach ($group->getUsers() as $toInvite) {
if (!$betRound->getUserGroup()->hasUser($toInvite)) {
$betRound->getUserGroup()->addUser($toInvite);
}
}
$this->getDoctrine()->getManager()->flush();
return $this->redirect($this->generateUrl('betround_show', array('id' => $betRound->getId())));
}
}
return array(
'form' => $form->createView(),
'betRound' => $betRound
);
}
This is expected behavior since your have relationship to, not really a new entity but unmanaged object.
While iterating you could try merging $toInvite. Don't worry, if objects you acquired via form have an appropriate identifier (#Id) value set they would be just reloaded from databases instead of marked for insertion. Newly added objects, on the other hand, will be marked.
So, before trying anything, ensure that each of old $toInvite have an ID set.
foreach ($group->getUsers() as $toInvite) {
$em->merge($toInvite);
$userGroup->addUser($toInvite);
}
// safe to do now, all of the added users are either reloaded or marked for insertion
$em->perist($usetGroup);
$em->flush($userGroup);
Hope this helps.
This is happening because $userGroup is already an entity which already exists and $toInvite is a new entity and you are trying to flush $userGroup. To make this code working you need to specify cascade={"persist"} in your entity file (Annotations or yaml whichever you prefer).
Other solution is to do the reverse and persist $toInvite
foreach ($group->getUsers() as $toInvite) {
$toInvite->setUserGroup($userGroup);
$em->persist($toInvite);
}
$em->flush();
I think the first option would be a better choice
After several attempts I found out that this issue were actually 2 issues:
1. I had a listener that updated some rows on persist even if it was not necessary.
2. The paramconverter somehow detaches the entities that are coming in. Which means that my UserGroup and all my users in it are detached. As soon as they are added again to the Usergroup, those Entities seem to be "new" for the EM. Therefore I needed to merge all users AND fix my listener that updates the "createdBy", which otherwise would have also needed to been merged.
I want to get the schools the user is in, but for some reason I can only access it by running a query to the school table that is otherwise unrelated. Here is my code:
This doesn't work (within the controller):
$schoolsEnrolled = $this->getUser()->getSchools();
The result is an Array with a School object with all it's properties as null (other than id for some reason).
This does work (within the controller):
//unrelated query
$repository = $this->getDoctrine()->getRepository('AcmeMainBundle:School');
$query = $repository->createQueryBuilder('s')->getQuery();
$schools = $query->getResult();
//the query I care about
$schoolsEnrolled = $this->getUser()->getSchools();
The result is an array of schools as desired.
Here are the related methods:
In the School Class:
public function getSchools(){
$schools = array();
foreach ($this->schoolHasUsers as $key=>$schoolHasUser){
$schools[] = $schoolHasUser->getSchool();
}
return $schools;
}
In the SchoolHasUser Class:
public function getSchool()
{
return $this->school;
}
How can I get the query I care about to work without the unrelated query?
In doctrine object can be lazy-loaded. Then you call $schoolHasUser->getSchool(); you trully no query the database, only get proxy object. So try to get some property of it, example getName or getId. This action make a call to DB and fetch object.
One of the proper ways to do it is like this
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('YourBundle:YourEntity')->findAll();