Flushing in postPersist possible or not? - symfony

I have read the docs about lifecycle events, and several questions here on SO about changing or persisting new entities during lifecycle events. Calling EnitityManager::flush() seems to be a problem.
Ok, but looking carefully at the docs, there is a code example where the field is changed in postPersist, but no flush is called.
I checked that, and the suggested change is not written to the DB. Only the object being persisted does receive the change.
<?php
/** #Entity #HasLifecycleCallbacks */
class User
{
// ...
/**
* #Column(type="string", length=255)
*/
public $value;
/** #PostPersist */
public function doStuffOnPostPersist()
{
$this->value = 'changed from postPersist callback!';
}
}
Maybe one should add this to the docs. I was mislead at first.
However, when adding the LifecyleEventArgs argument and flushing the contained EntityManager, they are written to DB:
/** #PostPersist */
public function doStuffOnPostPersist(LifecycleEventArgs $args)
{
$this->value = 'changed from postPersist callback!';
$args->getEntityManager()->flush(); // works in my tests. Is this safe to use ?
}
I don't know how to interpret the docs about whether it is OK or not to call flush inside the postPersist.
As you can see, I am searching for a reliable way to perform some kind of postprocessing to my entities after inserting or updating them. I have to use postPersist, since I need the auto-generated primary key value.
Side question: If yes, it is ok to flush, could I then also persist other objects in PostUpdate? Like so:
/** #PostPersist */
public function doStuffOnPostPersist(LifecycleEventArgs $args)
{
$this->value = 'changed from postPersist callback!';
$obj = new OtherObject("value " . $this->value);
$args->getEntityManager()->persist($obj);
$args->getEntityManager()->flush(); // works in my tests. Is this safe to use ?
}
Side-side question: I have tried the last variant, and it seems to work. But is it efficient, or am I possibly creating deep recursion stacks? According to the docs, the postPersist code is called during flush, so if I call flush during postPersist, I have to be careful not to persist an object that executes the same handler, which would lead to infinite recursion. Is this correct?

I checked that, and the suggested change is not written to the DB. Only the object being persisted does receive the change.
Maybe one should add this to the docs. I was mislead at first.
The code in the docs doesn't try to persist this modification of the value property
in database that's why no flush() is called. It's just show an example and this value could also be an unmapped to the database property of class User.
I don't know how to interpret the docs about whether it is OK or not to call flush inside the postPersist.
It is ok to call flush() on a PostPersist lifecycle callback in order to change a mapped property
of your entity. In your PostPersist callback your entity has already been inserted in your database. By changing the property
value and calling flush() your entity will be flag as to be updated, so the PostPersist event won't
be dispatched again (instead Pre/PostUpdate events will be dispatched).
Side question: If yes, it is ok to flush, could I then also persist other objects in PostUpdate?
It is also ok to persist an new object of another entity class in a PostPersist event callback with no problem,
but if you try to persist an object of the same (User) class in this PostPersist callback you will have an
infinite recursion, as you can easily understand.
Side-side question: I have tried the last variant, and it seems to work. But is it efficient, or am I possibly creating deep recursion stacks?
As I have explained before this code doesn't create too deep recursion stacks or infinite loops if not persisting objects of the same class (User) in which the callback belongs. The flush() will be called exactly two times. Although things could get more complicated when having also to deal with associations, in your example there is not such problem.

Related

How to implement Doctrine NotifyPropertyChanged in Symfony

