Gedmo\Loggable logs data that doesn't have changed - symfony

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#example.com"
["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.

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.)

Related

Save dates in UTC

I'm currently trying to save my dates in database with de UTC timezone.
To get the user timezone, I have a JS function that makes an AJAX request to my back-end like this :
import $ from 'jquery';
import jstz from 'jstz';
export default function setSessionTimezone(route)
{
var timezone = jstz.determine();
$.ajax({
type:'POST', async:true, cache:false, url:route, data:"timezone="+timezone.name(),
success:function(data) { if (data.reloadPage) location.reload(); }
});
}
This method is called only if the timezone is not already in session.
So, for now, I have the user timezone in my back-end, that was the first step.
I want to save it in the database.
With this SO post, I found something interesting : Symfony buildForm convert datetime to local and back to utc
They recommend to use the "model_timezone" and "view_timezone" for the forms, so did I :
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UtcTypeExtension extends AbstractTypeExtension
{
/**
* #var SessionInterface
*/
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
/**
* Return the class of the type being extended.
*/
public static function getExtendedTypes(): iterable
{
return [TimeType::class, DateType::class];
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'model_timezone' => 'UTC',
"view_timezone" => $this->session->get('tools_timezone')
]);
}
}
And hooora, it works.
But only within the forms.
If I want to display the dates with Twig or from PHP, I need to get the timezone from session and change the DateTime's Timezone.
So I searched another option.
I found this on the Doctrine Website to change the timezone directly from Doctrine.
This sound interesting, but I'm probably missing a point because it doesn't seem to work, even after I added the following configuration :
doctrine:
dbal:
types:
datetime: SomeNamespace\DoctrineExtensions\DBAL\Types\UTCDateTimeType
So I would like to know if what I want to do is even possible ? Or if I'm forced to override Twig "date" filter to use my timezone ? And if I want to display a date from PHP, I'm also force to use the timezone from the session?
I found something that seem to answer my question.
I added a listener to the doctrine's event "postLoad".
For information, the TimezoneProvider just return the DateTimezone object from the session or UTC if it was not defined.
use DateTime;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping\Column;
use SomeNamespace\Provider\TimezoneProvider;
use ReflectionClass;
use ReflectionException;
use Symfony\Component\PropertyAccess\PropertyAccess;
class DateTimeConverterListener
{
/**
* #var AnnotationReader
*/
private $reader;
/**
* #var TimezoneProvider
*/
private $timezoneProvider;
public function __construct(AnnotationReader $reader, TimezoneProvider $timezoneProvider)
{
$this->reader = $reader;
$this->timezoneProvider = $timezoneProvider;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$propertyAccessor = PropertyAccess::createPropertyAccessor();
try {
$reflection = new ReflectionClass(get_class($entity));
//We search the properties where we need to update the Timezone.
foreach ($reflection->getProperties() as $property) {
$annotation = $this->reader->getPropertyAnnotation($property, Column::class);
if (!$annotation instanceof Column)
continue;
switch ($annotation->type) {
case "time":
case "date":
case "datetime":
/** #var DateTime|null $attribute */
$attribute = $propertyAccessor->getValue($entity, $property->getName());
if (null === $attribute)
continue 2;
//The getTimezone function returns UTC in case of no session information. And because it's a
// DateTime object, we don't need to set the value after the modification
$attribute->setTimezone($this->timezoneProvider->getTimezone());
break;
}
}
} catch (ReflectionException $e) {
//Abort the transformation
}
}
}
To display the date properly with the twig filter "|date", I also update Twig through an event :
use SomeNamespace\Provider\TimezoneProvider;
use Twig\Environment;
use Twig\Extension\CoreExtension;
class SetupTwigTimezoneListener
{
/**
* #var TimezoneProvider
*/
private $timezoneProvider;
/**
* #var Environment
*/
private $twig;
public function __construct(TimezoneProvider $timezoneProvider, Environment $twig)
{
$this->timezoneProvider = $timezoneProvider;
$this->twig = $twig;
}
public function onKernelRequest()
{
//Define the timezone of the application based of the timezone of the user
$this->twig->getExtension(CoreExtension::class)->setTimezone($this->timezoneProvider->getTimezone()->getName());
}
}
I'm not quite sure this is a perfect solution, but it seem to work.

DateTimePicker and Datatransformer, the form render doesnt take the string into account

