Symfony2/Doctrine: Get the field(s) that changed after "Loggable" entity changed

6.5k views Asked by At

In a Symfony2 project I'm using the Loggable Doctrine Extension.

I saw that there is a LoggableListener.

Is there indeed an event that gets fired when a (loggable) field in a loggable entity changes? If it is so, is there a way to get the list of fields that triggered it?

I'm imagining the case of an entity with, let's say 10 fields of which 3 loggable. For each of the 3 I want to perform some actions if they change value, so 3 actions will be performed if the 3 of them change.

Any idea?

Thank you!

EDIT

After reading the comment below and reading the docs on doctrine's events I understood have 3 options:

1) using lifecycle callbacks directly at the entity level even with arguments if I'm using doctrine >2.4

2) I can listen and subscribe to Lifecycle Events, but in this case the docs say that "Lifecycle events are triggered for all entities. It is the responsibility of the listeners and subscribers to check if the entity is of a type it wants to handle."

3) doing what you suggest, which is using an Entity listener, where you can define at the entity level which is the listener that is going to be "attached" to the class.

Even if the first solution seems easier, I read that "You could also use this listener to implement validation of all the fields that have changed. This is more efficient than using a lifecycle callback when there are expensive validations to call". What's considered an "expensive validation?".

In my case what I have to perform is something like "if field X of entity Y changed than add a notification on the notification table saying "user Z changed the value of X(Y) from A to B"

Which would be the most suitable approach, considering that I have around 1000 fields like those?

EDIT2

To solve my problem I'm trying to inject the service_container service inside the listener, so that I can have access to the dispatcher to dispatch a new event which can perform the persist of new entity I need. But how can I do that?

I tried the usual way, I add the following to the service.yml

app_bundle.project_tolereances_listener:
    class: AppBundle\EventListener\ProjectTolerancesListener
    arguments: [@service_container] 

and of course I added the following to the listener:

protected $container;

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

but I get the following:

Catchable Fatal Error: Argument 1 passed to AppBundle\ProjectEntityListener\ProjectTolerancesListener::__construct() must be an instance of AppBundle\ProjectEntityListener\ContainerInterface, none given, called in D:\provarepos\user\vendor\doctrine\orm\lib\Doctrine\ORM\Mapping\DefaultEntityListenerResolver.php on line 73 and defined

Any idea?

1

There are 1 answers

2
Jean D. On

The Loggable listener only saves the changesvalue for the watched properties of your entities over time.

It does not fire an event, it listens to the onFlush and postPersist doctrine events.

I think you are looking for Doctrine listeners on preUpdate and prePersist events where you can manipulate the changeset before a flush.

see: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html

If you are using Doctrine 2.4+ you can add them easily to your entity:

Simple entity class:

namespace Your\Namespace\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 *  @ORM\Entity
 *  @ORM\EntityListeners({"Your\Namespace\Listener\DogListener"})
 */
class Dog
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     */
    private $age;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param int $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * @return int
     */
    public function getAge()
    {
        return $this->age;
    }

    /**
     * @param int $age
     */
    public function setAge($age)
    {
        $this->age = $age;
    }
}

Then in Your\Namespace\Listener you create the ListenerClass DogListener:

namespace Your\Namespace\Listener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Your\Namespace\Entity\Dog;

class DogListener
{
    public function preUpdate(Dog $dog, PreUpdateEventArgs $event)
    {         
        if ($event->hasChangedField('name')) {                
            $updatedName = $event->getNewValue('name'). ' the dog';
            $dog->setName($updatedName);         
        }

        if ($event->hasChangedField('age')) {
            $updatedAge = $event->getNewValue('age') % 2;
            $dog->setAge($updatedAge);
        }

    }

    public function prePersist(Dog $dog, LifecycleEventArgs $event)
    {
        //
    }
}

Clear the cache and the listener should be called when flushing.

Update

You are right about recomputeSingleEntityChangeSet which was not needed in this case. I updated the code of the listener.

The problem with the first choice (in-entity methods) is that you can't inject other services in the method. If you only need the EntityManager then yes, it is the easiest way code-wise.

With an external Listener class, you can do so.

If those 1000 fields are in several separate entities, the second type of Listener would be the most suited. You could create a NotifyOnXUpdateListener that would contain all your watch/notification logic.


Update 2

To inject services in an EntityListener declare the Listener as a service tagged with doctrine.orm.entity_listener and inject what you need.

<service id="app.entity_listener.your_service" class="Your\Namespace\Listener\SomeEntityListener">
        <argument type="service" id="logger" />
        <argument type="service" id="event_dispatcher" />
        <tag name="doctrine.orm.entity_listener" />
</service>

and the listener will look like:

class SomeEntityListener
{
    private $logger;
    private $dispatcher;

    public function __construct(LoggerInterface $logger, EventDispatcherInterface $dispatcher)
    {
        $this->logger = $logger;
        $this->dispatcher = $dispatcher;
    }

    public function preUpdate(Block $block, PreUpdateEventArgs $event)
    {
        //
    }
}

According to: How to use Doctrine Entity Listener with Symfony 2.4? it requires DoctrineBundle 1.3+