Check if a Symfony Doctrine Entity has Changed from Form Submission - php

Question
Can I use the Doctrine entity manager (or some other Symfony function) to check if an entity has been updated?
Background
I am building a CMS with the ability to save "versions" of each page. So I have a Doctrine annotated entity $view (which is basically the "page), and this entity has nested associated entities like $view->version (which contain the majority of the information that can be updated in different revisions). This entity is edited with a standard Symfony form in the CMS. When the form is submitted, it does a $em->persist($view) and the Entity Manager detects if any of the fields have been changed. If there are changes, the changes are persisted. If there are no changes, the entity manager ignores the persist and saves itself a database call to update. Great.
But before the entity is saved, my versioning system checks if it's been more than 30 minutes since the current version was last save, or if the user submitting the form is different than the user who saved the current version, and if so it clones the $viewVersion. So the main record for $view remains the same id, but it works from an updated revision. This works great.
HOWEVER... If it's been a while since the last save, and someone just looks at the record without changing anything, and hits save, I don't want the version system to clone a new version automatically. I want to check and confirm that the entity has actually changed. The Entity Manager does this before persisting an entity. But I can't rely on it because before I call $em->persist($view) I have to clone $view->version. But before I clone $view->version I need to check if any of the fields in the entity or it's nested entities have been updated.
Basic Solution
The solution is to calculate the change set:
$form = $this->createForm(new ViewType(), $view);
if ($request->isMethod( 'POST' )) {
$form->handleRequest($request);
if( $form->isValid() ) {
$changesFound = array();
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
// The Version (hard coded because it's dynamically associated)
$changeSet = $uow->getEntityChangeSet($view->getVersion());
if(!empty($changeSet)) {
$changesFound = array_merge($changesFound, $changeSet);
}
// Cycle through Each Association
$metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
$associations = $metadata->getAssociationMappings();
foreach($associations AS $k => $v) {
if(!empty($v['cascade'])
&& in_array('persist', $v['cascade'])
){
$fn = 'get'.ucwords($v['fieldName']);
$changeSet = $uow->getEntityChangeSet($view->getVersion()->{$fn}());
if(!empty($changeSet)) {
$changesFound = array_merge($changesFound, $changeSet);
}
}
}
}
}
The Complication
But I read that you shouldn't use this $uow->computerChangeSets() outside of a the lifecycle events listener. They say you should do a manual diff of the objects, e.g. $version !== $versionOriginal. But that doesn't work because some fields like timePublish always get updated, so they are always different. So is it really not possible to use this to getEntityChangeSets() in the context of a controller (outside of an event listener)?
How should I use an Event Listener? I don't know how to put all the pieces together.
UPDATE 1
I followed the advice and created an onFlush event listener, and presumably that should load automatically. But now the page has a big error which happens when my service definition for gutensite_cms.listener.is_versionable passes in another service of mine arguments: [ "#gutensite_cms.entity_helper" ]:
Fatal error: Uncaught exception 'Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException' with message 'Circular reference detected for service "doctrine.dbal.cms_connection", path: "doctrine.dbal.cms_connection".' in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php:456 Stack trace: #0 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(604): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServiceInlinedDefinitionsSetup('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #1 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(630): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addService('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #2 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(117): Symfony\Componen in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php on line 456
My Service Definition
# This is the helper class for all entities (included because we reference it in the listener and it breaks it)
gutensite_cms.entity_helper:
class: Gutensite\CmsBundle\Service\EntityHelper
arguments: [ "#doctrine.orm.cms_entity_manager" ]
gutensite_cms.listener.is_versionable:
class: Gutensite\CmsBundle\EventListener\IsVersionableListener
#only pass in the services we need
# ALERT!!! passing this service actually causes a giant symfony fatal error
arguments: [ "#gutensite_cms.entity_helper" ]
tags:
- {name: doctrine.event_listener, event: onFlush }
My Event Listener: Gutensite\CmsBundle\EventListener\isVersionableListener
class IsVersionableListener
{
private $entityHelper;
public function __construct(EntityHelper $entityHelper) {
$this->entityHelper = $entityHelper;
}
public function onFlush(OnFlushEventArgs $eventArgs)
{
// this never executes... and without it, the rest doesn't work either
print('ON FLUSH EXECUTING');
exit;
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$updatedEntities = $uow->getScheduledEntityUpdates();
foreach($updatedEntities AS $entity) {
// This is generic listener for all entities that have an isVersionable method (e.g. ViewVersion)
// TODO: at the moment, we only want to do the following code for the viewVersion entity
if (method_exists($entity, 'isVersionable') && $entity->isVersionable()) {
// Get the Correct Repo for this entity (this will return a shortcut
// string for the repo, e.g. GutensiteCmsBundle:View\ViewVersion
$entityShortcut = $this->entityHelper->getEntityBundleShortcut($entity);
$repo = $em->getRepository($entityShortcut);
// If the repo for this entity has an onFlush method, use it.
// This allows us to keep the functionality in the entity repo
if(method_exists($repo, 'onFlush')) {
$repo->onFlush($em, $entity);
}
}
}
}
}
ViewVersion Repo with onFlush Event: Gutensite\CmsBundle\Entity\View\ViewVersionRepository
/**
* This is referenced by the onFlush event for this entity.
*
* #param $em
* #param $entity
*/
public function onFlush($em, $entity) {
/**
* Find if there have been any changes to this version (or it's associated entities). If so, clone the version
* which will reset associations and force a new version to be persisted to the database. Detach the original
* version from the view and the entity manager so it is not persisted.
*/
$changesFound = $this->getChanges($em, $entity);
$timeModMin = (time() - $this->newVersionSeconds);
// TODO: remove test
print("\n newVersionSeconds: ".$this->newVersionSeconds);
//exit;
/**
* Create Cloned Version if Necessary
* If it has been more than 30 minutes since last version entity was save, it's probably a new session.
* If it is a new user, it is a new session
* NOTE: If nothing has changed, nothing will persist in doctrine normally and we also won't find changes.
*/
if($changesFound
/**
* Make sure it's been more than default time.
* NOTE: the timeMod field (for View) will NOT get updated with the PreUpdate annotation
* (in /Entity/Base.php) if nothing has changed in the entity (it's not updated).
* So the timeMod on the $view entity may not get updated when you update other entities.
* So here we reference the version's timeMod.
*/
&& $entity->getTimeMod() < $timeModMin
// TODO: check if it is a new user editing
// && $entity->getUserMod() ....
) {
$this->iterateVersion($em, $entity);
}
}
public function getChanges($em, $entity) {
$changesFound = array();
$uow = $em->getUnitOfWork();
$changes = $uow->getEntityChangeSet($entity);
// Remove the timePublish as a valid field to compare changes. Since if they publish an existing version, we
// don't need to iterate a version.
if(!empty($changes) && !empty($changes['timePublish'])) unset($changes['timePublish']);
if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);
// The Content is hard coded because it's dynamically associated (and won't be found by the generic method below)
$changes = $uow->getEntityChangeSet($entity->getContent());
if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);
// Check Additional Dynamically Associated Entities
// right now it's just settings, but if we add more in the future, this will catch any that are
// set to cascade = persist
$metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
$associations = $metadata->getAssociationMappings();
foreach($associations AS $k => $v) {
if(!empty($v['cascade'])
&& in_array('persist', $v['cascade'])
){
$fn = 'get'.ucwords($v['fieldName']);
$changes = $uow->getEntityChangeSet($entity->{$fn}());
if(!empty($changeSet)) $changesFound = array_merge($changesFound, $changes);
}
}
if(!$changesFound) $changesFound = NULL;
return $changesFound;
}
/**
* NOTE: This function gets called onFlush, before the entity is persisted to the database.
*
* VERSIONING:
* In order to calculate a changeSet, we have to compare the original entity with the form submission.
* This is accomplished with a global onFlush event listener that automatically checks if the entity is versionable,
* and if it is, checks if an onFlush method exists on the entity repository. $this->onFlush compares the unitOfWork
* changeSet and then calls this function to iterate the version.
*
* In order for versioning to work, we must
*
*
*/
public function iterateVersion($em, $entity) {
$persistType = 'version';
// We use a custom __clone() function in viewVersion, viewSettings, and ViewVersionTrait (which is on each content type)
// It ALSO sets the viewVersion of the cloned version, so that when the entity is persisted it can properly set the settings
// Clone the version
// this clones the $view->version, and the associated entities, and resets the associated ids to null
// NOTE: The clone will remove the versionNotes, so if we decide we actually want to keep them
// We should fetch them before the clone and then add them back in manually.
$version = clone $entity();
// TODO: Get the changeset for the original notes and add the versionNotes back
//$version->setVersionNotes($versionModified->getVersionNotes());
/**
* Detach original entities from Entity Manager
*/
// VERSION:
// $view->version is not an associated entity with cascade=detach, it's just an object container that we
// manually add the current "version" to. But it is being managed by the Entity Manager, so
// it needs to be detached
// TODO: this can probably detach ($entity) was originally $view->getVersion()
$em->detach($entity);
// SETTINGS: The settings should cascade detach.
// CONTENT:
// $view->getVersion()->content is also not an associated entity, so we need to manually
// detach the content as well, since we don't want the changes to be saved
$em->detach($entity->getContent());
// Cloning removes the viewID from this cloned version, so we need to add the new cloned version
// to the $view as another version
$entity->getView()->addVersion($version);
// TODO: If this has been published as well, we need to mark the new version as the view version,
// e.g. $view->setVersionId($version->getId())
// This is just for reference, but should be maintained in case we need to utilize it
// But how do we know if this was published? For the time being, we do this in the ContentEditControllerBase->persist().
}