Can anyone tell me how to implement Doctrine NotifyPropertyChanged in Symfony?
I have implemented this code in an Entity that will notify about is changes. But how to add listener for these changes in another Entity?
I mean I want to handle this situation: Entity Book has oneToMany to Authors. When something is being changed in a partcular Author, I want to react on it in a Book Entity. So I have implemented NotifyPropertyChanged in Author. In a setter I invoke notifications, but how to add this listener to a Book entity?
It looks like your use case is a little bit different: the ǸotifyPropertyChanged` is usefull to tell Doctrine your entity changed. By default, Doctrine2 will iterate over all properties and compare them (so that's "automatic"). IMO, drop it. Only use case I see for it right now are some very specific cases were using the default policy would cost too much.
Another way to do would be to use some lifecycle events. Changing other entities from a preUpdate or postUpdate is notoriously difficult. Doctrine already started calculting the "changeset", what to persist to DB, and there's no easy way to add another entity to it.
Since you are ok writing some code inside the setters, the easier path is something like this:
class Author
{
public function setName($name)
{
// ...
// for each setter, call onChanged
$this->onChange();
}
private function onChange()
{
foreach ($this->books as $book) {
// maybe call some method on your books, like onAuthorChanged() ?
}
}
}

Symfony2 - New entity was found through relationship - but it has been persisted [duplicate]

since 2 weeks, we are having this problem while trying to flush new elements:
CRITICAL: Doctrine\ORM\ORMInvalidArgumentException:
A new entity was found through the relationship 'Comment#capture' that was not configured to cascade persist operations for entity
But the capture is already in the database, and we are getting it by a findOneBy, so if we cascade persist it, or persist it, we get a
Table constraint violation: duplicate entry.
The comments are created in a loop with differents captures, with a new, and all required field are set.
With all of the entities persisted and / or got by a findOne (and all valid), the flush still fails.
I'm on this issue since a while, so please help me
I had the same problem and it was the same EntityManager. I wanted to insert an object related ManyToOne. And I don't want a cascade persist.
Example :
$category = $em->find("Category", 10);
$product = new Product();
$product->setCategory($category)
$em->persist($product);
$em->flush();
This throws the same exception for me.
So the solution is :
$category = $em->find("Category", 10);
$product = new Product();
$product->setCategory($category)
$em->merge($product);
$em->flush();
In my case a too early call of
$this->entityManager->clear();
caused the problem. It also disappeared by only doing a clear on the recent object, like
$this->entityManager->clear($capture);
My answer is relevant for topic, but not very relevant for your particular case, so for those googling I post this, as the answers above did not help me.
In my case, I had the same error with batch-processing entities that had a relation and that relation was set to the very same entity.
WHAT I DID WRONG:
When I did $this->entityManager->clear(); while processing batch of entities I would get this error, because next batch of entities would point to the detached related entity.
WHAT WENT WRONG:
I did not know that $this->entityManager->clear(); works the same as $this->entityManager->detach($entity); only detaches ALL of the repositorie`s entities.
I thought that $this->entityManager->clear(); also detaches related entities.
WHAT I SHOULD HAVE DONE:
I should have iterated over entities and detach them one by one - that would not detach the related entity that the future entities pointed to.
I hope this helps someone.
First of all, you should take better care of your code, I see like 3 differents indentations in your entity and controller - this is hard to read, and do not fit the Symfony2 coding standards.
The code you show for your controller is not complete, we have no idea from where $this->activeCapture is coming. Inside you have a $people['capture'] which contains a Capture object I presume. This is very important.
If the Capture in $people['capture'] is persisted / fetched from another EntityManager than $this->entityManager (which, again, we do not know from where it come), Doctrine2 have no idea that the object is already persisted.
You should make sure to use the same instance of the Doctrine Entity Manager for all those operations (use spl_object_hash on the EM object to make sure they are the same instance).
You can also tell the EntityManager what to do with the Capture object.
// Refreshes the persistent state of an entity from the database
$this->entityManager->refresh($captureEntity);
// Or
// Merges the state of a detached entity into the
// persistence context of this EntityManager and returns the managed copy of the entity.
$captureEntity = $this->entityManager->merge($captureEntity);
If this does not help, you should provide more code.
The error:
'Comment#capture' that was not configured to cascade persist operations for entity
The problem:
/**
* #ORM\ManyToOne(targetEntity="Capture", inversedBy="comments")
* #ORM\JoinColumn(name="capture_id", referencedColumnName="id",nullable=true)
*/
protected $capture;
dont configured the cascade persist
try with this:
/**
* #ORM\ManyToOne(targetEntity="Capture", inversedBy="comments", cascade={"persist", "remove" })
* #ORM\JoinColumn(name="capture_id", referencedColumnName="id",nullable=true)
*/
protected $capture;
Refreshing the entity in question helped my case.
/* $item->getProduct() is already set */
/* Add these 3 lines anyway */
$id = $item->getProduct()->getId();
$reference = $this->getDoctrine()->getReference(Product::class, $id);
$item->setProduct($reference);
/* Original code as follows */
$quote->getItems()->add($item);
$this->getDoctrine()->persist($quote);
$this->getDoctrine()->flush();
Despite my $item already having a Product set elsewhere, I was still getting the error.
Turns out it was set via a different instance of EntityManager.
So this is a hack of sorts, by retrieving id of the existing product, and then retrieving a reference of it, and using setProduct to "refresh" the whatever connection. I later fixed it by ensuring I have and use only a single instance of EntityManager in my codebase.
I got this error too when tried to add new entity.
A new entity was found through the relationship 'Application\Entity\User#chats'
that was not configured to cascade persist operations for entity: ###.
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"}).
My case was that I tried to save entity, that shouldn't be saved. Entity relations was filled and tried to be saved (User has Chat in Many2Many, but Chat was a temporary entity), but there were some collisions.
So If I use cascade={"persist"} I get unwanted behaviour - trash entity is saved. My solution was to remove non-saving entity out of any saving entities:
// User entity code
public function removeFromChats(Chat $c = null){
if ($c and $this->chats->contains($c)) {
$this->chats->removeElement($c);
}
}
Saving code
/* some code witch $chat entity */
$chat->addUser($user);
// saving
$user->removeFromChats($chat);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
I want to tell about my case as that might be helpful to somebody.
Given two entities: AdSet and AdSetPlacemnt. AdSet has the following property:
/**
* #ORM\OneToOne(targetEntity="AdSetPlacement", mappedBy="adSet", cascade={"persist"})
*
* #JMS\Expose
*/
protected $placement;
Then error appears when I try to delete some AdSet objects in a cycle after 1st iteration
foreach($adSetIds as $adSetId) {
/** #var AdSet $adSet */
$adSet = $this->adSetRepository->findOneBy(["id" => $adSetId]);
$this->em->remove($adSet);
$this->em->flush();
}
Error
A new entity was found through the relationship 'AppBundle\Entity\AdSetPlacement#adSet' that was not configured to cascade persist operations for entity: AppBundle\Entity\AdSet#00000000117d7c930000000054c81ae1. 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"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\AdSet#__toString()' to get a clue.
Solution
The solution was to add "remove" to $placement cascade options to be:
cascade={"persist","remove"}. This guarantees that Placement also becomes detached. Entity manager will "forget" about Placement object thinking of it as "removed" once AdSet is removed.
Bad alternative
When trying to figure out what's going on I've seen a couple answers or recommendations to simply use entity manager's clear method to completely clear persistence context.
foreach($adSetIds as $adSetId) {
/** #var AdSet $adSet */
$adSet = $this->adSetRepository->findOneBy(["id" => $adSetId]);
$this->em->remove($adSet);
$this->em->flush();
$this->em->clear();
}
So that code also works, the issue gets solved but it's not always what you really wanna do. Indeed it's happens quite rarely that you actually need to clear entity manager.

