4

Coming from this post and after fix things I'm in another issue/security question/problem.

As yours may see in the other post I'm trying to inject security context in the listener but if I leave the code intact without touch I got this error:

ServiceCircularReferenceException: Circular reference detected for service "doctrine.orm.default_entity_manager"

So, reading and researching I found a solution but is not clear to me if is the right or if it's secure for my application. So this is what I did:

Instead of inject [@security.context] I did this:

services:
    orderhascomment.listener:
        class: PL\OrderBundle\Listener\OrderHasCommentListener
        arguments: [@service_container]
        tags:
            - { name: doctrine.event_listener, event: prePersist, method: onPrePersist }

And my listener OrderHasCommentListener.php is as follow:

namespace PL\OrderBundle\Listener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerInterface;

class OrderHasCommentListener {

    protected $container;

    public function __construct(ContainerInterface $container = null) {
        $this->container = $container;
    }

    /**
     *
     * @param LifecycleEventArgs $args 
     */
    public function onPrePersist(LifecycleEventArgs $args) {

        $entity = $args->getEntity();
        $user = $this->container->get('security.context')->getToken()->getUser();
        $entity->setUser($user);
    }

}

Is that the right way to do this? Or exists another one? I read it's a bad idea to inject the whole container since I need just the security context, what's the solution then? (https://insight.sensiolabs.com/what-we-analyse)

Trying to convert UserCallable in a service

I'm trying to convert UserCallable in a service by following instructions here and taking a look at DoctrineBehaviors orm-services.yml file and also seeing how them do it at BlameableListener but I can not get it to work since I get this error:

ContextErrorException: Catchable Fatal Error: Argument 1 passed to PL\OrderBundle\Listener\OrderHasCommentListener::__construct() must be callable, string given

This is how my definition looks like at app/config/config.yml:

services:
    orderhascomment.listener:
        class: PL\OrderBundle\Listener\OrderHasCommentListener
        arguments: 
            - user_callable
        tags:
            - { name: doctrine.event_listener, event: prePersist, method: onPrePersist }
    user_callable:
        class: PL\OrderBundle\Util\UserCallable
        arguments:
            - "@service_container"
        public:  false

And this is how I passing to __construct() function in OrderHasCommentListener.php file:

/**
 * @param UserCallableInterface $user_callable 
 * */
public function __construct(callable $user_callable = null) {
    $this->userCallable = $user_callable;
}

What is wrong?

Community
  • 1
  • 1
ReynierPM
  • 17,594
  • 53
  • 193
  • 363
  • See the "security.token_storage" solution here: http://stackoverflow.com/questions/7561013/injecting-securitycontext-into-a-listener-prepersist-or-preupdate-in-symfony2-to#26011863 – webDEVILopers Feb 18 '15 at 09:40

1 Answers1

9

Injecting the whole container directly into the lister may be a working solution ... but we can do better :)

Inject a UserCallable that returns the current user instead.

This way you express the real purpose of the depedency more clearly without introducing a hard dependency between your listener and the container(-interface). An example would be ...

Knp\DoctrineBehaviors\ORM\Blameable\UserCallable

This particular example can be improved further by creating an interface and using that for type-hinting in your listener instead. That allows easier exchangeability if you plan to re-use the listener.

The interfaces:

namespace Acme\Common;

interface UserCallableInterface
{
    /**
     * @return \Symfony\Component\Security\Core\User\UserInterface
     */
    public function getCurrentUser();
}

namespace Acme\Common;

use Symfony\Component\Security\Core\User\UserInterface;

interface TrackableInterface
{
    /**
     * @param UserInterface $user
     */
    public function setUser(UserInterface $user);
}

The UserCallable:

namespace Acme\Util;

use Acme\Common\UserCallableInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class UserCallable implements UserCallableInterface
{
   /** @var ContainerInterface **/
   protected $container;

   /** 
    * @param ContainerInterface $container
    */
   public function __construct(ContainerInterface $container)
   {
      $this->container = $container;
   }

   /**
    * @{inheritdoc}
    */
   public function getCurrentUser()
   {
      return $this->container->get('security.context')->getToken()->getUser() ?: false;
   }

The listener:

use Acme\Common\UserCallableInterface;
use Acme\Common\TrackableInterface;
use Doctrine\Common\EventArgs;

class Listener
{
    /** @var UserCallableInterface **/
    protected $userCallable;

    /** 
     * @param UserCallableInterface $user_callable 
     **/    
    public function __construct(UserCallableInterface $user_callable)
    {
       $this->userCallable = $user_callable;
    }

    /** 
     * @param EventArgs $args 
     **/
    public function onPrePersist(EventArgs $args)
    {
       $entity = $args->getEntity();

       if ( !($entity instanceof TrackableInterface) ) {
           return;
       }

       if ( !($user = $this->userCallable->getCurrentUser())) {
           return;
       }

       $entity->setUser($user);
    }      
}
Lewis
  • 3,375
  • 3
  • 29
  • 26
Nicolai Fröhlich
  • 51,330
  • 11
  • 126
  • 130
  • Hmm I never did this before so I'm totally lost about this perhaps a piece of code using Listener or Entity will help better to me (I'm newbie) so hope you can help me – ReynierPM Mar 02 '14 at 15:06
  • What is left being unclear to you? I'll be happy to improve my answer. – Nicolai Fröhlich Mar 02 '14 at 17:04
  • Well I tried your code and get this error `ContextErrorException: Catchable Fatal Error: Argument 1 passed to PL\OrderBundle\Listener\OrderHasCommentListener::__construct() must be an instance of PL\OrderBundle\UserCallableInterface, instance of appDevDebugProjectContainer given` – ReynierPM Mar 02 '14 at 17:19
  • Okay - you'll need to turn the usercallable into a service and inject this service instead of the container. That's the essential thing this approach is about. I'll add some instructions and rework the answer - hang on :) – Nicolai Fröhlich Mar 02 '14 at 17:57
  • Nice, I was reading [Doctrine Behaviors](https://github.com/KnpLabs/DoctrineBehaviors/blob/master/src/Knp/DoctrineBehaviors/ORM/Blameable/UserCallable.php) but can't get how it works, I'll be on to see your suggestion – ReynierPM Mar 02 '14 at 17:59
  • I made a edit since I'm trying to convert `UserCallable` into a service as you suggest but I can get it to work – ReynierPM Mar 03 '14 at 13:26
  • @nifr you should inject to `UserCallable` only `security.context` service instead whole container. – NHG Apr 04 '14 at 11:42
  • @nifr I tried to go the UserCallable route but I am still getting the ServiceCircularReferenceException. I have opened up a [follow up question](http://stackoverflow.com/q/24800289/457268). – k0pernikus Jul 17 '14 at 09:56
  • I've actually implemented this pattern (without knowing this SO issue), for one of our listeners: https://github.com/hostnet/entity-blamable-component . The problem is, that I don't want to inject the container just because I need to ask the SecurityContext what the current user name/id is. I will work on a more elegant solution for that part – Anyone Aug 15 '14 at 06:42
  • 1
    BTW, use `$this->container->get('security.authorization_checker')` instead (new in 2.6) – Ronan Aug 30 '15 at 20:31