我可以使用Doctrine实体管理器(或其他一些Symfony函数)来检查实体是否已更新?
我正在构建一个能够保存每个页面“版本”的CMS。所以我有一个带有Doctrine注释的实体$view
(基本上就是“页面”),这个实体有嵌套的关联实体,如$view->version
(其中包含可以在不同版本中更新的大部分信息)在CMS中使用标准Symfony表单编辑此实体。提交表单时,它会执行$em->persist($view)
,实体管理器会检测是否有任何字段已更改。如果有更改,则更改为如果没有更改,实体管理器会忽略持久化并保存自己的数据库调用以进行更新。很棒。
但在保存实体之前,我的版本控制系统会检查自当前版本上次保存以来是否已超过30分钟,或者提交表单的用户是否与保存当前版本的用户不同,如果是它克隆了$viewVersion
。所以$view
的主要记录仍然是相同的ID,但它可以从更新的版本开始。这很有效。
但是......如果自上次保存以来已经有一段时间了,有人只是在不改变任何内容的情况下查看记录,并且点击保存,我不希望版本系统自动克隆新版本。我想检查并确认实体实际已更改。实体管理器在持久化实体之前执行此操作。但我不能依赖它,因为在我致电$em->persist($view)
之前,我必须克隆$view->version
。但在克隆$view->version
之前,我需要检查实体中的任何字段或嵌套实体是否已更新。
solution是计算变更集:
$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);
}
}
}
}
}
但我读到你shouldn't use this $uow->computerChangeSets()
outside of a the lifecycle events listener。他们说你应该对对象进行手动差异,例如$version !== $versionOriginal
。但这不起作用,因为像timePublish这样的某些字段总是会更新,因此它们总是不同的。那么在控制器的上下文中(在事件监听器之外)真的不可能将它用于getEntityChangeSets()
吗?
我应该如何使用事件监听器?我不知道如何将所有部分放在一起。
我遵循了建议并创建了一个onFlush事件监听器,大概应该自动加载。但是现在,当gutensite_cms.listener.is_versionable
的服务定义传递给我的另一个服务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
我的服务定义
# 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 }
我的事件监听器: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);
}
}
}
}
}
使用onFlush活动的ViewVersion回购: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().
}
答案 0 :(得分:6)
所以我的理解是你基本上需要检测doctrine是否要更新数据库中的实体,以便记录该更改或插入旧实体的版本。
您应该这样做的方法是向onFlush
事件添加一个侦听器。您可以阅读有关注册学说事件here的更多信息。
例如,您需要在配置文件中添加一个新的服务定义:
my.flush.listener:
class: Gutensite\CmsBundle\EventListener\IsVersionableListener
calls:
- [setEntityHelper, ["@gutensite_cms.entity_helper"]]
tags:
- {name: doctrine.event_listener, event: onFlush}
然后,您将像任何symfony服务一样创建类EventListener
。在这个类中,将调用与事件同名的函数,(在这种情况下为onFlush
)
在此功能中,您可以浏览所有更新的实体:
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()
只是我制作的一种方法,您可以将其添加到实体中,以便轻松决定是否跟踪此实体是否有变更。
注意:因为您在onFlush中执行此操作。这意味着已经计算了将保存到DB的所有更改。学说不会坚持任何新实体。如果您创建新实体,则需要手动计算更改并保留它们。
答案 1 :(得分:1)
第一件事:有一个versionable extension for Doctrine(它最近被重命名为Loggable),它正是你所描述的,检查出来,也许它解决了你的用例。
话虽如此,这听起来像onFlush事件监听器的工作。 UnitOfWork已经在&#34;计算变化&#34; state,您可以在其中询问所有实体的所有更改(您可以使用instanceof或类似的东西过滤它们)。
这仍然无法解决有关保存新版本和旧版本的问题。我不是100%肯定这会起作用,因为在onFlush监听器中持久存在会涉及变通方法(因为在onFlush中执行刷新将导致无限循环),但是有$ em-&gt; refresh($ entity)将实体回滚到&#34;默认&#34; state(因为它是从数据库构建的)。
所以你可以尝试类似的东西,检查实体是否有变化,如果有,克隆它,坚持新的,刷新旧的,并保存它们。但是,您必须为您的关系做额外的工作,因为克隆只会在PHP中创建一个浅层副本。
我建议使用可版本化的扩展程序,因为它已经找到了所有内容,但也可以在onFlush listener上阅读,也许你可以想出一些东西。
答案 2 :(得分:0)
如果某人仍然对接受的答案采取不同的方式感兴趣(这对我不起作用,而且在个人看来我发现它比这种方式更麻烦)。
我安装了JMS Serializer Bundle,并且在每个实体和每个属性上,我认为是一个更改,我添加了@Group({“changed_entity_group”})。这样,我就可以在旧实体和更新后的实体之间进行序列化,之后只需说$ oldJson == $ updatedJson即可。如果您感兴趣的属性或您想要考虑的属性发生更改,则JSON将不同,如果您甚至想要注册具体更改的内容,则可以将其转换为数组并搜索差异。
我使用这种方法,因为我主要对一堆实体的一些属性感兴趣,而不是完全不在实体中。这将是有用的一个例子是,如果你有一个@PrePersist @PreUpdate并且你有一个last_update日期,那将永远更新,因此你总是会得到实体是使用工作单元和类似的东西更新的。
希望这种方法对任何人都有帮助。