PostPersist & PostFlush infinite loop in Symfony - symfony

I'm trying to link a coffee profile to a user after the user is in the database.
This coffee profile data is in a session and I'm using this session in the postFlush.
However This code is creating an infinite loop and I don't know why:
UserListener.php:
<?php
namespace AppBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use AppBundle\Entity\Consumption;
use AppBundle\Entity\CoffeeOption;
use AppBundle\Entity\Consumeable;
use AppBundle\Entity\MomentPreference;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\Event\PostFlushEventArgs;
class UserListener
{
private $container;
private $user;
public function __construct(ContainerInterface $container = null)
{
$this->container = $container;
}
public function postPersist(LifecycleEventArgs $args)
{
$user = $args->getEntity();
$this->user = $user;
}
public function postFlush(PostFlushEventArgs $args)
{
$session = new Session();
if($session) {
$em = $args->getEntityManager();
$us = $em->getRepository('AppBundle:User')->findOneById($this->user->getId());
$consumption = $session->get('consumption');
$coffee = $session->get('coffee');
$moment = $session->get('moment');
$consumption->setUser($us);
//dummy data for the day, later this needs to be turned into datetime
$moment->setDay('monday');
$moment->setConsumption($consumption);
$em->persist($consumption);
$em->persist($coffee);
$em->persist($moment);
$em->flush();
} else {
return $this->redirectToRoute('fos_user_registration_register');
}
}
}
Services.yml:
zpadmin.listener.user:
class: AppBundle\EventListener\UserListener
arguments: ['#service_container']
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postFlush }
What is causing this loop and how can I fix it?

In your postFlush event, you're flushing again. That's what causing the infinite loop because the postFlush event is triggered every time you're calling the flush method.
I'm not sure what you're trying to achieve but your goal is to create a coffee consumption everytime you're saving a user, you can add a test like this one at the start of your method:
$entity = $args->getObject();
if (!$entity instanceof User) {
return;
}
This will prevent the infinite loop.
And a few more things:
Your postPersist method seems useless. It'll be called every time an object is persisted so your $this->user propety will not necessarily be a User object.
If you need the user, you don't have to fetch it in your database. Simply use $args->getObject() to get the flushed entity. In addition with the test above, you'll be sure the method will return you a User object.
This is not a very good practice to check if the user is logged in in your Doctrine listener. This is not what the class is supposed to do.
Don't inject the container in your constructor. Inject only what you need (in this case... nothingĀ ?)

You are calling $em->flush() inside your postPersist event, in the docs it is stated that:
postFlush is called at the end of EntityManager#flush().
EntityManager#flush() can NOT be called safely inside its listeners.
You should use other events like prePersist or postPersist.
If possible try to avoid multiple flush() on a single request.
by the way, there is no need to do this, because your user object is already contained inside your $user variable.
$us =
$em->getRepository('AppBundle:User')->findOneById($this->user->getId());

Related

Symfony 4 Doctrine EventSubscriber not used

Trying to register a Doctrine EventSubscriber but nothing is ever actually fired.
I have, on the Entity, in question, set the #ORM\HasLifeCycleCallbacks annotation.
Here's the Subscriber:
<?php
namespace App\Subscriber;
use App\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserPasswordChangedSubscriber implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents()
{
return [Events::prePersist, Events::preUpdate, Events::postLoad];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
private function updateUserPassword(User $user)
{
$plainPassword = $user->getPlainPassword();
if (!empty($plainPassword)) {
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$user->eraseCredentials();
}
}
}
The part that is making this particuarly frustrating is that this same code and configuration was fine in Symfony 3 whe autowiring was turned off and I manually coded all my services.
However, now, even if I manually code up a service entry for this, in the usual way, still nothing happens.
EDIT:
Here is my services.yaml after trying what suggested Domagoj from the Symfony docs:
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
It didn't work. Interestingly, If I un-implement the EventSubscriber interface, Symfony throws an exception (rightly). Yet my break points in the code are completely ignored.
I've considered an EntityListener, but it cannot have a constructor with arguments, doesn't have access to the Container and I shouldn't have to; this ought to work :/
I ended up figuring this out. The field that I was specifically updating was transient, and therefore Doctrine didn't consider this an Entity change (rightly).
To fix this, I put
// Set the updatedAt time to trigger the PreUpdate event
$this->updatedAt = new DateTimeImmutable();
In the Entity field's set method and this forced an update.
I also did need to manually register the Subscriber in the services.yaml using the following code. symfony 4 autowiring wasn't auto enough for a Doctrine Event Subscriber.
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
For your first problem, doctrine event subscribers are not autoconfigured/auto-tagged. For the reasons and solutions, you have some responses here.
Personnaly, I just have one Doctrine ORM mapper, so I put this in my services.yaml file :
services:
_instanceof:
Doctrine\Common\EventSubscriber:
tags: ['doctrine.event_subscriber']
You have to register your Event Listener as a service and tag it as doctrine.event_listener
https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#configuring-the-listener-subscriber

Run a php script after insert

