Symfony 2 - flush in postUpdate fire preUpdate event - symfony

I detected this problem "thanks" to an exception I got:
Catchable Fatal Error: Argument 3 passed to
Doctrine\ORM\Event\PreUpdateEventArgs::__construct()
must be an array, null given, called in
/.../vendor/doctrine/lib/Doctrine/ORM/UnitOfWork.php on line 804
and defined in
/.../vendor/doctrine/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php line 28
I am working on a project that requieres a specific logic:
When the order field in entity book is modified, I need to update field books_order_modified_at in the parent entity bookstore (this field allows me to know whether the order of books in a bookstore was changed).
I decided to do it in an event listener since there are many places in the code that might change the order of books.
I didn't find any way to update a related entity from preUpdate event, so I have a private field in the listener class which I use to tell the postUpdate event to update the relevant bookstore entity.
My problem is that when I do so the preUpdate event of the book entity is fired.
When I check the change-set it contains only the modified_at field, but it has the same value before and after.
If someone has another idea how to solve the problem - great.
If not - any idea how can I prevent the preUpdate event from being fired when the flush is called in teh postUpdate event??

Actually, this is a problem from doctrine Doctrine Issue DDC-2726. Solved it by adding a clear call on the entity manager after the flush in the listener so the 3-rd argument to that constructor, which is actually the entityChangeSets, will be re-written.

What about updating the modified_at within your entities and let doctrine handle it? You would change your setOrder method in your book to update the BookOrder entity like this:
class Book {
public function setOrder($order) {
// modify book
$this->bookOrder->updateModifiedAt();
}
}
Of course your BookOrder would have to implement modifiedAt:
class BookOrder {
public function updateModifiedAt() {
$this->modifiedAt = new \DateTime();
}
}
If you use other classes for your Datetime, you of course have to change this code!
Doctrine should recognize that BookOrder has changed and should update it without any need to use a event listener.

I can suggest you to use Timestampable extension for Doctrine from DoctrineExtensionsBundle.
By using it you don't need to set created_at or modified_at values. This extension does it automatically. Even it can set modified_at only when specific fields were modified. See example.
I think you are writing something like this extension. So, you don't need to do that because this is already done :)

I had a similar problem to this. Trying to use preupdate to modify child elements caused the same error. In the end, my solution to simply update the children belonging to the parent. No explicit call to flush required.
/**
* Update expiry dates for all runners belonging to a campaign
*
* #param $runners
* #param $expiryDate
*/
private function updateCampaignRunners($runners, $expiryDate){
foreach($runners as $runner){
$runner->setExpiresAt($expiryDate);
$this->getModelManager()->update($runner);
}
}
/**
* Post update and persist lifecycle callback
*
* #param Campaign $campaign
*/
private function postAction(Campaign $campaign)
{
$runnerExpire = $this->getForm()->get("runnerExpire")->getData();
if($runnerExpiryDate && $campaign->getRunners()){
$this->updateCampaignRunners($campaign->getRunners(), $runnersExpiryDate);
}
}

Related

Set up non-persistent relation in Doctrine 2