Preserving changes made to a document with Doctrine's PreUpdate lifecycle event

I have an event subscriber that contains two lifecycle event methods: prePersist and preUpdate. The prePersist one is working as expected - I modify the document, and the changes are reflected later when I retrieve the document. preUpdate is not working as expected, though. Here's basically what it looks like:
/**
* Also update the foo code when the bar profile is updated.
* #param LifecycleEventArgs $args
*/
public function preUpdate(LifecycleEventArgs $args)
{
$document = $args->getDocument();
if ($document instanceof BarProfile) {
$document->setFooCode('Salamanders');
}
}
If I create a document and set its fooCode to 'placeholder' in the perPersist function, then when I retrieve the document later it fooCode is 'placeholder'. If I then update it, and retrieve it again, then I expect its fooCode to be 'Salamanders'. However, it's still 'placeholder'. I've even tried putting error_log() stuff in there and it writes stuff to the logs so I can see that this method is being executed.
Is there a second step I have to do after $document->setFooCode() to make the new value of fooCode stick around?
You cannot modify the fields directly in the preUpdate event, you have to modify their primitive values. Changes to associations are not allowed. You would have to do:
$eventArgs->setNewValue('fooCode', 'Salamanders');
You stated "The prePersist one is working as expected - I modify the document, and the changes are reflected later when I retrieve the document."
This leads me to believe that you may not be aware of the difference between persist and update. In Doctrine, a persist occurs when you are creating the object for the first time. An update occurs when you are making changes to an existing object that is already being managed by Doctrine. A lot of people are confused by this, but you do not need to call persist() when you are updating an existing entity, you only need to call flush(). For example:
// inserts a new entity into the database
$document = new Document();
$document->setName('My Document');
$em->persist($document);
$em->flush();
and
// retrieves entity from the database, makes a change, then updates the database
$document = $em->findOneByName('My Document');
$document->setFooCode('Salamanders');
$em->flush();
I encourage you to read the Doctrine documentation like Cerad suggested. Play close attention to the following statements for the preUpdate event:
PreUpdate is the most restrictive to use event
Changes to associations of the updated entity are never allowed in this event
Changes to fields of the passed entities are not recognized by the flush operation anymore, use the computed change-set passed to the event to modify primitive field values

