44

I setup a listener class where i'll set the ownerid column on any doctrine prePersist. My services.yml file looks like this ...

services:
my.listener:
    class: App\SharedBundle\Listener\EntityListener
    arguments: ["@security.context"]
    tags:
        - { name: doctrine.event_listener, event: prePersist }

and my class looks like this ...

use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\SecurityContextInterface;

class EntityListener
{

protected $securityContext;

public function __construct(SecurityContextInterface $securityContext)
{
    $this->securityContext = $securityContext;
}


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

    $entity = $args->getEntity();
    $entityManager = $args->getEntityManager();

    $entity->setCreatedby();

}
}

The result of this is the following error.

ServiceCircularReferenceException: Circular reference detected for service "doctrine.orm.default_entity_manager", path: "doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection -> my.listener -> security.context -> security.authentication.manager -> fos_user.user_manager".

My assumption is that the security context has already been injected somewhere in the chain but I don't know how to access it. Any ideas?

Jon Winstanley
  • 23,010
  • 22
  • 73
  • 116
Jeremy
  • 1,908
  • 3
  • 25
  • 38

5 Answers5

68

I had similar problems and the only workaround was to pass the whole container in the constructor (arguments: ['@service_container']).

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

class MyListener
{
    protected $container;

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

    // ...

    public function prePersist(LifeCycleEventArgs $args)
    {
        $securityContext = $this->container->get('security.context');

        // ...
    }
}
kgilden
  • 10,336
  • 3
  • 50
  • 48
  • Good call, I ended up trying all sorts of things the day I asked the question and finally wound up getting it working after passing in the Container. In hindsight the error makes a little more sense. – Jeremy Sep 29 '11 at 22:05
  • 1
    @Jeremy: I never really figured out why should one get a circular reference exception by passing individual services. Why would it make sense? – kgilden Sep 29 '11 at 23:51
  • 9
    Strangely enough, even setting a property using `$container->get('security.context')` in the constructor throws a circular reference error. But, calling it within a member method works just fine... – Logan Bibby Mar 23 '12 at 05:28
  • 9
    It feels to me like a ``chmod 777 dir/name``, solution. As in, it fails the whole concepts. See [Kris Walsmith's answer](http://stackoverflow.com/questions/8708822/circular-reference-when-injecting-security-context-into-entity-listener-class) – renoirb May 17 '12 at 14:42
  • 3
    @LoganBibby, it should be obvious, you're trying to instantiate a class which needs another object for which the instance is needed, you end up in an infinite loop which can never stop. When passing the `Container` you're lazy-loading your service which means that between the moment when you want to your service it has been instantiated. It is a little bit like "I need money to buy a car, but I need a car to make money." – Trent Sep 27 '12 at 08:50
  • @Trent, That's true. I realize that now. It just irked me at the time. :) – Logan Bibby Sep 27 '12 at 16:38
  • @LoganBibby, I bet it, one year later ;) – Trent Sep 27 '12 at 16:45
  • 1
    @gilden Because Jeremy's custom Listener is called during initialization of Doctrine's EntityManager (as a Doctrine Listener it's like a requirement for the EntityManager to boot up). The custom Listener uses the Security Context which is extended by the FOSUserBundle to use the Database and thus needs the EntityManager (which we were trying to initialize in the first place). And there's your circular reference. – flu Oct 25 '13 at 09:32
  • You are passing service_container as an array, but type hinting as `ContainerInterface`. – Roman Newaza Feb 11 '14 at 10:44
  • 2
    **As of Symfony 2.6 there's a better solution. Please check my answer.** – Anyone Oct 06 '14 at 09:13
  • 3
    dirty solution... It's like buying the whole supermarket to just make a cake – Freelancer Mar 24 '15 at 14:36
36

As of Symfony 2.6 this issue should be fixed. A pull request has just been accepted into the master. Your problem is described in here. https://github.com/symfony/symfony/pull/11690

As of Symfony 2.6, you can inject the security.token_storage into your listener. This service will contain the token as used by the SecurityContext in <=2.5. In 3.0 this service will replace the SecurityContext::getToken() altogether. You can see a basic change list here: http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements#deprecated-the-security-context-service

Example usage in 2.6:

Your configuration:

services:
    my.entityListener:
        class: App\SharedBundle\Listener\EntityListener
        arguments:
            - "@security.token_storage"
        tags:
            - { name: doctrine.event_listener, event: prePersist }


Your Listener

namespace App\SharedBundle\Listener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class EntityListener
{
    private $token_storage;

    public function __construct(TokenStorageInterface $token_storage)
    {
        $this->token_storage = $token_storage;
    }

    public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entity->setCreatedBy($this->token_storage->getToken()->getUsername());
    }
}


