Doctrine PreUpdate never worked - php

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.

Related

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

Sonata Admin - Unable to get sonata_type_collection working with modal

In my admin I have a OneToMany defined as it:
/**
* #ORM\OneToMany(targetEntity="Module", mappedBy="sequence", cascade={"persist", "remove"})
*/
private $modules;
And the inversed side:
/**
* #ORM\ManyToOne(targetEntity="ModuleSequence", inversedBy="modules", cascade={"persist"}, fetch="LAZY")
* #ORM\JoinColumn(name="sequence_id", referencedColumnName="id")
*/
protected $sequence;
In my admin class I defined the 'modules' field as it:
->add('modules', 'sonata_type_collection',array(
'by_reference' => false
))
Finally in the ModuleSequence Entity here's the addModule method:
/**
* Add modules
*
* #param \AppBundle\Entity\Module $module
* #return ModuleSequence
*/
public function addModule(\AppBundle\Entity\Module $module)
{
$module->setSequence($this);
$this->modules[] = $module;
return $this;
}
I have the "add" button, I get the modal, I fill it and validate. The Ajax request is sent into the profiler but no new row appear.
The 'sequence_id' is not set in the database and I don't know why... Any idea please?
When I use the 'inline' & 'table' options, the id is well set.
Had the same issue and solved it with overriding the prePersist and preUpdate methods and then persist the associations.
public function prePersist($object)
{
$this->persistBlocks($object);
$content->mergeNewTranslations();
}
public function preUpdate($object)
{
$this->prePersist($object);
}
private function persistModules($object)
{
$em = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager');
foreach($object->getModules() as $module)
{
$module->setObject($object); // change Object to the name of your Entity!!
$em->persist($module);
}
}
After a long discussion on Sonata Admin GitHub here:
https://github.com/sonata-project/SonataAdminBundle/issues/4236
Problem partially solved...

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.

Symfony2 - Doctrine - no changeset in post update

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

Doctrine doesn't load associations from session correctly

I'm having this behavior with Doctrine 2.1 where I'm looking for a nice 'workaround'. The problem is as follows:
I have a user Entity:
/**
* #Entity(repositoryClass="Application\Entity\Repository\UserRepository")
* #HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var \DateTime
* #Column(type="datetime",nullable=false)
*/
protected $insertDate;
/**
*
* #var string
* #Column(type="string", nullable=false)
*/
protected $username;
/**
*
* #ManyToOne(targetEntity="UserGroup", cascade={"merge"})
*/
protected $userGroup;
}
And a usergroup entity:
/**
* #Entity
*/
class UserGroup extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var string
* #Column(type="string",nullable=false)
*/
protected $name;
}
If I instantiate a user object (doing this with Zend_Auth) and Zend_Auth puts it automatically the session.
The problem is however, that is I pull it back from the session at a next page then the data in the user class is perfectly loaded but not in the userGroup association. If I add cascade={"merge"} into the annotation in the user object the userGroup object IS loaded but the data is empty. If you dump something like:
$user->userGroup->name
You will get NULL back. The problem is no data of the usergroup entity is accesed before the user object is saved in the session so a empty initialized object will be returned. If I do something like:
echo $user->userGroup->name;
Before I store the user object in the session all data of the assocication userGroup is succesfully saved and won't return NULL on the next page if I try to access the $user->userGroup->name variable.
Is there a simple way to fix this? Can I manually load the userGroup object/association with a lifecycle callback #onLoad in the user class maybe? Any suggestions?
Your problem is a combination of what mjh_ca answered and a problem with your AbstractEntity implementation.
Since you show that you access entity fields in this fashion:
$user->userGroup->name;
I assume your AbstractEntity base class is using __get() and __set() magic methods instead of proper getters and setters:
function getUserGroup()
{
return $this->userGroup;
}
function setUserGroup(UserGroup $userGroup)
{
$this->userGroup = $userGroup;
}
You are essentially breaking lazy loading:
"... whenever you access a public property of a proxy object that hasn’t been initialized yet the return value will be null. Doctrine cannot hook into this process and magically make the entity lazy load."
Source: Doctrine Best Practices: Don't Use Public Properties on Entities
You should instead be accessing fields this way:
$user->getUserGroup()->getName();
The second part of your problem is exactly as mjh_ca wrote - Zend_Auth detaches your entity from the entity manager when it serializes it for storage in the session. Setting cascade={"merge"} on your association will not work because it is the actual entity that is detached. You have to merge the deserialized User entity into the entity manager.
$detachedIdentity = Zend_Auth::getInstance()->getIdentity();
$identity = $em->merge($detachedIdentity);
The question, is how to do this cleanly. You could look into implementing a __wakeup() magic method for your User entity, but that is also against doctrine best practices...
Source: Implementing Wakeup or Clone
Since we are talking about Zend_Auth, you could extend Zend_Auth and override the getIdentity() function so that it is entity aware.
use Doctrine\ORM\EntityManager,
Doctrine\ORM\UnitOfWork;
class My_Auth extends \Zend_Auth
{
protected $_entityManager;
/**
* override otherwise self::$_instance
* will still create an instance of Zend_Auth
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
public function getEntityManager()
{
return $this->_entityManager;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->_entityManager = $entityManager;
}
public function getIdentity()
{
$storage = $this->getStorage();
if ($storage->isEmpty()) {
return null;
}
$identity = $storage->read();
$em = $this->getEntityManager();
if(UnitOfWork::STATE_DETACHED === $em->getUnitOfWork()->getEntityState($identity))
{
$identity = $em->merge($identity);
}
return $identity;
}
}
And than add an _init function to your Bootstrap:
public function _initAuth()
{
$this->bootstrap('doctrine');
$em = $this->getResource('doctrine')->getEntityManager();
$auth = My_Auth::getInstance();
$auth->setEntityManager($em);
}
At this point calling $user->getUserGroup()->getName(); should work as intended.
When you store the entity to a session (via Zend_Auth or otherwise), the object is serialized and no longer maintained by Doctrine when subsequently retrieved and unserialized. Try merging the entity back into the EntityManager. See http://www.doctrine-project.org/docs/orm/2.1/en/reference/working-with-objects.html

Categories