Gedmo\Loggable logs data that doesn't have changed
Asked Answered
G

3

9

I'm using Symfony2.2 with StofDoctrineExtensionsBundle (and so Gedmo DoctrineExtensions). I've a simple entity

/**
 * @ORM\Entity
 * @Gedmo\Loggable
 * @ORM\Table(name="person")
 */
class Person {
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

[...]

    /**
     * @ORM\Column(type="datetime", nullable=true)
     * @Assert\NotBlank()
     * @Assert\Date()
     * @Gedmo\Versioned
     */
    protected $birthdate;
}

When changing an attribute for an existing object, a log entry is done in table ext_log_entries. An entry in this log table contains only changed columns. I can read the log by:

$em = $this->getManager();
$repo = $em->getRepository('Gedmo\Loggable\Entity\LogEntry');
$person_repo = $em->getRepository('Acme\MainBundle\Entity\Person');

$person = $person_repo->find(1);
$log = $repo->findBy(array('objectId' => $person->getId()));
foreach ($log as $log_entry) { var_dump($log_entry->getData()); }

But what I don't understand is, why the field birthdate is always contained in a log entry, even it's not changed. Here some examples of three log entries:

array(9) {
  ["salutation"]=>
  string(4) "Herr"
  ["firstname"]=>
  string(3) "Max"
  ["lastname"]=>
  string(6) "Muster"
  ["street"]=>
  string(14) "Musterstraße 1"
  ["zipcode"]=>
  string(5) "00000"
  ["city"]=>
  string(12) "Musterhausen"
  ["birthdate"]=>
  object(DateTime)#655 (3) {
    ["date"]=>
    string(19) "1893-01-01 00:00:00"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(13) "Europe/Berlin"
  }
  ["email"]=>
  string(17) "[email protected]"
  ["phone"]=>
  NULL
}

array(2) {
  ["birthdate"]=>
  object(DateTime)#659 (3) {
    ["date"]=>
    string(19) "1893-01-01 00:00:00"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(13) "Europe/Berlin"
  }
  ["phone"]=>
  string(9) "123456789"
}

array(2) {
  ["birthdate"]=>
  object(DateTime)#662 (3) {
    ["date"]=>
    string(19) "1893-01-01 00:00:00"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(13) "Europe/Berlin"
  }
  ["phone"]=>
  NULL
}

I want to log only really changed data. Is there any option I've not seen yet? It seems to be related to the fact, that birthdate is a DateTime object, doesn't it?

EDIT It is not related to the DateTime object. This occurs even in other entities. I've another entity containing a simple value:

/**
 * @ORM\Entity
 * @Gedmo\Loggable
 * @ORM\Entity(repositoryClass="Acme\MainBundle\Repository\ApplicationRepository")
 * @ORM\Table(name="application")
 */
class Application {

[...]

    /**
     * @ORM\Column(type="integer")
     * @Assert\NotBlank(groups={"FormStepOne", "UserEditApplication"})
     * @Gedmo\Versioned
     */
    protected $insurance_number;
}

When opening the edit form in browser an saving without modification, the log table contains:

update  2013-04-26 11:32:42     Acme\MainBundle\Entity\Application  a:1:{s:16:"insurance_number";s:7:"1234567";}
update  2013-04-26 11:33:17     Acme\MainBundle\Entity\Application  a:1:{s:16:"insurance_number";s:7:"1234567";}

Why?

Gabbro answered 26/4, 2013 at 9:28 Comment(0)
E
9

This might be a similar issue to the one I encountered when using another of these extensions (timestampable), namely: that the default change tracking policy used in doctrine (it tries to auto detect changes) sometimes marks entities as dirty, when they are not (for me this was happening when my entity contained a datetime object, which is understandable given that this is an object which needs to be constructed when pulling it from the database). This isn't a bug or anything - it's expected behaviour and there are a few ways around it.

Might be worth trying to implement an alternative change tracking policy on the entities you want to log and seeing if that fixes things - I would guess that this behaviour (logging) doesn't kick in unless the entity state is dirty, which you can avoid by implementing change tracking yourself manually:

http://docs.doctrine-project.org/en/latest/cookbook/implementing-the-notify-changetracking-policy.html

Don't forget to update your entity:

YourBundle\Entity\YourThing:
    type: entity
    table: some_table
    changeTrackingPolicy: NOTIFY

See this thread:

https://github.com/Atlantic18/DoctrineExtensions/issues/333#issuecomment-16738878

