I'm trying to delete unused tags. Although the relationship between Post and Tag has been deleted, the post-linked tag is not deleted.
"orphanRemoval" does not work because it has deleted all. cascade "remove" does not delete.
Post Entity:
class Post implements \JsonSerializable
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Cms\PostTag", inversedBy="posts", cascade={"persist", "remove"})
* #ORM\JoinTable(name="post_tag_taxonomy")
* #Assert\Count(max="5")
*/
private $tags;
}
Tag Entity:
class PostTag {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Cms\Post", mappedBy="tags")
*/
private $posts;
}
Here's a similar example, but for Java. How to delete an ManyToMany related object when one part is empty?
I suggest you use preUpdate event from Doctrine life cycle callbacks. On the event of update a Post, you tell doctrine to check if there's a Tag change (in this case it's to NULL), if yes then query the Tag check if any posts still use it.
In short, you need to :
Add #ORM\HasLifecycleCallbacks before your class to enable life cycles.
Add preUpdate function in Post class :
/**
* #ORM\PreUpdate
* #param PreUpdateEventArgs $event
*/
public function clearChangeSet(PreUpdateEventArgs $event)
{
if ($event->hasChangedField('field_you_want_to_check')
) {
$em = $event->getEntityManager();
// Now use the entityManager to query the tag and check.
}
}
By doing this doctrine will do the check for you, in the logic code you just need to perform the unlinking, no need to care about delete tags there.
Update : as pointed out, for associations in entity you cannot get the changes by $event->hasChangedField, use the method in Symfony 3 / Doctrine - Get changes to associations in entity change set
Solution:
/**
* #ORM\PostUpdate()
*/
public function postUpdate(LifecycleEventArgs $args)
{
/** #var PersistentCollection $tags */
$tags = $args->getEntity()->getTags();
if ($tags->isDirty() && ($deleted = $tags->getDeleteDiff())) {
$em = $args->getEntityManager();
foreach ($deleted as $tag) {
if ($tag->getPosts()->count() === 1) {
$em->remove($tag);
$em->flush($tag);
}
}
}
}
Related
So i am sending an email when a certain value on an entity is changed. I only want the email to send after the update in case the update fails for what ever reason. so on the preUpdate I can do this
public function preUpdate(LifecycleEventArgs $args){
if ($args->hasChangedField('value') && is_null($args->getOldValue('value'))) {
$this->sendEmail();
}
}
but i need to do this on postUpdate and as these methods are not available on postUpdate i refactored it to look like this:
public function postUpdate(LifecycleEventArgs $args){
$entity = $args->getEntity();
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity);
if ($entity instanceof Entity && isset( $changeSet['value'] ) && empty( $changeSet['value'][0] )) {
$this->sendEmail();
}
}
However this returns an empty change set, but changes have been made and can be seen in preUpdate. Can anyone see what i am doing wrong? help would be much appreciated :)
On preUpdate event you get event object of class PreUpdateEventArgs where You have change set for entity.
On postUpdate you just get event object of class LifecycleEventArgs where you can ask only for Updated entity (and get latest state of it).
If you want to play with changeset then you need to do it before actual updating entity (preUpdate event).
A workaround could be to save change set somewhere by yourself and later retrieve it in postUpdate. It is a siplified exaple I've implement once:
<?php
namespace Awesome\AppBundle\EventListener;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
/**
* Store last entity change set in memory, so that it could be
* usable in postUpdate event.
*/
class EntityChangeSetStorageListener implements EventSubscriber
{
/**
* #var ArrayCache
*/
private $cache;
/**
* #param ArrayCache $cacheStorage
*/
public function __construct(ArrayCache $cacheStorage)
{
$this->cache = $cacheStorage;
}
/**
* Store last entity change set in memory.
*
* #param PreUpdateEventArgs $event
*/
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
$this->cache->setNamespace(get_class($entity));
$this->cache->save($entity->getId(), $event->getEntityChangeSet());
}
/**
* Release the memory.
*/
public function onClear()
{
$this->clearCache();
}
/**
* Clear cache.
*/
private function clearCache()
{
$this->cache->flushAll();
}
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [
Events::preUpdate,
Events::onClear,
];
}
}
Later inject ChangeSetStorage service to the listener where it is necessary on postUpdate event.
I had a really annoying issue with the changeset data, sometimes I got the collection of changes and sometimes not.
I sorted out by adding this line $event->getEntityManager()->refresh($entity); in the prePersist and preUpdate events inside a doctrine.event_subscriber
After the refresh line, changesetdata was updated so the following line started to work:
/** #var array $changeSet */
$changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($entity);
I've an entity Order, with a property events which should contain a list of all changes made to this entity.
The Order class:
<?php
/**
* #ORM\Entity
*/
class Order
{
// more stuff...
/**
* #ORM\OneToMany(
* targetEntity="OrderEvent",
* mappedBy="order",
* cascade={"persist", "merge"}
* )
*/
protected $events;
// more stuff...
}
The OrderEvent class:
/**
* #ORM\Entity
*/
class OrderEvent
{
// more stuff...
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
*/
protected $order;
// more stuff...
}
class OrderLifecycle
{
public function preUpdate(Order $order, PreUpdateEventArgs $args)
{
$changes = $args->getEntityChangeSet();
if (!empty($changes)) {
$event = new OrderEvent();
$event->setOrder($order)
->setChanges($changes);
$order->addEvent($event);
return $event;
}
}
}
But according to the doctrine documentation, the preUpdate method should not be used to change associations.
What is the recommended way to do things like this one?
I am using Zend Framework 2, but I think that's not relevant.
I think in this case you could use PostUpdate event. In that case you are sure that the update action was successful and you can do what you want; add the new OrderEvent instance to your $events collection.
EDIT
You are not the first one implementing such thing. Maybe you should check existing examples and see how they deal with this (or even consider using it). For example the Gedmo Loggable solution.
With this extension you can mark entities as loggable with a simple #annotiation:
/**
* #Entity
* #Gedmo\Loggable
*/
class Order
{
// Your class definition
}
I have searched, and there is a lot of questions wiith the same problem, but none of them solves to my issue.
I have an Entity, here is it's code:
/*
* #ORM\HasLifecycleCallbacks
*/
class MyEntity
{
// some preoperties here...
/**
* #ORM\Column(type="text", nullable=true)
* #Assert\MaxLength(limit="500")
*/
private $delivery = null;
/**
* #var $deliveryOn bool
*
* Virtual field used for $delivery property organization
*/
private $deliveryOn = false;
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preSetDelivery()
{
if ($this->deliveryOn == false)
$this->delivery = null;
}
/**
* #ORM\PostLoad()
*/
public function loadDeliveryOn()
{
if ($this->delivery != null)
$this->deliveryOn = true;
}
}
loadDeliveryOn method perfectly works all the time. But the preSetDelivery fired only when I persist the entity to the database for the first time. I want it to be called when object is updated too, but it doesn't work. And I have no any idea why.
My edit controller:
public function editAction($id)
{
// some code here...
// ...
$request = $this->getRequest();
if ($request->isMethod('POST'))
{
$form->bind($request);
if ($form->isValid())
{
$em->flush();
}
}
}
From official docs concerning preUpdate:
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.
If you have access to UnitOfWork maybe there is a way to recompute change-set as there is in onFlush?
if you use inheritance - for example #ORM\InheritanceType("SINGLE_TABLE")
you need to add #ORM\MappedSuperclass in the parent class
PreUpdate only fires if there are actual changes on the entity. If you don't change anything in the form, there will be no changes on the entity, and no preUpdate listeners will be called.
My problem is exactly that described in the Strategy Pattern article in Doctrine documentation :
A Page entity
A page can have some blocks
A block might be a text, an image, a form, a calendar, ... (strategy)
A page knows about blocks it contains, but doesn't know about their behaviours
Block inheritance is not possible
Described solution (Strategy pattern) seems exactly what I need (read article for further information) :
Page:
<?php
namespace Page\Entity;
class Page
{
/**
* #var int
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #var string
* #Column
*/
protected $title;
/**
* #var string
* #Column(type="text")
*/
protected $body;
/**
* #var Collection
* #OneToMany(targetEntity="Block", mappedBy="page")
*/
protected $blocks;
// ...
}
Block:
<?php
namespace Page\Entity;
class Block
{
/**
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #ManyToOne(targetEntity="Page", inversedBy="blocks")
*/
protected $page;
/**
* #Column
*/
protected $strategyClass;
/**
* Strategy object is instancied on postLoad by the BlockListener
*
* #var BlockStrategyInterface
*/
protected $strategyInstance;
// ...
}
Strategy interface:
<?php
namespace Page\BlockStrategy;
interface BlockStrategyInterface
{
public function setView($view);
public function getView();
public function setBlock(Block $block);
public function getBlock();
public function renderFrontend();
public function renderBackend();
}
I can easily imagine what would be my strategy if I would display a form or a calendar;
but what if my strategy is to display content of an other entity ?
The block needs to know about entity class/id and has to be deleted when related entity is removed.
I imagined to add entityClass and entityId properties in Block and load related entity on the postLoad event in a BlockListener.
But what if related entity doesn't exist ? I can't remove the block in postLoad.
So, I imagined to create an other listener watching for removal of related entity and remove refering Block in that listener.
But it means that I need to add a listener for every entity that can be put in a block.
It could work, but it seems very complicated... maybe someone has a better idea ?
I'm not sure if I understood well your question, but if what you want is to have entities inside entities and delete the child entities when the father is deleted, you could also treat the entity as a block and use a Composite pattern.
You basically could use the same interface on the entity and the blocks, and on the entity the display function could be something like:
foreach ($block in $blocks) {
$block->display();
}
For deleting all the children when you delete the parent entity, you could simply do it on the destructor of the entity.
function __destruct() {
foreach ($block in $blocks) {
/* call a common interface function that does all that need to be done, implemented on each block */
}
}
For more on Composite pattern:
http://en.wikipedia.org/wiki/Composite_pattern
Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2
The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).
I've used a pretty simple example of a Classroom having many Pupils to keep it simple.
So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.
Lets first define our Doctrine Entities.
The Classroom Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="classroom")
*/
class Classroom
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $miscVars;
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
*/
protected $pupils;
public function __construct()
{
$this->pupils = new ArrayCollection();
}
// ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============
}
The Pupil Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="pupil")
*/
class Pupil
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $moreVars;
/**
* #ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
* #ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
*/
protected $classroom;
// ========== GENERATED FUNCTIONS BELOW ============
}
And our generic Action function:
public function someAction(Request $request, $id)
{
$em = $this->getDoctrine()->getEntityManager();
$classroom = $em->find('AcmeTestBundle:Classroom', $id);
$form = $this->createForm(new ClassroomType(), $classroom);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// Normally you would do the following:
$em->persist($classroom);
$em->flush();
// But how do I create a new row with a new ID
// Including new rows for the Many side of the relationship
// ... other code goes here.
}
}
return $this->render('AcmeTestBundle:Default:index.html.twig');
}
I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.
Thanks in advance to any assistance.
The thing with clone is...
When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.
If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:
public function __clone() {
// Get current collection
$pupils = $this->getPupils();
$this->pupils = new ArrayCollection();
foreach ($pupils as $pupil) {
$clonePupil = clone $pupil;
$this->pupils->add($clonePupil);
$clonePupil->setClassroom($this);
}
}
NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.
When you clone your parent class, this function will create a new collection full of child object clones as well.
$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();
$em->persist($cloneClassroom);
$em->flush();
You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
*/
protected $pupils;
I did it like this and it works fine.
Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.
/**
* Clone element with values
*/
public function __clone(){
// we gonna clone existing element
if($this->id){
// get values (one-to-many)
/** #var \Doctrine\Common\Collections\Collection $values */
$values = $this->getElementValues();
// reset id
$this->id = null;
// reset values
$this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
// if we had values
if(!$values->isEmpty()){
foreach ($values as $value) {
// clone it
$clonedValue = clone $value;
// add to collection
$this->addElementValues($clonedValue);
}
}
}
}
/**
* addElementValues
*
* #param \YourBundle\Entity\ElementValue $elementValue
* #return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
if (!$this->getElementValues()->contains($elementValue))
{
$this->elementValues[] = $elementValue;
$elementValue->setElement($this);
}
return $this;
}
Somewhere just clone it:
// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
I do this:
if ($form->isValid()) {
foreach($classroom->getPupils() as $pupil) {
$pupil->setClassroom($classroom);
}
$em->persist($classroom);
$em->flush();
}