What is the Correct LifeCycleEventArgs to use in Symfony 2.5 preUpdate Event Listener

In a Symfony 2.5 preUpdate event listener, I'm confused about whether/when to pass in LifeCycleEventArgs or PreUpdateEventArgs (and which one).
The Doctrine Documentation says that starting in 2.4 you use LifeCycleEventArgs:
Since 2.4 the triggered event is given to the lifecycle-callback. With
the additional argument you have access to the EntityManager and
UnitOfWork APIs inside these callback methods.
But the example directly below that and all subsequent examples show PreUpdateEventArgs:
class User
{
public function preUpdate(PreUpdateEventArgs $event)
{
if ($event->hasChangedField('username')) {
// Do something when the username is changed.
}
}
}
So does that mean LifeCycleEventArgs (if it's available in your version) will do all that PreUpdateEventArgs does and more? They both seem to work (i.e. no errors).
And using the new LifeCycleEventArgs, is it still necessary to use $entity->setNewValue('fieldName', $value)? Is preUpdate still as restrictive or can we just do $entity->setFieldName($value)?
On Doctrine 2.2, class PreUpdateEventArgs extends LifecycleEventArgs and it only declares the entity changeset for the entity being pre-updated.
So yes, PreUpdateEventArgs does more, though theoretically (i.e. I didn't test that!) you could achieve the same using the EM's UOW:
In the LifecycleEventArgs, you can access the entity that has triggered the event using getEntity().
Same class, you can access the EM using getEntityManager()
Hence: $this->getEntityManager()->getUnitOfWork()->getEntityChangeSet($this->getEntity()) should give you the same changeset as the one provided by PreUpdateEventArgs
Conclusion: PreUpdateEventArgs facilitates a job that could already be achieved using slightly more code ;)
To dive a little deeper: UnitOfWork::commit() triggers the update by calling UnitOfWork::executeUpdates, which dispatches an instance of PreUpdateEventArgs filled with the already-computed changeset for this entity, directly taken from the UOW's changesets. (Again: I'm using 2.2, might be different on your release.)
About the $event->setNewValue('field', 'value') vs. $entity->setField('value'), I believe you need to just update the changeset (which is setNewValue's job). Calling the entity's setter directly might de-synchronize the entity from its state in DB or, more probably, just be useless as it shouldn't be taken into consideration. Except if you manually call the UOW's computeChangeSets().
Hope this helps!

Doctrine - A new entity was found through the relationship

since 2 weeks, we are having this problem while trying to flush new elements:
CRITICAL: Doctrine\ORM\ORMInvalidArgumentException:
A new entity was found through the relationship 'Comment#capture' that was not configured to cascade persist operations for entity
But the capture is already in the database, and we are getting it by a findOneBy, so if we cascade persist it, or persist it, we get a
Table constraint violation: duplicate entry.
The comments are created in a loop with differents captures, with a new, and all required field are set.
With all of the entities persisted and / or got by a findOne (and all valid), the flush still fails.
I'm on this issue since a while, so please help me
I had the same problem and it was the same EntityManager. I wanted to insert an object related ManyToOne. And I don't want a cascade persist.
Example :
$category = $em->find("Category", 10);
$product = new Product();
$product->setCategory($category)
$em->persist($product);
$em->flush();
This throws the same exception for me.
So the solution is :
$category = $em->find("Category", 10);
$product = new Product();
$product->setCategory($category)
$em->merge($product);
$em->flush();
In my case a too early call of
$this->entityManager->clear();
caused the problem. It also disappeared by only doing a clear on the recent object, like
$this->entityManager->clear($capture);
My answer is relevant for topic, but not very relevant for your particular case, so for those googling I post this, as the answers above did not help me.
In my case, I had the same error with batch-processing entities that had a relation and that relation was set to the very same entity.
WHAT I DID WRONG:
When I did $this->entityManager->clear(); while processing batch of entities I would get this error, because next batch of entities would point to the detached related entity.
WHAT WENT WRONG:
I did not know that $this->entityManager->clear(); works the same as $this->entityManager->detach($entity); only detaches ALL of the repositorie`s entities.
I thought that $this->entityManager->clear(); also detaches related entities.
WHAT I SHOULD HAVE DONE:
I should have iterated over entities and detach them one by one - that would not detach the related entity that the future entities pointed to.
I hope this helps someone.
First of all, you should take better care of your code, I see like 3 differents indentations in your entity and controller - this is hard to read, and do not fit the Symfony2 coding standards.
The code you show for your controller is not complete, we have no idea from where $this->activeCapture is coming. Inside you have a $people['capture'] which contains a Capture object I presume. This is very important.
If the Capture in $people['capture'] is persisted / fetched from another EntityManager than $this->entityManager (which, again, we do not know from where it come), Doctrine2 have no idea that the object is already persisted.
You should make sure to use the same instance of the Doctrine Entity Manager for all those operations (use spl_object_hash on the EM object to make sure they are the same instance).
You can also tell the EntityManager what to do with the Capture object.
// Refreshes the persistent state of an entity from the database
$this->entityManager->refresh($captureEntity);
// Or
// Merges the state of a detached entity into the
// persistence context of this EntityManager and returns the managed copy of the entity.
$captureEntity = $this->entityManager->merge($captureEntity);
If this does not help, you should provide more code.
The error:
'Comment#capture' that was not configured to cascade persist operations for entity
The problem:
/**
* #ORM\ManyToOne(targetEntity="Capture", inversedBy="comments")
* #ORM\JoinColumn(name="capture_id", referencedColumnName="id",nullable=true)
*/
protected $capture;
dont configured the cascade persist
try with this:
/**
* #ORM\ManyToOne(targetEntity="Capture", inversedBy="comments", cascade={"persist", "remove" })
* #ORM\JoinColumn(name="capture_id", referencedColumnName="id",nullable=true)
*/
protected $capture;
Refreshing the entity in question helped my case.
/* $item->getProduct() is already set */
/* Add these 3 lines anyway */
$id = $item->getProduct()->getId();
$reference = $this->getDoctrine()->getReference(Product::class, $id);
$item->setProduct($reference);
/* Original code as follows */
$quote->getItems()->add($item);
$this->getDoctrine()->persist($quote);
$this->getDoctrine()->flush();
Despite my $item already having a Product set elsewhere, I was still getting the error.
Turns out it was set via a different instance of EntityManager.
So this is a hack of sorts, by retrieving id of the existing product, and then retrieving a reference of it, and using setProduct to "refresh" the whatever connection. I later fixed it by ensuring I have and use only a single instance of EntityManager in my codebase.
I got this error too when tried to add new entity.
A new entity was found through the relationship 'Application\Entity\User#chats'
that was not configured to cascade persist operations for entity: ###.
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"}).
My case was that I tried to save entity, that shouldn't be saved. Entity relations was filled and tried to be saved (User has Chat in Many2Many, but Chat was a temporary entity), but there were some collisions.
So If I use cascade={"persist"} I get unwanted behaviour - trash entity is saved. My solution was to remove non-saving entity out of any saving entities:
// User entity code
public function removeFromChats(Chat $c = null){
if ($c and $this->chats->contains($c)) {
$this->chats->removeElement($c);
}
}
Saving code
/* some code witch $chat entity */
$chat->addUser($user);
// saving
$user->removeFromChats($chat);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
I want to tell about my case as that might be helpful to somebody.
Given two entities: AdSet and AdSetPlacemnt. AdSet has the following property:
/**
* #ORM\OneToOne(targetEntity="AdSetPlacement", mappedBy="adSet", cascade={"persist"})
*
* #JMS\Expose
*/
protected $placement;
Then error appears when I try to delete some AdSet objects in a cycle after 1st iteration
foreach($adSetIds as $adSetId) {
/** #var AdSet $adSet */
$adSet = $this->adSetRepository->findOneBy(["id" => $adSetId]);
$this->em->remove($adSet);
$this->em->flush();
}
Error
A new entity was found through the relationship 'AppBundle\Entity\AdSetPlacement#adSet' that was not configured to cascade persist operations for entity: AppBundle\Entity\AdSet#00000000117d7c930000000054c81ae1. 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"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\AdSet#__toString()' to get a clue.
Solution
The solution was to add "remove" to $placement cascade options to be:
cascade={"persist","remove"}. This guarantees that Placement also becomes detached. Entity manager will "forget" about Placement object thinking of it as "removed" once AdSet is removed.
Bad alternative
When trying to figure out what's going on I've seen a couple answers or recommendations to simply use entity manager's clear method to completely clear persistence context.
foreach($adSetIds as $adSetId) {
/** #var AdSet $adSet */
$adSet = $this->adSetRepository->findOneBy(["id" => $adSetId]);
$this->em->remove($adSet);
$this->em->flush();
$this->em->clear();
}
So that code also works, the issue gets solved but it's not always what you really wanna do. Indeed it's happens quite rarely that you actually need to clear entity manager.

Resources