I am working on an application using Symfony 3 and Twig templates. I have created forms using symfony formBuilder. I need to run a php script every time a row is inserted in database. Is there anyway that I can do this ?
yes of course, you can use the Events and Event Listeners https://symfony.com/doc/current/event_dispatcher.html or Doctrine Event Listeners and Subscribers https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
First, to run a script, you can use the Process component of Symfony.
Here is an example of usage:
$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
$process = new Process("{$phpBinaryPath} worker.php");
$process->run();
You should read the related doc for more insights.
Then you want to hook after the flush of doctrine, then use an event listener. It's a class with a specific method that you register as a service.
You need to define a class:
namespace App\EventListener;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
class YourListener
{
private $persisted = [];
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof YourRecord) {
return;
}
$this->persisted[] = $entity;
}
public function postFlush(PostFlushEventArgs $args)
{
foreach ($persisted as $row) {
// Execute your action for the given row
}
}
}
Then you need to register it as service:
# services.yaml
services:
App\EventListener\YourListener:
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postFlush }
Check the related documentation: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html

Symfony2/Doctrine - postFlush

If you saw my previous question, this is kind of linked to it but a new question. So I have an Entity and I have a listener linked up to this. In my createAction I create my Object and then persist-flush it to my database. In my listener, I have set up a postFlush function
public function postFlush(PostFlushEventArgs $args)
{
$em = $args->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof AvailabilityAlert) {
var_dump("TEST");
$this->api_service->addFlightsAction($entity);
}
}
}
What I am trying to do in this function is get the entity that was just flushed. I have tried all the different actions of getUnitsOfWork e.g. getScheduledEntityDeletions but for none of them I can get into where the var_dump occurs.
How would I go about getting the flushed entity within this postFlush function?
According to Doctrine2 documentation here : http://doctrine-orm.readthedocs.org/en/latest/reference/events.html you can't call the flushed entities on PostFlush event.
However you can split your logic : use the OnFlush event to get these entities and then pass it to the PostFlush if the flush succeeded.
To get just persisted to database entity you need not postFlush but postPersist:
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
}
And don't forget to add next tag to your service:
{ name: "doctrine.event_listener", event: "postPersist" }

How to a dynamically add roles to a user?

When a user logs in, whether it be for the first time or via a cookie that's present, I want to assign them one or more additional user roles based on the result of some queries I run.
I think I need to create an Event Listener that gets the security component and entity manager injected, so I can run my queries and add roles to the current user.
I'm not quite sure if this is possible with events though, since the event would need to be fired within the Firewall context before the authorization is done, but after authentication.
I did see this existing question, but I can't get it to work (the roles are not actually recognized after the event is run).
Is it possible to do this with an event listener?
I think my alternative would be to use a postload lifecycle callback on the User entity and run some queries there, but that doesn't seem right.
You could create an event listener on the kernel. It will run everytime a page is loaded.
It will check if their is a logged in user and then you can do some custom logic to see if you need to update their role, if you do then update it log them in with the new settings and then they'll continue in the system with their new role.
I haven't tested this code, so it might have some bugs.
services.yml
bundle.eventlistener.roles:
class: Sample\MyBundle\EventListener\Roles
arguments: [#service_container, #security.context]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Listener\Roles.php
namespace Sample\MyBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Sample\MyBundle\Entity\Users; //Your USER entity
class Roles {
private $container;
private $context;
public function __construct(Container $container, SecurityContext $context) {
$this->container = $container;
$this->context = $context;
}
public function onKernelController(FilterControllerEvent $event) {
if($this->context->getToken()->getUser() instanceof Users) {
//Custom logic to see if you need to update the role or not.
$user = $this->context->getToken()->getUser();
//Update your roles
$user->setRole('ROLE_WHATEVER');
$em = $this->container->get('doctrine')->getManager();
$em->persist($user);
$em->flush();
//Create new user token
//main == firewall setting
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main', $user->getRoles());
$this->context->setToken($token);
}
}
}

Listener on Bundle call

My point is to call a generic function in all my controllers in some Bundle (let's say AdminBundle). I got a login listener in whitch I set a session that contain true or false and I need to check this session before every method of my AdminBundle.
So I tried to make a __construct() function in my AdminBundle controllers, but it appears that I can't access to a service from this method (because the container is not yet loaded so I can't access $this).
The best practice should be to use a listener to call this service before very method of those controllers but I can't figure out whitch listener I need to use (cannot find a clue on google...).
Hope in clear enough, don't hesitate to ask questions if you don't understand my point !
Thanks in advance for any solution/idea (if you think that I'm not using the correct way to do it please explain my your point of view !)
After the afternoon on this issue i finally get a solution thanks to mahok.
For those whitch have the same issue here's my controller listener :
<?php
namespace Site\MyBundle\Listener;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ControllerListener
{
protected $container;
protected $router;
public function __construct(ContainerInterface $container, Router $router)
{
$this->router = $router;
$this->container = $container;
}
public function onKernelController(FilterControllerEvent $event)
{
if (HttpKernel::MASTER_REQUEST == $event->getRequestType())
{
$controller = $event->getController();
$controller = $controller[0];
$new = new \ReflectionObject($controller);
if($new->getNamespaceName() == 'Site\MyBundle\Controller')
{
$test = $this->container->get('myservice')->test();
if(empty($test) || !$test)
{
$index = $this->router->generate('index');
$event->setController(function() use($index) {
return new RedirectResponse($index);
});
}
}
}
}
}
So basically it compare the namespace of your current controller's action with another and if true i check some variable to know if the user can be here or not.
Thanks again mahok you show me the way !

Resources