Symfony 3 / Doctrine - Get changes to associations in entity change set - php

So I already know that I can get changes to a specific entity in the preUpdate lifecycle event:
/**
* Captures pre-update events.
* #param PreUpdateEventArgs $args
*/
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof ParentEntity) {
$changes = $args->getEntityChangeSet();
}
}
However, is there a way to also get changes for any associated Entities? For example, say ParentEntity has a relationship setup like so:
/**
* #ORM\OneToMany(targetEntity="ChildEntity", mappedBy="parentEntity", cascade={"persist", "remove"})
*/
private $childEntities;
And ChildEntity also has:
/**
* #ORM\OneToMany(targetEntity="GrandChildEntity", mappedBy="childEntity", cascade={"persist", "remove"})
*/
private $grandChildEntities;
Is there a way to get all relevant changes during the preUpdate of ParentEntity?

All of the associated entities from a OneToMany or ManyToMany relationships appear as a Doctrine\ORM\PersistentCollection.
Take a look at the PersistentCollection's API, it have some interesting public methods even if they are marked as INTERNAL: https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/PersistentCollection.php#L308
For example you can check if your collection is dirty which means that its state needs to be synchronized with the database. Then you can retrieve the entities that have been removed from the collection or inserted into it.
if ($entity->getChildEntities()->isDirty()) {
$removed = $entity->getChildEntities()->getDeleteDiff();
$inserted = $entity->getChildEntities()->getInsertDiff();
}
Also you can get a snapshot of the collection at the moment it was fetched from the database: $entity->getChildEntities()->getSnapshot();, this is used to create the diffs above.

May be this is not optimal, but it can do the job. You can add a version field on ParentEntiy with a timestamp, then on each related entity setter function (Child or GranChild) you need to add a line updating that parent timestamp entity. In this way each time you call a setter you will produce a change on the parent entity that you can capture at the listener.
I have used this solution to update ElasticSearch documents that need to be updated when a change happens on a child entity and it works fine.

Related

Issue with Doctrine OneToMany relationship

I have an issue with the doctrine relationship. I try different ways but anything won't work.
Idea is that I have a News entity and every news should have many comments. So I try next:
The News entity:
/**
* #ORM\OneToMany(targetEntity="App\ORM\Entity\NewsComment", mappedBy="news")
*/
protected \Doctrine\Common\Collections\Collection $comments;
/**
* News constructor.
*/
public function __construct() {
$this->comments = new ArrayCollection();
}
And NewsComment entity:
/**
* #ORM\ManyToOne(targetEntity="App\ORM\Entity\News", inversedBy="comments")
*/
protected \App\ORM\Entity\News $news;
Every entity has its own get and set methods as well.
But, when I receive a News entity a can get comments collection but it always empty. On the other hand, I can take any NewsComment entity and get from this News entity. It is working fine. But not to another way.
Is anything wrong with my code?
Doctrine sets owned (non-inversed) collection as lazy by default.
When retrieving an entity by database, you should see an empty PersistentCollection instead of ArrayCollection, with initialized property set to false.
When calling any method on that collection, doctrine fires the queries needed to initialize the collection and populate it.
Collection emptiness should be only checked invoking isEmpty.

Doctrine launches INSERT always instead of UPDATE for existing entities

Let's say I have the following entities:
App\Entity\MainEntity:
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType1FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType1;
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType2FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType2;
Basically, one-directional 1:1 relationship from main entity to the same dependent entity using two different columns in the main entity table.
It doesn't matter, whether I use fetch="EAGER" or normal lazy loading through Doctrine proxy classes, when I do something like this:
$mainEntity = $this->mainEntityRepository->find(74);
$mainEntity->setDependentEntityType1($this->dependentEntityRepository->find(35));
$this->mainEntityRepository->saveTest($mainEntity);
where ::saveTest() is:
public function saveTest(MainEntity $mainEntity) {
$this->_em->persist($mainEntity->getDependentEntityType1());
$this->_em->merge($mainEntity);
$this->_em->flush();
}
it always tries to INSERT a new dependent entity to the table, even though I never made any changes (and even if I made them, it should have been UPDATE! for it)
The question is: why does Doctrine decide this dependent entity is a new one if I did $this->dependentEntityRepository->find(35) , so loaded an existing one?
I tried fetch="EAGER" thinking that spl_object_hash might return different hashes for a Proxy class instance and the actual DependantEntity one, but it doesn't matter, the DependantEntity is for some reason always considered as "new".
UPDATE: here is the code of ::setDependentEntityType1()
public function setDependentEntityType1(DependentEntity $dependentEntity) : void {
$this->dependentEntity = $dependentEntity;
}

