4
votes

I have successfully set up an Entity Event Listener in Symfony 3.4. I registered the service like this, in the Resources/config/services.yml of a separate bundle:

services:
    resource.listener:
        class: Company\ResourceManagementBundle\EventListener\Entity\ResourceUpdateListener
            arguments: ["@security.token_storage"]
            tags:
            - { name: doctrine.event_listener, event: preUpdate, method: preUpdate }
            - { name: doctrine.event_listener, event: postUpdate, method: postUpdate }

I also added the necessary code in the Entity:

/**
 * @ORM\EntityListeners(
 *    {"Company\ResourceManagementBundle\EventListener\Entity\ResourceUpdateListener"}
 * )
 */
class Resource implements UserInterface
{

Then in my Event Listener, I created a constructor with Token Storage as the only parameter, type-hinted with the TokenStorageInterface. Here is my event Listener:

namespace Company\ResourceManagementBundle\EventListener\Entity;

use Company\ResourceManagementBundle\Service\UserNoteLogger;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;

class ResourceUpdateListener
{
    protected $fields;
    private $token_storage;

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

    public function preUpdate(Resource $resource, PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();
        if ($entity instanceof Resource) {
            $changes = $args->getEntityChangeSet();
        }
        return $this->setFields($entity, $args);
    }
    public function postUpdate(Resource $resource, LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $hasChanged = false;
        if ($entity instanceof Resource) {
            foreach ($this->fields as $field => $detail) {
                if($detail[0] == null) {
                    //continue;
                } else {
                    $hasChanged = true;
                }
            }
            if ($hasChanged == true) {

                $userNoteLog = new UserNoteLogger($args->getEntityManager(), $this->token_storage);
                $comment = "The resource, " . $resource->getFullName() . ", was changed by the user, " . $this->token_storage->getToken()->getUser()->getFullName();
                $userNoteLog->logNote($comment, $resource);
            }
        }
    }
    public function setFields($entity, LifecycleEventArgs $eventArgs)
    {
        $this->fields = array_diff_key(
            $eventArgs->getEntityChangeSet(),
            [ 'modified'=>0 ]
        );
        return true;
    }
}

This is the error I receive:

Type error: Argument 1 passed to Company\ResourceManagementBundle\EventListener\Entity\ResourceUpdateListener::__construct() must implement interface Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface, none given, called in /var/www/sites/sentient02/vendor/doctrine/doctrine-bundle/Mapping/ContainerAwareEntityListenerResolver.php on line 83

This error does not go away, as long as the Token Storage parameter exists in the constructor.

If you look at the EventListener code above, I am trying to log information every time the Entity changes during the update, and this information needs to know the name of the logged in user.

2

2 Answers

2
votes

The service definition seems OK, but by default, the EntityListener annotation only support empty constructor.

See in the Doctrine documentation :

An Entity Listener could be any class, by default it should be a class with a no-arg constructor.

A little clarification here :

  • a Doctrine Event Listener is called for all entities (generally require test like $entity instanceof MyClass before doing any action)
  • a Doctrine Entity Listener are used for easy case, and are only called on one entity.

In your code, it seems that you write a common doctrine listener, but use it as an entity listener.

Furthermore, you already declare your service like a common doctrine event with the doctrine.event_listener tag (EntityListener must have the doctrine.orm.entity_listener tag.)

In short, if you just delete the @ORM\EntityListeners annotation, it should be ok.

Note that to get changes when update an entity, you can use the onFlush event. Here is an example with the very useful unitOfWork in order to get an array with all fields that will change in the scheduled updates.

  /**
   * @param OnFlushEventArgs $args
   */
  public function onFlush(OnFlushEventArgs $args)
  {
      $entityManager = $args->getEntityManager();
      $unitOfWork = $entityManager->getUnitOfWork();

      // Get all updates scheduled entities in the unit of work.
      foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {

          if ($entity instanceof Resource) {

              dump( $unitOfWork->getEntityChangeSet($entity) );
              // In this array, you'll have only fields and values that will be update.
              // TODO : your stuff.
          }
      }
  }
0
votes

Try putting autowire: true in your services.yml:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
        public: false       # Allows optimizing the container by removing unused services; this also means
                            # fetching services directly from the container via $container->get() won't work.
                            # The best practice is to be explicit about your dependencies anyway.

Take a look at the docs https://symfony.com/doc/current/service_container/3.3-di-changes.html.