So my understanding is that you basically need to detect if doctrine is going to update an entity in the database so you can record that change or insert a version of the old entity.
The way you should do that is by adding a listener to the onFlush event. You can read more about registering doctrine events here.
For example you will need to add to your config file a new service definition like that:
my.flush.listener:
class: Gutensite\CmsBundle\EventListener\IsVersionableListener
calls:
- [setEntityHelper, ["#gutensite_cms.entity_helper"]]
tags:
- {name: doctrine.event_listener, event: onFlush}
Then you will create the class EventListener like any symfony service. In this class, a function with the same name as the event will be called, ( onFlush in this case )
Inside this function you can go through all updated entities:
namespace Gutensite\CmsBundle\EventListener;
class IsVersionableListener {
private $entityHelper;
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$updatedEntities = $uow->getScheduledEntityUpdates();
foreach ($updatedEntities as $entity) {
if ($entity->isVersionable()) {
$changes = $uow->getEntityChangeSet($entity);
//Do what you want with the changes...
}
}
}
public function setEntityHelper($entityHelper)
{
$this->entityHelper = $entityHelper;
return $this;
}
}
$entity->isVersionable() is just a method I made up which you can add to your entities to easily decide whether this entity is tracked for changes or not.
NOTE: Since you are doing this in the onFlush. That means that all changes that will be saved to the DB have been computed. Doctrine will not persist any new entities. If you create new entities you will need to manually compute the changes and persist them.