I got somme issue with Symfony to convert a DateTime into string. I use a DataTransformer to format my Datetime but in the form, there is an error that say : "This value should be of type string".
Here is my code:
My Entity : Shift.php (only the necessary)
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
* #Assert\DateTime(message="La date de début doit être au format DateTime")
*/
private $start_at;
My ShiftType :
$builder
->add('start_at', TextType::class, ['attr' => [ 'class' => 'dateTimePicker']])
->add('end_at', TextType::class, ['attr' => [ 'class' => 'dateTimePicker']])
->add('has_eat')
->add('break_duration')
->add('comment')
;
$builder->get('start_at')->addModelTransformer($this->transformer);
$builder->get('end_at')->addModelTransformer($this->transformer);
And my DataTransformer :
/**
* #param DateTime|null $datetime
* #return string
*/
public function transform($datetime)
{
if ($datetime === null)
{
return '';
}
return $datetime->format('Y-m-d H:i');
}
/**
* #param string $dateString
* #return Datetime|null
*/
public function reverseTransform($dateString)
{
if (!$dateString)
{
throw new TransformationFailedException('Pas de date(string) passé');
return;
}
$date = \Datetime::createFromFormat('Y-m-d H:i', $dateString);
if($date === false){
throw new TransformationFailedException("Le format n'est pas le bon (fonction reverseTransform)" . "$dateString");
}
return $date;
}
As i said, when i want submit the form, there are errors with the form.
It said "This value should be of type string." and it's caused by :
Symfony\Component\Validator\ConstraintViolation {#1107 ▼
root: Symfony\Component\Form\Form {#678 …}
path: "data.start_at"
value: DateTime #1578465000 {#745 ▶}
}
Something weard, when i want to edit a shift, Symfony get the date from the db and transform it into string with no error message. But as i want to save the edit, i got the same issue
Could you help me please ?
Thanks
I have had a similar issue in the past when using $this-> inside a form.
Sometimes $this would not contains the current data. It might explain why it loads at first, but, on submit, it might not be filled properly.
I would suggest creating a custom form type for better reusability, and I know it works very well.
Since you already have your DataTransformer class, you would need to create a new custom form Type. This new form type will extend TextType and use the datatransformer.
For example:
namespace App\Form\Branch\Type;
use App\Form\Branch\DataTransformer\PhoneFormatTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PhoneType extends AbstractType
{
private $tools;
public function __construct()
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new PhoneFormatTransformer();
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array());
}
public function getParent()
{
return TextType::class;
}
public function getBlockPrefix()
{
return 'phone';
}
}
If you services are not autowired, you would need to add a service definition with a specific tag
App\Form\Branch\Type\PhoneType:
tags:
- { name: form.type, alias: phone }
All that is left to do is use your new form type in your form builder:
use App\Form\Branch\Type\PhoneType;
...
$builder
->add('phone', PhoneType::class);
It is a tad more work, but it makes it very easy to reuse, which no doubt, you will have to do everytime a datetime field is needed.
Hope this helps!
Thanks for the answer. I tried tour solution and i wil use it, but it was not the solution to my issue. With your code, i got the same error.
The solution was in the Entity. In the annotations of start_at, I did an #Assert/DateTime and it was a bad use of it. I just delete this line and all is now correct :
/**
* #ORM\Column(type="datetime")
*
*/
private $start_at;
/**
* #ORM\Column(type="datetime", nullable=true)
* #Assert\GreaterThan(propertyPath="start_at", message="La date de fin doit être ultérieure...")
*/
private $end_at;
However, i use your code because it's a lot reusable so thank you for your contribution

Typo3 Error: The ColumnMap for property is missing (m:n)

