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?
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
I also encountered this problem today and solved it. Here is complete solution, working for all string, float, int and DateTime values.
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;
}
}
For Symfony application, register your listener in config.yml file.
stof_doctrine_extensions:
orm:
default:
loggable: true
class:
loggable: MyBundle\Loggable\Listener\MyLoggableListener
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With