Symfony2 - Doctrine - no changeset in post update - php

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);

Related

Persist another object withing a lifecycle event

Within a given lifecycle callback, can another object be modified and added to the same flush? Originally, I didn't include a second flush() in the callback and it wasn't being saved. I try adding one and it is saved but initiates multiple calls to EntityOne::preFlush() and I needed to add a flag to ignore the future calls which doesn't seem right. What is the correct way to do this?
<?php
class MyService
{
public function save()
{
//...
$this->em->persist($entityTwo);
$this->em->flush();
}
}
class EntityOne
{
/**
* #var EntityTwo
*/
private $entityTwo;
private $preFlushComplete=false;
/**
* #ORM\PreFlush
*/
public function preFlush(PreFlushEventArgs $event)
{
if(!$this->preFlushComplete) {
$this->preFlushComplete=true;
$this->entityTwo->setTime(now());
$event->getEntityManager()->persist($this->entityTwo);
}
}
}

Symfony delete ManyToMany related object when one part is empty?

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);
}
}
}
}

Calling a service inside a lifecycle event

I have a lifecycle event. As soon as an order is created the prePersist lifecycle event add a few more details to the order before it is persisted to the database.
This is my prePersist event class;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Qi\Bss\BaseBundle\Entity\Business\PmodOrder;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* Listener class
* Handles events related to list prices
*/
class OrderUserListener
{
/**
* Service container
* #var type
*/
private $serviceContainer;
/**
* Performs tasks before destruction
* #ORM\PrePersist
*/
public function prePersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$user = $this->serviceContainer->get('security.token_storage')->getToken()->getUser();
if ($user) {
$order->setCreatedBy($user);
$order->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
$order->setDepartment($user->getDepartment());
$order->setStatus(PmodOrder::STATUS_AWAITING_APPROVAL);
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
}
/**
* Sets the sales order exporter object
* #param type $serviceContainer
*/
public function setServiceContainer($serviceContainer)
{
$this->serviceContainer = $serviceContainer;
}
}
It works perfectly but this part $this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created'); doesn't want to work. I try to call a service inside it. I know the service works perfectly inside my controllers, but here I get an error;
A new entity was found through the relationship
'Qi\Bss\BaseBundle\Entity\Business\PmodLog#order' that was not
configured to cascade persist operations for entity: Nuwe Test vir
logger. 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"}).
This is how my OrderLogger service class looks like;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use Qi\Bss\BaseBundle\Entity\Business\PmodLog;
/**
* Class AppLogger. Purchase Module logger.
* #package FcConnectBundle\Lib
*/
class OrderLogger {
private $em;
private $tokenStorage;
/**
* Constructor.
*
* #param EntityManager $em
* #param TokenStorage $securityTokenStorage
*/
public function __construct(EntityManager $em, TokenStorage $securityTokenStorage)
{
$this->em = $em;
$this->tokenStorage = $securityTokenStorage;
}
/**
* Log an order action.
*
* #param string $text
*/
public function log($order, $action)
{
$logRecord = new PmodLog();
if (is_object($this->tokenStorage->getToken())) {
$user = $this->tokenStorage->getToken()->getUser();
if (is_object($user)) {
$logRecord->setUser($user);
}
}
$logRecord->setOrder($order);
$logRecord->setAction($action);
$logRecord->setTime(new \DateTime());
$this->em->persist($logRecord);
$this->em->flush();
}
}
I have already tried changing the persist in my log to merge, but that also doesn't work. Can somebody please help and explain what I do wrong?
This is not the best architecture, but it will work:
On prePersist add all messages to some kind of private variable (like $logMessages), and add another event
/**
* #param PostFlushEventArgs $args
*/
public function postFlush(PostFlushEventArgs $args)
{
$logMessages = $this->logMessages;
$this->logMessages = array(); //clean to avoid double logging
if (!empty($logMessages)) {
foreach ($logMessages as $message) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($message);
}
}
}
I fixed the problem by adding a postPersist and call the logger in there instead of inside my prePersist;
/**
* Performs tasks before destruction
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
Because what I think is happening is that the logger tries to be executed but the order in the logger doesn't yet exists as it is not yet persisted. This way makes more sense to me, and I think this is the easiest fix. I could be wrong though, any comments and other opinions on my answer are welcome.

FOSUserBundle adding a group to the user wont do anything

I am using FOSuser with SonataUserBundle and I am trying to add a user to the Clients group everytime someone registers, but it doesnt work. I dont get any errors, but I am not adding the group either... I tried it in two ways:
1) I overwritten the registrationController and made the confirmAction save the new group like this:
/**
* Tell the user his account is now confirmed
*/
public function confirmedAction()
{
$repository = $em->getRepository('ApplicationSonataUserBundle:Group');
$group = $repository->findOneByName('Clients');
$em = $this->getDoctrine()->getEntityManager();
$user = $this->getUser();
$user->addGroup($group);
$this->em->flush();
$userManager = $this->get('fos_user.user_manager');
$userManager->updateUser($user);
}
}
2) Icreated an eventListener and made the groupping there:
<?php
namespace Application\Sonata\UserBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityManager;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class GrouppingListener implements EventSubscriberInterface
{
protected $em;
protected $user;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$this->user = $event->getForm()->getData();
$entity = $this->em->getRepository('ApplicationSonataUserBundle:Group')->findOneByName('Clients'); // You could do that by Id, too
$this->user->addGroup($entity);
$this->em->flush();
}
}
My group entity is being extended like this:
<?php
/**
* This file is part of the <name> project.
*
* (c) <yourname> <youremail>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseGroup as BaseGroup;
/**
* This file has been generated by the Sonata EasyExtends bundle ( http://sonata-project.org/bundles/easy-extends )
*
* References :
* working with object : http://www.doctrine-project.org/projects/orm/2.0/docs/reference/working-with-objects/en
*
* #author <yourname> <youremail>
*/
class Group extends BaseGroup
{
/**
* #var integer $id
*/
protected $id;
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
}
None of these options worked... I did this based on other stackoverflow answers...Why wont it work?
You're missing persist in both of your cases. In order for an entity to become manageable, it needs to be persisted first.
$user->addGroup($group);
$this->em->flush();
change this in your controller action to:
$user->addGroup($group);
$this->em->persist($user);
$this->em->flush();
A paragraph from Doctrine manual:
An entity can be made persistent by passing it to the EntityManager#persist($entity) method. By applying the persist operation on some entity, that entity becomes MANAGED, which means that its persistence is from now on managed by an EntityManager. As a result the persistent state of such an entity will subsequently be properly synchronized with the database when EntityManager#flush() is invoked.

Doctrine PreUpdate never worked

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.

Categories