Engrossment answered 9/5, 2013 at 19:50 Comment(3)
Even it's an old question, I've decided to not implement my own notify function and let this crappy behaviour goin on :(Gabbro
@Gabbro Not really the fault of Doctrine - but PHP's crappy object comparisonEngrossment
Oh sure, this could be also a fault of PHP, sorry. But I could not imagine that it would be that difficult, to compare objects on my own, if I write an bundle/plugin for an application based on PHP, but I would not hack into Gedmo bundle. But thanks for your answer, nevertheless.Gabbro
D
3

I also encountered this problem today and solved it. Here is complete solution, working for all string, float, int and DateTime values.

  1. Make your own LoggableListener and use it instead of Gedmo Listener.

    <?php
    
    namespace MyBundle\Loggable\Listener;
    
    use Gedmo\Loggable\LoggableListener;
    use Gedmo\Tool\Wrapper\AbstractWrapper;
    
    class MyLoggableListener extends LoggableListener
    {
        protected function getObjectChangeSetData($ea, $object, $logEntry)
        {
            $om = $ea->getObjectManager();
            $wrapped = AbstractWrapper::wrap($object, $om);
            $meta = $wrapped->getMetadata();
            $config = $this->getConfiguration($om, $meta->name);
            $uow = $om->getUnitOfWork();
            $values = [];
    
            foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
                if (empty($config['versioned']) || !in_array($field, $config['versioned'])) {
                    continue;
                }
    
                $oldValue = $changes[0];
                if ($meta->isSingleValuedAssociation($field) && $oldValue) {
                    if ($wrapped->isEmbeddedAssociation($field)) {
                        $value = $this->getObjectChangeSetData($ea, $oldValue, $logEntry);
                    } else {
                        $oid = spl_object_hash($oldValue);
                        $wrappedAssoc = AbstractWrapper::wrap($oldValue, $om);
                        $oldValue = $wrappedAssoc->getIdentifier(false);
                        if (!is_array($oldValue) && !$oldValue) {
                            $this->pendingRelatedObjects[$oid][] = [
                                'log' => $logEntry,
                                'field' => $field,
                            ];
                        }
                    }
                }
    
                $value = $changes[1];
                if ($meta->isSingleValuedAssociation($field) && $value) {
                    if ($wrapped->isEmbeddedAssociation($field)) {
                        $value = $this->getObjectChangeSetData($ea, $value, $logEntry);
                    } else {
                        $oid = spl_object_hash($value);
                        $wrappedAssoc = AbstractWrapper::wrap($value, $om);
                        $value = $wrappedAssoc->getIdentifier(false);
                        if (!is_array($value) && !$value) {
                            $this->pendingRelatedObjects[$oid][] = [
                                'log' => $logEntry,
                                'field' => $field,
                            ];
                        }
                    }
                }
    
                //fix for DateTime, integer and float entries
                if ($value == $oldValue) {
                    continue;
                }
    
                $values[$field] = $value;
            }
    
            return $values;
        }
    }
    
  2. For Symfony application, register your listener in config.yml file.

    stof_doctrine_extensions:
        orm:
            default:
                loggable: true
        class:
            loggable: MyBundle\Loggable\Listener\MyLoggableListener
    
  3. If you are using DateTime fields in your entities, but in database you store only date, then you also need to reset time part in all setters.

    public function setDateValue(DateTime $dateValue = null)
    {
        $dateValue->setTime(0, 0, 0);
        $this->dateValue = $dateValue;
        return $this;
    }
    

That should do the job.

Dullish answered 13/4, 2017 at 20:57 Comment(0)
K
0

For \DateTime I am still working on it but for the second part of your question there is a way that solved my problem with my Numeric properties:

/**
 * @ORM\Entity
 * @Gedmo\Loggable
 * @ORM\Entity(repositoryClass="Acme\MainBundle\Repository\ApplicationRepository")
 * @ORM\Table(name="application")
 */
class Application {

[...]

    /**
     * @ORM\Column(type="integer")
     * @Assert\NotBlank(groups={"FormStepOne", "UserEditApplication"})
     * @Gedmo\Versioned
     */
    protected $insurance_number;
}

Here you declare insurance_number as an integer property but as we know PHP has no type and does dynamic casting which is a conflicting thing with Gedmo Loggable.

To solve, just make sure that you are doing explicit casting yourself either in your Setter Method or in your Business Logic.

for instance replace this(Business Logic):

$application->setInsuranceNumber($valueComeFromHtmlForm)

with this one:

$application->setInsuranceNumber( (int)$valueComeFromHtmlForm)

Then when you persist your object you will not see any records in your logs.

I think this is because Loggable or Doctrine Change Tracker expects Integer and receives String (which is a 'not casted Integer') and So it marks the property dirty. We can see it in Log Record (S denotes that the new value is String.)

Kaiulani answered 18/10, 2016 at 8:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.