First thing: there is a versionable extension for Doctrine (it was recently renamed to Loggable), that does exactly what you are describing, check that out, maybe it solves your use case.
With that said, this sounds like a job for an onFlush event listener. The UnitOfWork is already in a "changes computed" state, where you can just ask for all of the the changes on all of the entities (you can filter them with an instanceof, or something like that).
This still doesn't solve your issue about saving a new, and the old version too. I am not 100% sure this will work, because persisting something in an onFlush listener will involve workarounds (since doing a flush in an onFlush will result in an infinite loop), but there is $em->refresh($entity) that will roll back an entity to its "default" state (as it was constructed from the database).
So you can try something like, check to see if there are changes to the entity, if there are, clone it, persist the new one, refresh the old one, and save them. You will have to do extra legwork for your relations though, because cloning only creates a shallow copy in PHP.
I'd advise to go with the versionable extension, since it has everything figured out, but read up on the onFlush listener too, maybe you can come up with something.

In case someone is still interested in a different way than the accepted answer (it was not working for me and I found it messier than this way in my personal opinion).
I installed the JMS Serializer Bundle and on each entity and on each property that I consider a change I added a #Group({"changed_entity_group"}). This way, I can then make a serialization between the old entity, and the updated entity and after that it's just a matter of saying $oldJson == $updatedJson. If the properties that you are interested in or that you would like to consider changes the JSON won't be the same and if you even want to register WHAT specifically changed then you can turn it into an array and search for the differences.
I used this method since I was interested mainly in a few properties of a bunch of entities and not in the entity entirely. An example where this would be useful is if you have a #PrePersist #PreUpdate and you have a last_update date, that will always be updated therefore you will always get that the entity was updated using unit of work and stuff like that.
Hope this method is helpful to anyone.

Related

How to get the created object in controller when implementing CQRS

I am evaluating the CQRS pattern and wonder what would be the best way to obtain an Entity created by a command in the same action so I can render it in the view.
The two options I can think of are.
1) Create an id in the controller and send it with the command to fetch the entity by finding it by id.
2) Create an instance of the entity and send it with the command so I have a reference to it after it's populated
Example code
public function createEntityAction(array $data) {
$eventDispatcher = $this->get('event_dispatcher');
$eventDispatcher->dispatch(
CreateEntityHandler::name, // Handler
new Entity($data) // Command
);
// Placeholder //
$entity = get-the-created-entity
// //
return $this->view($entity, Response::HTTP_OK);
}
Second option is not really an option. "Entity creation", which is in fact is a business operation, is a command handling.
Generally speaking, the one who sends a command, whose handler creates an entity, should send the entity id with it. In what way the identity is generated is just an implementation concern.
Usually, command handlers either do what they suppose to do and return nothing (or ACK) or throw (or NAK).

