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.
Related
I have been looking around for a clean solution on how to update (keep in sync) a many to many relationship?
I have the following scenario:
A Sprint Entity owns the Many To Many relationship towards the Ticket entity.
When editing a Ticket (or Sprint, but I am not there yet), I want to be able to select (checkboxes) the Sprints that this ticket belongs to.
Upon persistance (save), I want to update my join table tickets_sprint (which is just a join table on ticket_id, sprint_id).
Adding Sprints to the Ticket seems easy enough, but removing Sprints from the Ticket is not reflected at all.
Code
Ticket Entity contains this method for adding a Ticket to a Sprint:
public function setSprints($sprints) {
/**
* #var $sprint \AppBundle\Entity\Sprint
*/
foreach ($sprints as $sprint) {
$this->sprints[] = $sprint;
$sprint->addTicket($this);
}
}
I have read here that the only way to go would be to remove all relations and re-save them upon persistance.
Coming from the Laravel world, this hardly feels like a good idea :)
This is how it is done in Laravel:
/**
* #param \App\User $user
* #param \App\Http\Requests\StoreUserRequest $request
* #return \Illuminate\Http\RedirectResponse
* Update the specified resource in storage.
*/
public function update(User $user, StoreUserRequest $request)
{
$user->fill($request->input());
$user->employee_code = strtolower($user->employee_code);
$user->roles()->sync($request->role ? : []);
$user->save();
\Session::flash('flash_message_success', 'The user was successfully updated.');
return redirect()->route('frontend::users.show', [$user]);
}
All suggestions are welcome!
The EntityType that you may use to create a multiple selectbox doesn't have a by_reference option like CollectionType.
If your Ticket Entity use the "inversedBy" side, you don't need to add the reference in the other object. So you can symply do this :
public function setSprints($sprints) {
$this->sprints = $sprints;
}
Maybe this will be enough to add and remove your elements automatically (Sorry didn't try).
Otherwise you have to do it manually and you can create a new method to remove elements returns by the difference between your new ArrayCollection and the old one.
is it possible whem element is removed from collection in front end to keep the item marked to remove in DB, only detached it from original object, so the reference to that object will be null?
It's done via Symfony basic collection remove/add way in front end. But no matter what I try, the entity, which user remove from the form in front end is deleted.
Even that I comment the code which actually remove it from collection, entity is still deleted.
/**
* Remove address.
*
* #param Address $address
*/
public function removeAddress(Address $address)
{
//$this->addresses->removeElement($address);
$address->setContact(null);
}
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!
I have a tree of Employee objects (they are in a tree-like hierarchy, with everyone having one leader, and all leaders having more employees). All the Employees have a integer parameter called units.
/**
* #ORM\Entity
* #ORM\Table(name="employees")
*/
class Employee
{
/**
* #ORM\Id
* #ORM\Column(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Employee", mappedBy="leader")
*/
protected $employees;
/**
* #ORM\ManyToOne(targetEntity("Employee", inversedBy="employees")
*/
protected $leader;
}
I need to get all the employees, who have at most N units, where N is defined in config.yml. At first, I was trying to push $configContainer into $GLOBALS, and use it in ArrayCollection::filter()'s Closure. Now I found a method, so I can use variables in the Closure:
public function getBestEmployees(&$configContainer)
{
return $this->getAllEmployees()->filter(
function bestEmployees($employee) use ($configContainer)
{
return ($employee->getUnits() >= $configContainer->getParameter('best_unit_count'));
}
);
}
Now I wonder if there is any other way to access the configuration parameters from an Entity, or do I really have to pass the whole configContainer as a reference? Or am I doing it totally wrong?
You shouldn't be accessing the service container at all inside entities. The value itself should be passed instead
public function getBestEmployees($bestUnitCount)
{
return $this->getAllEmployees()->filter(function ($employee) use ($bestUnitCount) {
return $employee->getUnits()->count() >= $bestUnitCount;
});
}
Of course, we haven't actually solved the problem yet: the parameter still needs to be fetched from the container somewhere. If this method gets invoked mostly in controller actions, I wouldn't bother doing any extra work to make things cleaner and would pass the container parameter straight in the controller action.
However, should there be a need to get the best employees in a Twig template, for example, it would be nice if it wouldn't be necessary to pass the parameter. One possibility would be using a setter method and passing the parameter down beforehand to each and every entity that gets retrieved from the database. You could do this either in repositories or entitiy managers. The most advanced solution would be to listen to the postLoad event and pass the parameter in an event listener.
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);
}
}