I am having the same issue that was posted here:
Typo3 Error: The ColumnMap for property is missing
...except I am using a m:n relational table. Unfortunately my error continues:
I'm using Typo3 version 8.7.19 and I'm developing an extention. The two tables "mitarbeiter" and "zusatzlich" are connectet with a m:n relation. I try to search for a field in the table "zusatzlich" in the repository of "mitarbeiter". The relation of both is necessary.
If I try to execute the following query I get the error "The ColumnMap for property "tx_khsjmitarbeiter_domain_model_zusatzlich" of class "...\Mitarbeiter" is missing."
$query = $this->createQuery();
$zu = [];
if($zusatz1 != ""){
$zu[] = $query->equals('tx_khsjmitarbeiter_domain_model_zusatzlich.zusatz', $zusatz1);
}
if(count($zu)>0){
$query->matching($query->logicalAnd( $zu ));
}
return $query->execute();
The relevant TCA code of the field "connection_id" in "mitarbeiter" which contains the UID of "zusatzlich":
'connection_id' => [
'exclude' => true,
'label' => 'LLL:EXT:khsj_mitarbeiter/Resources/Private/Language/locallang_db.xlf:tx_khsjmitarbeiter_domain_model_mitarbeiter.connection_id',
'config' => [
'type' => 'select',
'renderType' => 'selectCheckBox',
'foreign_table' => 'tx_khsjmitarbeiter_domain_model_zusatzlich',
'MM' => 'tx_khsjmitarbeiter_mitarbeiter_zusatzlich_mm',
],
],
This is the object model:
/**
* connectionId
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich>
* #cascade remove
*/
protected $connectionId = null;
/**
* Initializes all ObjectStorage properties
* Do not modify this method!
* It will be rewritten on each save in the extension builder
* You may modify the constructor of this class instead
*
* #return void
*/
protected function initStorageObjects()
{
$this->connectionId = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
}
/**
* Adds a Zusatzlich
*
* #param ..\Model\Zusatzlich $connectionId
* #return void
*/
public function addConnectionId(..\Model\Zusatzlich $connectionId)
{
$this->connectionId->attach($connectionId);
}
/**
* Removes a Zusatzlich
*
* #param \..\Model\Zusatzlich $connectionIdToRemove The Zusatzlich to be removed
* #return void
*/
public function removeConnectionId(\..\Model\Zusatzlich $connectionIdToRemove)
{
$this->connectionId->detach($connectionIdToRemove);
}
/**
* Returns the connectionId
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich> connectionId
*/
public function getConnectionId()
{
return $this->connectionId;
}
/**
* Sets the connectionId
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\..\Model\Zusatzlich> $connectionId
* #return void
*/
public function setConnectionId(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $connectionId)
{
$this->connectionId = $connectionId;
}
I can add and apply new zusatz items in the BE to any mitarbeiter item so I am confident it is set up properly in that respect.
However I also noticed that if I change this line:
$zu[] = $query->equals('tx_khsjmitarbeiter_domain_model_zusatzlich.zusatz', $zusatz1);
...to this...
$zu[] = $query->equals('ANYTHINGATALL.zusatz', $zusatz1);
I get the same error referencing ANYTHINGATALL instead of tx_khsjmitarbeiter_domain_model_zusatzlich
Can anybody point me in the right direction?
j4k3’s answer led me to the right direction but is missing the property of the related model “zusatzlich”. Thus it should be:
if ($zusatz1 != "") {
$zu[] = $query->contains('connection_id.zusatz', $zusatz1);
}
It will be transformed into an SQL LeftJoin with the correct table names, given your relations are properly defined in the TCA.
In case of a property with underscore (e.g. zusatz_xy), the lowerCamelCase version will also work (zusatzXy).
You need to supply a property that is described in the TCA as constraint operator, not a table column. As far as I can tell, your query constraint should be:
if($zusatz1 != ""){
$zu[] = $query->contains('connection_id', $zusatz1);
}

JMSSerializerBundle - preserve relation name

I'm using Symfony2 with JMSSerializerBundle. And I'm new with last one =) What should I do in such case:
I have Image model. It contains some fields, but the main one is "name". Also, I have some models, which has reference to Image model. For example User and Application. User model has OneToOne field "avatar", and Application has OneToOne field "icon". Now, I want to serialize User instance and get something like
{
...,
"avatar": "http://example.com/my/image/path/image_name.png",
....
}
Also, I want to serialize Application and get
{
...,
"icon": "http://example.com/my/image/path/another_image_name.png",
...
}
I'm using #Inline annotation on User::avatar and Application::icon fields to reduce Image object (related to this field) to single scalar value (only image "name" needed). Also, my Image model has ExclusionPolicy("all"), and exposes only "name" field. For now, JMSSerializer output is
(For User instance)
{
...,
"name": "http://example.com/my/image/path/image_name.png",
...
}
(For Application instance)
{
...,
"name": "http://example.com/my/image/path/another_image_name.png",
...
}
The question is: How can I make JMSSerializer to preserve "avatar" and "icon" keys in serialized array instead of "name"?
Finally, I found solution. In my opinion, it is not very elegant and beautiful, but it works.
I told to JMSSerializer, that User::avatar and Application::icon are Images. To do that, I used annotation #Type("Image")
//src\AppBundle\Entity\User.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="avatar", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $avatar;
//...
//src\AppBundle\Entity\Application.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="icon", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $icon;
//...
I implemented handler, which serializes object with type Image to json.
<?php
//src\AppBundle\Serializer\ImageTypeHandler.php
namespace AppBundle\Serializer;
use AppBundle\Entity\Image;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpFoundation\Request;
class ImageTypeHandler implements SubscribingHandlerInterface
{
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
static public function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Image',
'method' => 'serializeImageToWebPath'
]
];
}
public function serializeImageToWebPath(JsonSerializationVisitor $visitor, Image $image = null, array $type, Context $context)
{
$path = $image ? "http://" . $this->request->getHost() . "/uploads/images/" . $image->getPath() : '';
return $path;
}
}
And the last step is to register this handler. I also injected request service to generate full web path to image in my handler.
app.image_type_handler:
class: AppBundle\Serializer\ImageTypeHandler
arguments: ["#request"]
scope: request
tags:
- { name: jms_serializer.subscribing_handler }
Also, you can use this workaround, to modify serialized data in post_serialize event.