Symfony2 Doctrine 'preUpdate' event to encode password

Based on question and this answer, i confuesd what the event preUpdate() in combination with symfony and doctrine really does.
To be exact, i mean this line in the answer above:
$event->setNewValue('password', $user->getPassword());
Why i need the extra step of manually setting the new value?
I thought that doctrine handles the event and update the entity by itself without a call of e.g. flush?
Supportive of the thrown exception
Field "password" is not a valid field of the entity "...\UserBundle\Entity\User" in PreUpdateEventArgs.
when i use this, i don't understand why i need to modify the entity manually.
Extended info about the exception: it's only thrown when users logged in. Not when users are registered or edited.
The setNewValue() method can be used to change the value of fields that have been marked as updated. It doesn't work if, for whatever reason, the field you want to update isn't already changed.
Usually, setting the password to null is enough to add it to the changeset. In the event that it isn't, you can request the unit of work recompute the changes.
For example:
<?php
if ($event->hasChangedField('password')) {
$event->setNewValue('password', $newPassword);
}
else {
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata(User::class),
$entity
);
}

How can I tell if the current transaction will change any entities with Doctrine 2?

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.
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);
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.
#PreUpdate event won't be invoked if there's no change on the entity.
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).

Doctrine postLoad event for associations

I currently have an entity which I would like to modify slightly upon load. This modification will be a one time change which will then be persisted in a new field along with the entity.
To clarify my current objective: The entity is a "Location" and forms part of a nested set. It has a name, lft/rgt values and an Id. One computationally expensive task I was performing with this entity was to fetch a full location path and display it as text. For example, with the location entity "Waterloo" I want to display as "Waterloo|London|United Kingdom". This involves traversing through the entire set (to the root node).
To reduce the cost of this I've created a new field on the Location entity that can be stamped with this value (and updated as/when the location (or any location within the tree) name is modified). Considering my application is in a live state I need to avoid running this as a one off process as it would incur quite an intensive one-time hit on the DB, instead I'd like to apply this update as and when each location (without that value) is loaded. I assumed Doctrine's postLoad event mechanism would be perfect for achieving this, however..
The Location entities are not loaded directly by my application, they will always be the inverse side of a relation. With this is mind, and the fact that doctrine's postLoad event:
Doesn't load (allow access to) any associated data
Is only fired for owning Entities
I have no way of gently making these modifications.
Anyone have any advice, or experience on this?
I was able to load the associated Location objects within the postLoad event by using the initializeObject() method on the Entity Manager.
/**
* Upon loading the object, if the location text isn't set, set it
* #param \Doctrine\ORM\Event\LifecycleEventArgs $args
*/
public function postLoad(\Doctrine\ORM\Event\LifecycleEventArgs $args)
{
$this->em = $args->getEntityManager();
$entity = $args->getEntity();
if ($entity instanceof \Entities\Location)
{
if (is_null($entity->getPathText()))
{
$entity->setPathText("new value");
$this->em->flush($entity);
}
} elseif ($entity instanceof {parent Entity})
{
$location = $entity->getLocation();
// This triggers the postLoad event again, but this time with Location as the parent Entity
$this->em->initializeObject($location);
}
}

Symfony2 Form handler check changes before persist

I'd like to check changes of my hydrated entity before the persist.
I've tryed to get a new entity into the onSuccess (after bindRequest) with DB values using a find, but this object have the hydrated values instead of DB values !
This is what i've tryed :
public function onSuccess(TachesDetails $detail) {
$tache_new = $detail->getTache();
$tache_old = $this->em->getRepository('NomDuBundle:Taches')->find($tache_new->getId());
var_dump($tache_old);
// ...
$this->em->persist($detail);
$this->em->persist($detail->getTache());
$this->em->flush();
}
Var_dump of $tache_old give me hydrated values.
EDIT :
I've find the solution after hours.
To resolve this problem, you'll have to create a clone of your entity in the controller and send it trough formHandler parameters.
In the onSuccess function, you can access it like this :
$this->entityCloned
First of all, you could use prePersist event for checking what changed. It's more suitable than onSuccess.
There's also more native way to check changes, using UnitOfWork object:
$unitOfWork = $entityManager->getUnitOfWork();
$unitOfWork->computeChangeSets();
$changes = $unitOfWork->getEntityChangeSet($yourEntity);

Categories