I have an object $user that has a one to many relation with $establishment. I can use:
$user->getEstablishments();
The user can select a stablishment to work on. I have this method that I call in the controller:
$user->setCurrentEstablishment($establishment);
And this one that I call in the view:
$establishment = $user->getCurrentEstablishment();
I want to be able to call:
$user->setCurrentEstablishmentBy Slug($establishment_slug);
where the slug is a string, and let the user object look for the establishment.
Doctrine discourages the practice of accessing the Entity Manager inside the Entity object, but I think that using it in the controller is even worse.
I suspect that some special Doctrine annotation exists that takes care of non persistent relations like this, or some method other than serving the Entity Manager through a service should be used here. Some easy way of referencing other entities from inside the model.
¿Is there any? ¿How could I do that?
There is no Annotation in Doctrine which could convert slug into object.
What can help You is ParamConverter, with it you can automatically convert slug from query into object. But it still must be used in Controller.
Example usage:
/**
* #Route("/some-route/{slug}")
* #ParamConverter("object", class="AppBundle:Establishment", options={"id" = "slug", "repository_method" = "findEstablishmentBySlug"})
*/
public function slugAction(Establishment $object)
{
...
Docs about param converter: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html

How to make work soft deletable and unique entity using symfony 2

I've soft deletable and a uniqueentity field. It works great but...
If the record is deleted "softdeleted", I can't create the same record. I think it's because the record is not realy deleted in the DB. But I need to that.
So what is the best way to dothis ?
Totaly deleted the record ? So is softdeletable a good choice ?
Find a way that if the record is softdeleted, I can create again the same record
Thanks for your advices
After you removed the unique constraint from the database level, You can set to your entity this.
#UniqueEntity(fields={"name", "deleteTime"}, ignoreNull=false)
In this case the validation will fail if you already have a "non-soft deleted" row with the given name in your database, but it won't if the deleteTime is setted.
since you are using soft delete and unique constraints, you can't actually use a unique constraint on the database level.
I suggest you handle the unique constraint check manually, this could be done in a doctrine life cycle event
One way to do this is by creating a callback function in your entity and annotate it to fire on the event:
/** #PrePersist */
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
// check if this entity's unique field is OK
}
This will only ensure you don't save anything incorrect in the database, but it won't handle your forms nicely. So in addition, you probably want to use the UniqueEntity validator for this, and create a custom repositoryMethod to check the uniqueness.
This custom repository method can be used by both the prePersist and the UniqueEntity validator.
You have three choices
Hard Delete the item
Remove the Unique (and handle it in doctrine)
When you create the new entity, you deactivate the softdeletable filter
$em->getFilters()->disable('soft-deleteable');
This will let you find the "deleted" items. Then you can do things like overwrite the old entry, harddelete it manually or whatever your app needs you to do with it.
In my case, I used this way
Remove the unique index of the column on the Database
public function up(Schema $schema) : void
{
$this->addSql('DROP INDEX UNIQ_A2E0150FE7927C74 ON admins');
$this->addSql('CREATE UNIQUE INDEX UNIQ_A2E0150FE7927333 ON
admins (email,deleted_at)');
}
Add this constraint on your Entity
/**
* #ORM\Entity(repositoryClass=AdminRepository::class)
* #ORM\Table(name="admins",
* uniqueConstraints={
* #UniqueConstraint(name="admins",
* columns={"email", "deleted_at"})
* })
It means that you make the pair email (unique column) and deleted_at unique, instead of just the email field. And now, I can create another admin with the same email, if the old one was deleted (Using soft delete)

Doctrine Events: How can I track additions / removals to a ManyToMany collection?

I have an Application entity that has a ManyToMany relationship to the SortList entity. The owning side is Application. There's a simple join table that creates the mapping for this relationship.
Here's how the Application entity looks with regards to managing the collection:
/**
* Add sortLists
*
* #param \AppBundle\Entity\SortList $sortList
* #return Application
*/
public function addSortList(SortList $sortList)
{
$this->sortLists[] = $sortList;
$sortList->addApplication($this);
return $this;
}
/**
* Remove sortLists
*
* #param \AppBundle\Entity\SortList $sortList
*/
public function removeSortList(SortList $sortList)
{
$this->sortLists->removeElement($sortList);
$sortList->removeApplication($this);
}
/**
* Get sortLists
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getSortLists()
{
return $this->sortLists;
}
I want to track when SortLists have been added or removed from an Application.
I've already learned that I can't use postUpdate lifecycle event to track these changes collections.
Instead, it seems I should use onFlush and then $unitOfWork->getScheduledCollectionUpdates() and $unitOfWork->getScheduledCollectionDeletions().
For updates, I see I can use the "internal" method getInsertDiff to see which items in the collection were added and getDeleteDiff to see which items in the collection were removed.
But I have a couple concerns:
If all items in the collection were removed, there's no way to see which items were actually removed since $unitOfWork->getScheduledCollectionDeletions() doesn't have this information.
I'm using methods that are marked as "internal"; it seems like they could "disappear" or be refactored some point in the future without me knowing?
I solved this empty getDeleteDiff in https://stackoverflow.com/a/75277337/5418514
The reason this is sometimes empty is an old but still existing problem. The solution at the moment is to fetch the data again yourself.
public function onFlush(OnFlushEventArgs $args)
{
$uow = $args->getEntityManager()->getUnitOfWork();
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
/**
* "getDeleteDiff" is not reliable, collection->clear on PersistentCollection also clears the original snapshot
* A reliable way to get removed items is: clone collection, fetch original data
*/
$removedData = $collection->getDeleteDiff();
if (!$removedData) {
$clone = clone $collection;
$clone->setOwner($collection->getOwner(), $collection->getMapping());
// This gets the real data from the database into the clone
$uow->loadCollection($clone);
// The actual removed items!
$removedData = $clone->toArray();
}
}
}
I think the examples below cover everything you would need so you just need to implement which ever you want/need in your app.
For tracking persist operations, you can use prePersist and
postPersist event listener on an entity or prePersist and
postPersist event subscriber on an entity examples. PrePersist
won't give you the ID cos it doesn't exist in DB yet whereas
PostPersist will as shown in the example.
For tracking remove operations, you can use preRemove and
postRemove event listener on an entity example.
For tracking update operations which is the tricky one, you can use
preUpdate event listener on an entity example but pay attention
how it is done.
For inserting, updating and removing operations, you can use
onFlush event listener on an entity example which covers
UnitOfWork getScheduledEntityInsertions,
getScheduledEntityUpdates and getScheduledEntityDeletions
methods.
There are many other useful listener examples in that website so just use search feature for listener keyword. Once I did same thing as you wanted for M-N associations but cannot find the example. If I can I'll post it but not sure if I can!