For a nice created_by example, you can use https://github.com/hostnet/entity-blamable-component/blob/master/src/Listener/BlamableListener.php for inspiration. It uses the hostnet/entity-tracker-component which provides a special event that is fired when an entity is changed during your request. There's also a bundle to configure this in Symfony2

ctatro85
  • 311
  • 3
  • 11
Anyone
  • 2,814
  • 1
  • 22
  • 27
  • Nice, I'm glad there's no need to inject the entire container anymore. – kgilden Oct 06 '14 at 11:21
  • 2
    What about a service that is not a security token ? – Lighthart Apr 14 '15 at 21:22
  • What do you mean Lighthart? The security token in symfony is not a service, there's a services that gives you access to it: security.token_storage. – Anyone Apr 15 '15 at 09:14
  • @Lighthart I've asked the question here since it came up during development for me as well. How would you inject any service dependend on the entity manager into a listener. Check out my question here if you have any ideas: http://stackoverflow.com/q/34227467/1847340 – ferdynator Dec 11 '15 at 15:51
  • On 2.3 you can still inject the service_container service. However, I highly recommend to put some effort in upgrading. – Anyone Mar 17 '16 at 19:00
  • camelCase $tokenStorage, not $token_storage; – Ivan Proskuryakov Jun 12 '16 at 17:38
  • If you want to call it like that, go ahead. There's no rules stating which codestyle you have to follow. – Anyone Jun 13 '16 at 11:52
1

There's a great answer already in this thread but everything changes. Now there're entity listeners classes in Doctrine: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners-class

So you can add an annotation to your entity like:

/**
 * @ORM\EntityListeners({"App\Entity\Listener\PhotoListener"})
 * @ORM\Entity(repositoryClass="App\Repository\PhotoRepository")
 */
class Photo 
{
    // Entity code here...
}

And create a class like this:

class PhotoListener
{        
    private $container;

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

    /** @ORM\PreRemove() */
    public function preRemoveHandler(Photo $photo, LifecycleEventArgs $event): void
    {
         // Some code here...
    }
}

Also you should define this listener in services.yml like that:

photo_listener:
  class: App\Entity\Listener\PhotoListener
  public: false
  autowire: true
  tags:
    - {name: doctrine.orm.entity_listener}
Nikita Leshchev
  • 1,784
  • 2
  • 14
  • 26
0

I use the doctrine config files to set preUpdate or prePersist methods:

Project\MainBundle\Entity\YourEntity:
    type: entity
    table: yourentities
    repositoryClass: Project\MainBundle\Repository\YourEntitytRepository
    fields:
        id:
            type: integer
            id: true
            generator:
                strategy: AUTO

    lifecycleCallbacks:
        prePersist: [methodNameHere]
        preUpdate: [anotherMethodHere]

And the methods are declared in the entity, this way you don't need a listener and if you need a more general method you can make a BaseEntity to keep that method and extend the other entites from that. Hope it helps!

Dani Sancas
  • 1,365
  • 11
  • 27
Calin Bolea
  • 87
  • 1
  • 3
0

Symfony 6.2.4

Add this in your Entity :

#[ORM\EntityListeners(["App\Doctrine\MyListener"])]

Add this in your services.yaml:

App\Doctrine\MyListener:
    tags: [doctrine.orm.entity_listener]

Then you can do this :

<?php

namespace App\Doctrine;

use App\Entity\MyEntity;
use Symfony\Component\Security\Core\Security;

class MyListener
{
    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    public function prePersist(MyEntity $myEntity)
    {
        //Your stuff   
    }
}

Hope it helps.

Za Rech
  • 96
  • 5