Disable Doctrine Timestampable auto-updating the `updatedAt` field on certain update

In a Symfony2 project, I have a Doctrine entity that has a datetime field, called lastAccessed. Also, the entity uses Timestampable on updatedAt field.
namespace AppBundle\Entity;
use
Doctrine\ORM\Mapping as ORM,
Gedmo\Mapping\Annotation as Gedmo
;
class MyEntity {
/**
* #ORM\Column(type="datetime")
*/
private $lastAccessed;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="update")
*/
private $updatedAt;
}
I need to update the field lastAccessed without also updating the updatedAt field. How can I do that?
I just stumbled upon this as well and came up with this solution:
public function disableTimestampable()
{
$eventManager = $this->getEntityManager()->getEventManager();
foreach ($eventManager->getListeners('onFlush') as $listener) {
if ($listener instanceof \Gedmo\Timestampable\TimestampableListener) {
$eventManager->removeEventSubscriber($listener);
break;
}
}
}
Something very similar can be used to disable the blamable behavior as well of course.
There is also an easy way (or more proper way) how to do it, just override listener.
first create interface which will be implemented by entity
interface TimestampableCancelInterface
{
public function isTimestampableCanceled(): bool;
}
than extend Timestampable listener and override updateField.
this way we can disable all all events or with cancelTimestampable define custom rules for cancellation based on entity state.
class TimestampableListener extends \Gedmo\Timestampable\TimestampableListener
{
protected function updateField($object, $eventAdapter, $meta, $field)
{
/** #var \Doctrine\Orm\Mapping\ClassMetadata $meta */
$property = $meta->getReflectionProperty($field);
$newValue = $this->getFieldValue($meta, $field, $eventAdapter);
if (!$this->isTimestampableCanceled($object)) {
$property->setValue($object, $newValue);
}
}
private function isTimestampableCanceled($object): bool
{
if(!$object instanceof TimestampableCancelInterface){
return false;
}
return $object->isTimestampableCanceled();
}
}
implement interface. Most simple way is to just set property for this
private $isTimestampableCanceled = false;
public function cancelTimestampable(bool $cancel = true): void
{
$this->isTimestampableCanceled = $cancel;
}
public function isTimestampableCanceled():bool {
return $this->isTimestampableCanceled;
}
or define rules like you want
last thing is to not set default listener but ours.
I'm using symfony so:
stof_doctrine_extensions:
orm:
default:
timestampable: true
class:
timestampable: <Namespace>\TimestampableListener
Than you can just do
$entity = new Entity;
$entity->cancelTimestampable(true)
$em->persist($entity);
$em->flush(); // and you will get constraint violation since createdAt is not null :D
This way can be timestamps disabled per single entity not for whole onFlush. Also custom behavior is easy to apply based on entity state.
Timestampable is just doctrine behavior so is executed every time when you use an ORM.
In my opinion the simplest way is just using DBAL layer and raw sql query.
For example:
$sql = "UPDATE my_table set last_accessed = :lastAccess where id = :id";
//set parameters
$params['lastAccess'] = new \DateTime();
$params['id'] = $some_id;
$stmt = $this->entityManager->getConnection()->prepare($sql);
$stmt->execute($params);
You can of course put it into proper repository class

Resources