Persist another object withing a lifecycle event - php

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

Related

Type hinting additional class on a Laravel listener class

I have my event listener defined as this:
use App\Events\LeadCreated;
use App\Services\LeadService;
class NewLeadNotifyProspectListener
{
/**
* Handle the event.
*
* #param \App\Events\LeadCreated $event
* #param App\Services\LeadService $leadService
* #return void
*/
public function handle(LeadCreated $event, LeadService $leadService)
{
$leadService->notifyProspect($event->lead);
}
}
And my event
use App\Models\Lead;
class LeadCreated
{
public Request $request;
public Lead $lead;
public function __construct(Lead $lead, Request $request)
{
$this->request = $request;
$this->lead = $lead;
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
And I'm calling it in my controller like this:
LeadCreated::dispatch($lead, $request);
The error I'm receiving:
Too few arguments to function App\Listeners\NewLeadNotifyProspectListener::handle(), 1 passed in /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php on line 424 and exactly 2 expected
I wonder why I'm not able to type-hint LeadService in my listener? What's the proper way to include that class in my listener?
You are loading a dependency, you have to follow Dependency Injection rules.
In Laravel you can use Dependency Injection in Controllers like that, because that's build by Laravel/Symfony.
Add your dependency in the constructor instead.
use App\Events\LeadCreated;
use App\Services\LeadService;
class NewLeadNotifyProspectListener
{
private LeadService $leadService;
public function __construct(LeadService $leadService)
{
$this->leadService = $leadService;
}
/**
* Handle the event.
*
* #param \App\Events\LeadCreated $event
* #return void
*/
public function handle(LeadCreated $event)
{
$this->leadService->notifyProspect($event->lead);
}
}
If you have setup the NewLeadNotifyProspectListener correctly, Dependency Injection in Laravel will inject that service in the constructor, just like it would have done in a Controller

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

Doesn't it hurt Demeter's law when using services/factories in model?

class ForumThread
{
/**
* #return bool
*/
public function findBadLanguage ($inWhat)
{
return (bool)rand(0,1);
}
/**
* #return
*/
public function add ($threadName)
{
if (!$this->findBadLanguage ($threadName))
{
INSERT INTO
}
}
}
class ForumPost
{
/**
* #return
*/
public function post ($toThreadId, $comment)
{
// im talking about this:
Services::getForumThread()->findBadLanguage($comment);
}
}
I know findBadLanguage() should be in another class, but lets suppose thats okay. Lets focus on Services::get****() calls. Is it OK to turn to a global container and get objects from it? Or to turn to a factory? Doesnt it hury Demeter's law? It says we must not use object from the outside

Use extbase service in sr_feuser_register hook

I have a hook that gets successfully called
class tx_srfeuserregister_MyHooksHandler {
public function registrationProcess_afterSaveCreate ($recordArray, &$invokingObj) {
var_dump($recordArray); //i get here
}
}
thanks to being registered in the sr_feuser_register/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['sr_feuser_register']['tx_srfeuserregister_pi1']['registrationProcess'][] = 'EXT:sr_feuser_register/hooks/class.tx_srfeuserregister_MyHooksHandler.php:&tx_srfeuserregister_MyHooksHandler';
To do useful things here, and without copy/pasting code, I'd like to call a service method from an extbase service that is located in another extension
typo3conf/ext/my_extension/Classes/Domain/Service/Tx_MyExtension_Domain_Service_EntityFactory.php
How do I inject that into my hook, or get it through the object factory? I've tried a couple of things and googled a lot, but could not figure it out.
I suggest you get an instance of the extbase object manager, get your service and call your method. Something like this:
/**
* #var Tx_Extbase_Object_ObjectManager
*/
protected $objectManager;
/**
* #var Tx_MyExt_Service_MyService
*/
protected $myService;
public function registrationProcess_afterSaveCreate ($recordArray, &$invokingObj) {
$this->initializeObjects();
// Now you can use your Service.
$this->myService->myMethod($recordArray);
}
/**
* #return void
*/
public function initializeObjects() {
if (empty($this->objectManager)) {
$this->objectManager = t3lib_div::makeInstance('Tx_Extbase_Object_ObjectManager');
}
if (empty($this->myService)) {
$this->myService = $this->objectManager->get('Tx_MyExt_Service_MyService');
}
}

Categories