SonataAdminBundle - check changes in `preUpdate` hook

Is it possible to check if field was changed on preUpdate hook? I'm looking for something like preUpdate hasChangedField($fieldName) Doctrine functionality. Any ideas?
This question is a bit similar to this one
Your solution is just to compare the field of the old object with the new one and see where it differs.
So for example:
public function preUpdate($newObject)
{
$em = $this->getModelManager()->getEntityManager($this->getClass());
$originalObject = $em->getUnitOfWork()->getOriginalEntityData($newObject);
if ($newObject->getSomeField() !== $originalObject['fieldName']) {
// Field has been changed
}
}
For me the best approach is this in Sonata Admin:
$newField = $this->getForm()->get('field')->getData();
$oldField = $this->getForm()->get('field')->getConfig()->getData();
You shouldn't use unit of work unless there is no option. Also, if you have a not mapped field, you can't access it by entity object.
In a normal Doctrine lyfe cycle event, the best option is Doctrine preupdate event doc

ResultSetManager clobbered after postLoad event handler

I have a postLoad event listener that executes a query to retrieve some file data that is popped into an array on the entity that is being loaded. I am doing this because we have a number of different items that need to be added to the entity, but aren't essential to the entity. Right now it is files, but we will eventually have at least 7 of these "items". Instead of creating 7 different mappings to the individual "items", we decided to implement them as services in Symfony that drop the "payload" that they provide into the infoArray. Now when we want to add a new "item" we don't have to edit dozens of business object to add a new mapping, we can just add it to the infoArrray (keyed by service name) and whoever needs it can get it from that array.
so my entity looks like
/**
* #var integer $id
*
* #ORM\Column(name="ID", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Type("integer")
* #SerGroups({"modulerevision", "module_revision"})
*/
protected $id;
.
.
.
public $servicesArray;
In the event listener I end up calling the following method
public function getFiles(ConsumerInterface $entity, $fullPath = false){
$query = $this->em->createQuery(
'SELECT f
FROM FileManagerBundle:File f
JOIN f.owners o
WHERE o.id = ?1');
$query->setParameter(1, $entity->getOwner());
$files = $query->getResult();
return $files;
}
This works great. The query is executed and I get my array of files and I push it onto $infoArray in my entity.
After running the postLoad event code, we jump back in to Doctrine\ORM\Internal\Hydration\ObjectHydrator at line 480
if ($this->_rsm->isMixed) {
At this point the private varialble _rsm is no longer set and the house comes crashing down with the following exception
Notice: Trying to get property of non-object in /var/www/symfony/xesapps/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php line 480
Is it possible that calling the additional query during the postLoad event is killing the ResultSetManager being used to hydrate the object being loaded? If so, is there any way around this? Do I need to approach this in a different way?
PS - this works great in another controller where the only object being hydrated is the parent object. It fails in a controller where the object is being hydrated as part of an object graph.
You are probably trying to access an association in the postLoad event.
From the documentation:
Note that the postLoad event occurs for an entity before any
associations have been initialized. Therefore it is not safe to access
associations in a postLoad callback or event handler.
Just a note for anyone who finds this question in the future. There is a workaround. It isn't pretty, but it seems to work. You can read more at http://www.doctrine-project.org/jira/browse/DDC-1010
That ticket also points to committed code in doctrine two work around this issue - https://github.com/doctrine/doctrine2/commit/8d13601e39d0fdefdd1d2c0a85704c440b8bdd37, so hopefully a better solution will be coming soon.

Resources