5

I'm using Doctrine to save user data and I want to have a last modification field. Here is the pseudo-code for how I would like to save the form once the user presses Save:

  • start transaction
  • do a lot of things, possibly querying the database, possibly not
  • if anything will be changed by this transaction
    • modify a last updated field
  • commit transaction

The problematic part is if anything will be changed by this transaction. Can Doctrine give me such information?

How can I tell if entities have changed in the current transaction?

edit

Just to clear things up, I'm trying to modify a field called lastUpdated in an entity called User if any entity (including but not limited to User) will be changed once the currect transaction is commited. In other words, if I start a transaction and modify the field called nbCars of an entity called Garage, I wish to update the lastUpdated field of the User entity even though that entity hasn't been modified.

Shawn
  • 10,931
  • 18
  • 81
  • 126
  • If I understand your question correctly, you want to use the Unit of Work object to check if there are any changes pending. [Unit of Work API](http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.UnitOfWork.html) – datasage Mar 07 '13 at 21:03
  • MySQL has native support for what you would like to do. See http://dev.mysql.com/doc/refman/5.0/en/timestamp-initialization.html – Daniel Williams Mar 07 '13 at 21:18

4 Answers4

6

This is a necessary reply that aims at correcting what @ColinMorelli posted (since flushing within an lifecycle event listener is disallowed - yes, there's one location in the docs that says otherwise, but we'll get rid of that, so please don't do it!).

You can simply listen to onFlush with a listener like following:

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;

class UpdateUserEventSubscriber implements EventSubscriber
{
    protected $user;

    public function __construct(User $user)
    {
        // assuming the user is managed here
        $this->user = $user;
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        $em  = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        // before you ask, `(bool) array()` with empty array is `false`
        if (
            $uow->getScheduledEntityInsertions()
            || $uow->getScheduledEntityUpdates()
            || $uow->getScheduledEntityDeletions()
            || $uow->getScheduledCollectionUpdates()
            || $uow->getScheduledCollectionDeletions()
        ) {
            // update the user here
            $this->user->setLastModifiedDate(new DateTime());
            $uow->recomputeSingleEntityChangeSet(
                $em->getClassMetadata(get_class($this->user)), 
                $this->user
            );
        }
    }

    public function getSubscribedEvents()
    {
        return array(Events::onFlush);
    }
}

This will apply the change to the configured User object only if the UnitOfWork contains changes to be committed to the DB (an unit of work is actually what you could probably define as an application level state transaction).

You can register this subscriber with the ORM at any time by calling

$user         = $entityManager->find('User', 123);
$eventManager = $entityManager->getEventManager();
$subscriber   = new UpdateUserEventSubscriber($user);

$eventManager->addEventSubscriber($subscriber);
Ocramius
  • 25,171
  • 7
  • 103
  • 107
  • The `getScheduled...` functions seem to return all entities which are persisted, not only those for which a value has been changed. Or perhaps they return all entities which have been assigned values even if those values are the same as they were before. In any case, using these `getScheduled...` functions doesn't give me the information I'm looking for. – Shawn Mar 25 '13 at 18:24
  • @Shawn that doesn't work indeed. If your values didn't change, either you have broken encapsulation (See http://stackoverflow.com/questions/10836123/how-to-set-a-date-in-doctrine-2/15519257#15519257 ) or you have to use a tracking strategy explicitly as of http://docs.doctrine-project.org/en/latest/reference/change-tracking-policies.html – Ocramius Mar 25 '13 at 20:24
2

Sorry for giving you the wrong answer at first, this should guide you in the right direction (note that it's not perfect).

You'll need to implement two events. One which listens to the OnFlush event, and acts like this:

// This should listen to OnFlush events
public function updateLastModifiedTime(OnFlushEventArgs $event) {
    $entity = $event->getEntity();
    $entityManager = $event->getEntityManager();
    $unitOfWork = $entityManager->getUnitOfWork();

    if (count($unitOfWork->getScheduledEntityInsertions()) > 0 || count($unitOfWork->getScheduledEntityUpdates()) > 0) {
        // update the user here
        $this->user->setLastModifiedDate(new \DateTime());
    }
}

We need to wait for the OnFlush event, because this is the only opportunity for us to get access to all of the work that is going to be done. Note, I didn't include it above, but there is also $unitOfWork->getScheduledEntityDeletions() as well, if you want to track that.

Next, you need another final event listener which listens to the PostFlush event, and looks like this:

// This should listen to PostFlush events
public function writeLastUserUpdate(PostFlushEventArgs $event) {
    $entityManager = $event->getEntityManager();
    $entityManager->persist($this->user);
    $entityManager->flush($this->user);
}

Once the transaction has been started, it's too late, unfortunately, to get doctrine to save another entity. Because of that, we can make the update to the field of the User object in the OnFlush handler, but we can't actually save it there. (You can probably find a way to do this, but it's not supported by Doctrine and would have to use some protected APIs of the UnitOfWork).

Once the transaction completes, however, you can immediately execute another quick transaction to update the datetime on the user. Yes, this does have the unfortunate side-effect of not executing in a single transaction.

Colin M
  • 13,010
  • 3
  • 38
  • 58
  • This works for a single Entity, I'm trying to see if ANY entity will be changed by a certain transaction... Is that possible to achieve with PreUpdate/PrePersist ? – Shawn Mar 07 '13 at 21:06
  • @Shawn You can bind an event listener using the function I added in my edit. That could, in theory, work for any entity. – Colin M Mar 07 '13 at 21:08
  • @Shawn Just edited it again to look for the `Timestampable` interface on entities. Create an interface `Timestampable` with one method `setLsatModifiedDate` that accepts a `DateTime` and make the models that you want to exhibit this behavior implement that interface – Colin M Mar 07 '13 at 21:15
  • I think I may not have gotten my point accross, or maybe I just don't understand what your code does... I'm trying to modify a field in the `User` entity if ANY entity (including but not limited to `User`) will be changed upon `commit`ing the transaction. Doesn't your method only check on one entity to decide whether it's own `lastModified` field should be updated? – Shawn Mar 07 '13 at 21:18
  • @Shawn Do you have access to the `User` object anywhere in your application? If so, this can still be done easily using the `PreFlush` event handler. – Colin M Mar 07 '13 at 21:19
  • I do have access to the `User` object – Shawn Mar 07 '13 at 21:25
  • @Shawn See my updated answer, it assumes you have access to `$this->user` (the `User` object) inside of the event listener class – Colin M Mar 07 '13 at 21:33
  • To which class should I add these functions (`updateLastModifiedTime` and `writeLastUserUpdate`)? – Shawn Mar 07 '13 at 21:57
  • @Shawn A new class should be created and added as an event subscriber to doctrine. Please refer to [this page](http://docs.doctrine-project.org/en/2.0.x/reference/events.html) for details about how to do that. – Colin M Mar 07 '13 at 21:58
  • I discovered that the `getScheduled...` functions seem to return all entities which are persisted, not only those for which a value has been changed. Or perhaps they return all entities which have been assigned values even if those values are the same as they were before. In any case, using these `getScheduled...` functions doesn't give me the information I'm looking for. – Shawn Mar 25 '13 at 18:24
  • `getScheduledEntityUpdates` will _only_ return entities which are _going to be_ updated (meaning one or more attributes has changed). If it's returning all entities in your application, then: 1) your application is modifying all of these entities somehow, or 2) you have a buggy build of Doctrine. – Colin M Mar 25 '13 at 22:01
1

@PreUpdate event won't be invoked if there's no change on the entity.

Ondřej Mirtes
  • 5,054
  • 25
  • 36
1

My guess would have been, similarly to the other 2 answers, is to say whatever you want to do when there will or will not be changes, use event listeners.

But if you only want to know before the transaction starts, you can use Doctrine_Record::getModified() (link).

marekful
  • 14,986
  • 6
  • 37
  • 59
  • I'm trying to see if any entities will be modified once I `commit` the transaction or if nothing happened during that transaction. Am I correct in thinking that getModified only returns those changes that have already been commited? – Shawn Mar 07 '13 at 21:15
  • No, it's not correct. You definitely can use them during Doctrine events, that is, after save() is invoked but _before_ the actual database transaction. You can terminate for e.g. an actual commit based on what is modified. I think it is even possible to use them outside those events. E.g. if you modify an object instance that is based on a Doctrine model, the change is returned by getModified before save() but I'm not sure about this one. – marekful Mar 07 '13 at 21:21
  • If you want to know whether there was a db. change, use one of the event hooks that are _always_ invoked regardless of db. change. (preSave, preDelete) combined with getModified. If you only want to do something if there was a db. change, use preUpdate or preInsert. – marekful Mar 07 '13 at 21:25
  • Isn't `Doctrine_Record::getModified()` a Doctrine1 feature? Doctrine2's entities do not have a common ancestor with methods like this. – caponica Aug 22 '14 at 20:38