Doctrine force event on parent entity?

Is there a way to force a doctrine event ( like preUpdate ) on a parent associated entity ?
So for example: I have a order entity with one-to-many orderItem entities.
Now, I want to do a bunch of checkup's and possible changes to the order entity or even one of it's orderItem entities ( where I need to access many other services) whenever any of the orderItems change. But the doctrine events do not fire on the order entity when one of its orderItem entities changes.
Note: this post entirely focuses on the particular case of the preUpdate event. It is possible to dispatch an event manually by using the event manager. The problem lies in the fact that simply triggering the preUpdate event of an entity is not enough to have its new state persisted to the database if the preUpdate method modified something.
There are multiple ways to do this but none of them are really straightforward. Considering only the case of the preUpdate event, I had quite a lot of trouble to find how to do this in a clean way as association updates are simply not built in a way to handle such cases as discussed in the Doctrine documentation.
Either way, if you want to do this, among the solutions I found, there were many that suggested to directly mess up with the UnitOfWork of Doctrine. This can be quite powerful but then you have to be careful about what you use and when you use it as Doctrine might not be able to actually dispatch the event you want in some cases discussed below.
Anyway, I ended up implementing something that makes use of a change of tracking policy for the parent entity. By doing so, the parent entity preUpdate event can be triggered if one of its properties is modified or if one of its "children" was modified.
Main concerns with the UnitOfWork
If you wish to use the UnitOfWork (that you can retrieve by using $args->getEntityManager()->getUnitOfWork() with any type of arguments of lifecycle events), you can use the public method scheduleForUpdate(object $entity). However, if you wish to use this method, you will need to call it before the commit order is computed inside of the unit of work. Moreover, if you have a preUpdate event associated to the entity you scheduled for update, it will raise an error if your entity has an empty change set (which is exactly the case we are dealing with when the main entity is not modified but one of its related entities is).
Thus calling $unitOfWork->scheduleForUpdate($myParentEntity), in a preUpdate of a child entity is not an option as explained in the documentation (performing calls to the UnitOfWork API is strongly discouraged as it does not work as it would outside of the flush operation).
It should be noted that $unitOfWork->scheduleExtraUpdate($parentEntity, array $changeset) can be used in that specific context but this method is marked as "INTERNAL". The following solutions avoid using it but it might be a good approach if you know what you are getting into.
Possible solutions
Note: I did not test the implementation of the wanted behaviour with the onFlush event but it was often presented as the most powerful approach. For the other two possibilities listed here, I tried them successfully with a OneToMany association.
In the following section, when I'm talking about a parent entity, I refer to the entity that has the OneToMany association while children entities are refering to the entities that have the ManyToOne association (thus, the children entities are the owning side of the association).
1. Using onFlush event
You can try to work your way out of this by using the onFlush event however, in that case you have to deal with the UnitOfWork internals as suggested in the documentation. In that case, you can't do it within an Entity listener (introduced in 2.4) as the onFlush event is not among the possible callbacks. Some examples based on what's given by the official doc can be found on the web. Here is a possible implementation: Update associated entities in doctrine.
The main drawback here is that you don't really trigger the preUpdate event of your entity, you just handle the behaviour you wanted somewhere else. It seemed a bit too heavy handed for me, so I searched for other solutions.
2. Using the UnitOfWork in preFlush event of the child entities
One way to actually trigger the preUpdate event of the parent entity, is to add another entity listener to the child entity and to use the UnitOfWork. As explained before, you can't simply do this in the preUpdate event of the child entity.
In order for the commit order to be properly computed, we need to call scheduleForUpdate and propertyChanged in the preFlush event of the child entity listener as shown below:
class ChildListener
{
public function preFlush(Child $child, PreFlushEventArgs $args)
{
$uow = $args->getEntityManager()->getUnitOfWork();
// Add an entry to the change set of the parent so that the PreUpdateEventArgs can be constructed without errors
$uow->propertyChanged($child->getParent(), 'children', 0, 1);
// Schedule for update the parent entity so that the preUpdate event can be triggered
$uow->scheduleForUpdate($child->getParent());
}
}
As you can see, we need to notify the UnitOfWork that a property has changed so that everything works properly. It looks a bit sloppy but it gets the work done.
The important part is that we mark the children property (the OneToMany association of the parent) as changed so that the change set of the parent entity is not empty. A few important notes about the internals at stake with this propertyChanged call:
The method expects a persistent field name (non-persistent ones will be ignored), any mapped field will do, even associations, that is why using children works here.
The change set that is modified consecutively to this call does not have any side effects here as it will be recomputed after the preUpdate event.
The main problem of this approach is that the parent entity is scheduled for update even if it is not needed. As there is no direct way to tell if the child entity has changed in its preFlush event (you could use the UnitOfWork but it would become a bit redundant with its internals), you will trigger the preUpdate event of the parent at every flush where a child entity is being managed.
Moreover, with this solution, Doctrine will begin a transaction and commit even if there are no queries performed (e.g. if nothing was modified at all, you will still find in the Symfony Profiler, two consecutives entries "START TRANSACTION" and "COMMIT" in the Doctrine logs).
3. Change the tracking policy of the parent and handle the behaviour explicitly
Since I've been messing with the internals of the UnitOfWork quite a bit, I stumbled upon the propertyChanged method (that was used in the previous solution) and noticed that it was part of the interface PropertyChangedListener. It happens that this is linked to a documented topic: the tracking policy. By default, you can just let Doctrine detect the changes but you can also change this policy and manage everything manually as explained here, in the documentation.
After reading about this, I eventually came up with the following solution that cleanly handles the wanted behaviour, the cost being that you have to do some extra work in your entities.
Thus, to have exactly what I desired, my parent entity follows the NOTIFY tracking policy and children notify the parent when one of their properties is modified. As described in the official documentation, you have to implement the NotifyPropertyChanged interface and then notify the listeners of properties changes (the UnitOfWork automatically adds itself to the listeners if it detects that one of the managed entities implements the interface). After that, if the annotation #ChangeTrackingPolicy is added, at commit times, Doctrine will rely on the change set that was built via propertyChanged calls and not on an automatic detection.
Here is how you would do it for a basic Parent entity:
namespace AppBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
/**
* ... other annotations ...
* #ORM\EntityListeners({"AppBundle\Listener\ParentListener"})
* #ORM\ChangeTrackingPolicy("NOTIFY")
*/
class Parent implements NotifyPropertyChanged
{
// Add the implementation satisfying the NotifyPropertyChanged interface
use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;
/* ... other properties ... */
/**
* #ORM\Column(name="basic_property", type="string")
*/
private $basicProperty;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Child", mappedBy="parent", cascade={"persist", "remove"})
*/
private $children;
/**
* #ORM\Column(name="other_field", type="string")
*/
private $otherField;
public function __construct()
{
$this->children = new \Doctrine\Common\Collections\ArrayCollection();
}
public function notifyChildChanged()
{
$this->onPropertyChanged('children', 0, 1);
}
public function setBasicProperty($value)
{
if($this->basicProperty != $value)
{
$this->onPropertyChanged('basicProperty', $this->basicProperty, $value);
$this->basicProperty = $value;
}
}
public function addChild(Child $child)
{
$this->notifyChildChanged();
$this->children[] = $child;
$child->setParent($this);
return $this;
}
public function removeChild(Child $child)
{
$this->notifyChildChanged();
$this->children->removeElement($child);
}
/* ... other methods ... */
}
with the trait taken from the code given in the documentation:
namespace AppBundle\Doctrine\Traits;
use Doctrine\Common\PropertyChangedListener;
trait NotifyPropertyChangedTrait
{
private $listeners = [];
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listeners[] = $listener;
}
/** Notifies listeners of a change. */
private function onPropertyChanged($propName, $oldValue, $newValue)
{
if ($this->listeners)
{
foreach ($this->listeners as $listener)
{
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
}
and the following Child entity with the owning side of the association:
namespace AppBundle\Entity;
class Child
{
/* .. other properties .. */
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Parent", inversedBy="children")
*/
private $parentEntity;
/**
* #ORM\Column(name="attribute", type="string")
*/
private $attribute;
public function setAttribute($attribute)
{
// Check if the parentEntity is not null to handle the case where the child entity is created before being attached to its parent
if($this->attribute != $attribute && $this->parentEntity)
{
$this->parentEntity->notifyChildChanged();
$this->attribute = $attribute;
}
}
/* ... other methods ... */
}
And there it is, you have everything fully working. If, your child entity is modified, you explicitly call notifyChildChanged that will then notify the UnitOfWork that children field has changed for the parent entity thus cleanly triggering the update process and the preUpdate event if one is specified.
Unlike the solution #2, the event will be triggered only if something has changed and you can control with precision why it should be marked as changed. For example, you could mark the children as changed if only a certain set of attributes is changed and ignore other changes as you have full control other what is eventually notified to the UnitOfWork.
Note:
With the NOTIFY tracking policy, apparently, preFlush events won't be triggered in the Parent entity listener (preFlush event being triggered in computeChangeSet which is simply not called for entities using this policy).
It is necessary to track every "normal" property to trigger updates if normal properties are changed. One solution to do this without having to modify all your setters is to use magic calls as shown below.
It is safe to set a children entry in the change set as it will be simply ignored when the update query is created since the parent entity is NOT the owning side of the association. (i.e. it does not have any foreign keys)
Use of magic calls to handle notifications easily
In my application, I added the following trait
namespace AppBundle\Utils\Traits;
trait MagicSettersTrait
{
/** Returns an array with the names of properties for which magic setters can be used */
abstract protected function getMagicSetters();
/** Override if needed in the class using this trait to perform actions before set operations */
private function _preSetCallback($property, $newValue) {}
/** Override if needed in the class using this trait to perform actions after set operations */
private function _postSetCallback($property, $newValue) {}
/** Returns true if the method name starts by "set" */
private function isSetterMethodCall($name)
{
return substr($name, 0, 3) == 'set';
}
/** Can be overriden by the class using this trait to allow other magic calls */
public function __call($name, array $args)
{
$this->handleSetterMethodCall($name, $args);
}
/**
* #param string $name Name of the method being called
* #param array $args Arguments passed to the method
* #throws BadMethodCallException if the setter is not handled or if the number of arguments is not 1
*/
private function handleSetterMethodCall($name, array $args)
{
$property = lcfirst(substr($name, 3));
if(!$this->isSetterMethodCall($name) || !in_array($property, $this->getMagicSetters()))
{
throw new \BadMethodCallException('Undefined method ' . $name . ' for class ' . get_class($this));
}
if(count($args) != 1)
{
throw new \BadMethodCallException('Method ' . $name . ' expects 1 argument (' . count($args) . ' given)');;
}
$this->_preSetCallback($property, $args[0]);
$this->$property = $args[0];
$this->_postSetCallback($property, $args[0]);
}
}
which I could then use in my entities. Here is an example of my Tag entity whose preUpdate event needed to be called when one of its aliases was modified:
/**
* #ORM\Table(name="tag")
* #ORM\EntityListeners({"AppBundle\Listener\Tag\TagListener"})
* #ORM\ChangeTrackingPolicy("NOTIFY")
*/
class Tag implements NotifyPropertyChanged
{
use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;
use \AppBundle\Utils\Traits\MagicSettersTrait;
/* ... attributes ... */
protected function getMagicSetters() { return ['slug', 'reviewed', 'translations']; }
/** Called before the actuel set operation in the magic setters */
public function _preSetCallback($property, $newValue)
{
if($this->$property != $newValue)
{
$this->onPropertyChanged($property, $this->$property, $newValue);
}
}
public function notifyAliasChanged()
{
$this->onPropertyChanged('aliases', 0, 1);
}
/* ... methods ... */
public function addAlias(\AppBundle\Entity\Tag\TagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases[] = $alias;
$alias->setTag($this);
return $this;
}
public function removeAlias(\AppBundle\Entity\Tag\TagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases->removeElement($alias);
}
}
I can then reuse the same trait in my "child" entity named TagAlias:
class TagAlias
{
use \AppBundle\Utils\Traits\MagicSettersTrait;
/* ... attributes ... */
public function getMagicSetters() { return ['alias', 'main', 'locale']; }
/** Called before the actuel set operation in the magic setters */
protected function _preSetCallback($property, $newValue)
{
if($this->$property != $newValue && $this->tag)
{
$this->tag->notifyAliasChanged();
}
}
/* ... methods ... */
}
Note: If you chose to do this, you might encounter errors when Forms are trying to hydrate your entities as magic calls are disabled by default. Simply add the following to your services.yml to enable magic calls. (taken from this discussion)
property_accessor:
class: %property_accessor.class%
arguments: [true]
A more pragmatic approach is to version your parent entity. A simple example of this would be a timestamp (e.g. updated_at) that is updated when the collection of child entities is modified. This assumes you update all the child entities through its parent.

Symfony does not remove entity from collection

I know there are loads of posts on this topic in general. Unfortunately those mostly deal with the actual persist-operation to the database. In my case I have a problem that happens before the persist-operation:
I have a form with a (Doctrine) persistenceCollection of entities. You can remove "objects" from the DOM via javascript. After submit, when handleRequest is called on the form, the function in my entity is called which removes the entity from the collection in the object itself, and it is called as I can check in the debugger:
/**
* Remove prices
*
* #param \Whizzpm\Bundle\Entity\Supplier\SupplierPrice $prices
*/
public function removePrice(\Whizzpm\Bundle\Entity\Supplier\SupplierPrice $prices)
{
$this->prices->removeElement($prices);
}
And this is the definition of $prices:
/**
* #var
* #ORM\OneToMany(targetEntity="SupplierPrice", mappedBy="priceList", cascade={"all"})
*/
private $prices;
The basic idea is to compare the updated entity with it's previous state but after the function above has finished the entitiy is still in the collection.
To make this more precise: If I check $this right after the "removeElement($prices)" is through, it still contains the object that just should have been removed.
Maybe this is important:
supplier (main entity)
pricelist (property of main entity - also entity itself)
prices (property of pricelist, collection of entities (price items)
prices is the collection of which the element (price item) should be removed.
Any thoughts on this? I can add any information you need on this question I just don't know, which of it makes sense, sincer there are loads.
Finally I found a solution in this post:
removeElement() and clear() doesn't work in doctrine 2 with array collection property
I have to unset the corresponding value in the owning entity too:
public function removePrice(\Whizzpm\Bundle\Entity\Supplier\SupplierPrice $prices)
{
$this->prices->removeElement($prices);
$prices->setPriceList(null);
}
and add orphanRemoval=true to the entity collection
/**
* #var
* #ORM\OneToMany(targetEntity="SupplierPrice", mappedBy="priceList", cascade={"all"}, orphanRemoval=true)
*/
private $prices;

Doctrine 2 Entities Relations Remove

I have an owning entity that has the following relation to an "attribute" entity:
/**
* #ORM\OneToMany(targetEntity="Attribute", mappedBy="entity", cascade={"persist", "remove", "merge"})
**/
protected $attributes;
On the side, the owned entity relation looks like this:
/**
* #ORM\ManyToOne(targetEntity="Entity", inversedBy="attributes")
* #ORM\JoinColumn(name="entity_id", referencedColumnName="id")
*/
protected $entity;
When I create an instance of an entity, add attributes to it and save it. It all works fine.
When I remove one attribute from the entity and persist, the attribute is not deleted in the database and re-appears upon refresh.
Anyone has an idea?
Solution
What you're looking for is orphan removal.
You can read on if you'd like details on why your current situation isn't working.
Cascade woes
The cascade operation won't do what you want unfortunately. The "cascade=[remove]" just means that if the entity object is removed then doctrine will loop through and remove all child attributes as well:
$em->remove($entity);
// doctrine will basically do the following automatically
foreach ($entity->getAttributes() as $attr)
{
$em->remove($attr);
}
How to do it manually
If you needed to remove an attribute from an entity you'd delete the attribute like so:
$entity->getAttributes()->removeElement($attr);
$em->remove($attribute);
Solution details
But, to do that automatically we use the orphan removal option. We simply tell doctrine that attributes can only belong to entities, and if an attribute no longer belongs to an entity, simply delete it:
/**
* #ORM\OneToMany(targetEntity="Attribute", mappedBy="entity", orphanRemoval=true, cascade={"persist", "remove", "merge"})
**/
protected $attributes;
Then, you can remove the attribute by simply doing this:
$entity->getAttributes()->removeElement($attr);
Be careful when using orphan removal.
If you remove an element and then call refresh on the main entity the element is not removed from the internal orphan removal array of doctrine.
And if flush is called later, will result in removing that entry from the db, ignoring the refresh.
This looks like a bug to me, and resulted in loss of images on a lot of products in my app. I had to implement a listener to call persist again on those entities, after they ware scheduled for delete.

Categories