原则 2 - 记录多对多关系中的变化
Doctrine 2 - Log changes in manyToMany relation
我使用 Loggable behavioral extension 来记录我的实体中的更改。我也想记录多对多关系的变化。我想向用户显示这种更改日志:
+--------------------------------------------------+
| Article "My Article" change log: |
+-------+------------+-----------------------------+
| Who | When | What |
+-------+------------+-----------------------------+
| Admin | 2015-07-01 | Removed tags "tag1", "tag2" |
| Admin | 2015-07-01 | Added tags "tag3" |
+-------+------------+-----------------------------+
事件问题
我认为,Doctrine doesn't fire events when manyToMany relation changes,所以 Loggable(侦听学说事件)不保存日志条目。我可以通过创建自己的 manyToMany table 来解决这个问题,但这是第二个问题:
自己的多对多问题
当我在没有 @JoinTable 注释的情况下创建表示多对多关系的实体时,我不知道如何编写新实体使其表现得像旧的 JoinTable 实体。我不想要 BC 休息。你能给我一个线索吗,Doctrine 是如何处理这个的?
你有什么建议,如何记录多对多关系的变化?
无需创建自己的连接表的解决方案。
我已经修改了我创建的 LoggableListener 以覆盖 Gedmo LoggableListener,我的版本有效,尝试使用它直到你让它工作。
基本上,使用您自己的版本扩展 Gedmo LoggableListener 并覆盖/添加一些修改后的函数:
prePersistLogEntry 已启用,允许您根据需要修改 logEntry。我的 logEntry 实体包含一个用户实体和用户全名而不是用户名。
getCollectionsChangeSetData 是一个新函数,用于提取集合并访问 Doctrine PersistentCollections 方法。
[http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html]
stripCollectionArray 新函数,用于从集合实体中提取所需的信息并将它们插入到 php 数组中以持久保存到 LogEntry。
有关信息,如果您计划使用 Loggable doctrine 扩展的恢复功能,那么您还需要扩展和覆盖 LogEntryRepository 中的恢复方法。当前的还原方法无法识别保存在 LogEntry 中的 ManyToMany 关联中的 id。这就是 stripCollectionArray 函数还将 'id' 和 'class' 值保存到 LogEntry 的原因。
祝你好运。
<?php
namespace AppBundle\Listener;
use Doctrine\Common\EventArgs;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Loggable\LoggableListener as GedmoLoggableListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use AppBundle\Entity\Clause;
use AppBundle\Entity\GuidanceNote;
use AppBundle\Entity\Standard;
use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry;
use Doctrine\ORM\PersistentCollection;
/**
* Loggable listener
*
* Extends the Gedmo loggable listener to provide some custom functionality.
*
*
* @author Mark Ogilvie <mark.ogilvie@specshaper.com>
*/
class LoggableListener extends GedmoLoggableListener {
// Token storage to get user
private $tokenStorage;
// Injet token storage in the services.yml
public function __construct(TokenStorageInterface $token) {
$this->tokenStorage = $token;
}
/**
* Manipulate the LogEntry entity prior to persisting.
* In this case add a user, and set entity information
* according to the custom entity family group.
*
* @param EventArgs $eventArgs
*
* @return void
*/
protected function prePersistLogEntry($logEntry, $object) {
$user = $this->tokenStorage->getToken()->getUser();
$logEntry instanceof AbstractLogEntry;
$logEntry
->setUser($user)
->setChangedObject('text.default')
->setUsername($user->getFullName())
;
switch (true) {
case $object instanceof Clause:
$logEntry->setChangedObject('text.clause')
->setEntity($object)
;
break;
case $object instanceof GuidanceNote:
$logEntry->setChangedObject('text.guidanceNote')
->setEntity($object->getClause())
;
break;
case $object instanceof Standard:
$logEntry->setChangedObject('text.standard')
;
break;
}
}
/**
* Returns an objects changeset data
*
* Modified to create an array which has old and new values instead
* of just the new.
*
* Also added reference to UoW collection changes to pick up ManyToMany
* relationships
*
* @param LoggableAdapter $ea
* @param object $object
* @param object $logEntry
*
* @return array
*/
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();
// Define an array to return as the change set data.
$returnArray = array();
foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
if (empty($config['versioned']) || !in_array($field, $config['versioned'])) {
continue;
}
$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][] = array(
'log' => $logEntry,
'field' => $field,
);
}
}
}
$returnArray[$field]['previous'] = $changes[0];
$returnArray[$field]['new'] = $value;
}
// For each collection add it to the return array in our custom format.
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
return $returnArray;
}
/**
* New custom function to get information about changes to entity relationships
* Use the PersistentCollection methods to extract the info you want.
*
* @param PersistentCollection $col
* @return array
*/
private function getCollectionChangeSetData(PersistentCollection $col) {
$fieldName = $col->getMapping()['fieldName'];
// http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html
// $col->toArray() returns the onFlush array of collection items;
// $col->getSnapshot() returns the prePersist array of collection items
// $col->getDeleteDiff() returns the deleted items
// $col->getInsertDiff() returns the inserted items
// These methods return persistentcollections. You need to process them to get just the title/name
// of the entity you want.
// Instead of creating two records, you can create an array of added and removed fields.
// Use private a newfunction stripCollectionArray to process the entity into the array
$newValues[$fieldName]['new'] = $this->stripCollectionArray($col->toArray());
$newValues[$fieldName]['previous'] = $this->stripCollectionArray($col->getSnapshot());
return $newValues;
}
/**
* Function to process your entity into the desired format for inserting
* into the LogEntry
*
* @param type $entityArray
* @return type
*/
private function stripCollectionArray($entityArray) {
$returnArr = [];
foreach ($entityArray as $entity) {
$arr = [];
$arr['id'] = $entity->getId();
$arr['class'] = get_class($entity);
if (method_exists($entity, 'getName')) {
$arr['name'] = $entity->getName();
} elseif (method_exists($entity, 'getTitle')) {
$arr['name'] = $entity->getTitle();
} else {
$arr['name'] = get_class($entity);
}
$returnArr[] = $arr;
}
return $returnArr;
}
}
由于我无法对已接受的答案添加评论,所以我会写在这里:)
如果您在同一个刷新中有多个主要实体的持久化,则接受的解决方案将不起作用。
最后一组 ManyToMany 集合将附加到所有持久化实体。
如果您只想选择合适的,则必须检查该集合是否属于已处理的对象。
例如
而不是
// For each collection add it to the return array in our custom format.
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
你可以使用
// For each collection add it to the return array in our custom format.
$objectHash = spl_object_hash($object);
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$collectionOwner = $col->getOwner();
if (spl_object_hash($collectionOwner) === $objectHash) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
}
我使用 Loggable behavioral extension 来记录我的实体中的更改。我也想记录多对多关系的变化。我想向用户显示这种更改日志:
+--------------------------------------------------+
| Article "My Article" change log: |
+-------+------------+-----------------------------+
| Who | When | What |
+-------+------------+-----------------------------+
| Admin | 2015-07-01 | Removed tags "tag1", "tag2" |
| Admin | 2015-07-01 | Added tags "tag3" |
+-------+------------+-----------------------------+
事件问题
我认为,Doctrine doesn't fire events when manyToMany relation changes,所以 Loggable(侦听学说事件)不保存日志条目。我可以通过创建自己的 manyToMany table 来解决这个问题,但这是第二个问题:
自己的多对多问题
当我在没有 @JoinTable 注释的情况下创建表示多对多关系的实体时,我不知道如何编写新实体使其表现得像旧的 JoinTable 实体。我不想要 BC 休息。你能给我一个线索吗,Doctrine 是如何处理这个的?
你有什么建议,如何记录多对多关系的变化?
无需创建自己的连接表的解决方案。
我已经修改了我创建的 LoggableListener 以覆盖 Gedmo LoggableListener,我的版本有效,尝试使用它直到你让它工作。
基本上,使用您自己的版本扩展 Gedmo LoggableListener 并覆盖/添加一些修改后的函数:
prePersistLogEntry 已启用,允许您根据需要修改 logEntry。我的 logEntry 实体包含一个用户实体和用户全名而不是用户名。
getCollectionsChangeSetData 是一个新函数,用于提取集合并访问 Doctrine PersistentCollections 方法。 [http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html]
stripCollectionArray 新函数,用于从集合实体中提取所需的信息并将它们插入到 php 数组中以持久保存到 LogEntry。
有关信息,如果您计划使用 Loggable doctrine 扩展的恢复功能,那么您还需要扩展和覆盖 LogEntryRepository 中的恢复方法。当前的还原方法无法识别保存在 LogEntry 中的 ManyToMany 关联中的 id。这就是 stripCollectionArray 函数还将 'id' 和 'class' 值保存到 LogEntry 的原因。
祝你好运。
<?php
namespace AppBundle\Listener;
use Doctrine\Common\EventArgs;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Loggable\LoggableListener as GedmoLoggableListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use AppBundle\Entity\Clause;
use AppBundle\Entity\GuidanceNote;
use AppBundle\Entity\Standard;
use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry;
use Doctrine\ORM\PersistentCollection;
/**
* Loggable listener
*
* Extends the Gedmo loggable listener to provide some custom functionality.
*
*
* @author Mark Ogilvie <mark.ogilvie@specshaper.com>
*/
class LoggableListener extends GedmoLoggableListener {
// Token storage to get user
private $tokenStorage;
// Injet token storage in the services.yml
public function __construct(TokenStorageInterface $token) {
$this->tokenStorage = $token;
}
/**
* Manipulate the LogEntry entity prior to persisting.
* In this case add a user, and set entity information
* according to the custom entity family group.
*
* @param EventArgs $eventArgs
*
* @return void
*/
protected function prePersistLogEntry($logEntry, $object) {
$user = $this->tokenStorage->getToken()->getUser();
$logEntry instanceof AbstractLogEntry;
$logEntry
->setUser($user)
->setChangedObject('text.default')
->setUsername($user->getFullName())
;
switch (true) {
case $object instanceof Clause:
$logEntry->setChangedObject('text.clause')
->setEntity($object)
;
break;
case $object instanceof GuidanceNote:
$logEntry->setChangedObject('text.guidanceNote')
->setEntity($object->getClause())
;
break;
case $object instanceof Standard:
$logEntry->setChangedObject('text.standard')
;
break;
}
}
/**
* Returns an objects changeset data
*
* Modified to create an array which has old and new values instead
* of just the new.
*
* Also added reference to UoW collection changes to pick up ManyToMany
* relationships
*
* @param LoggableAdapter $ea
* @param object $object
* @param object $logEntry
*
* @return array
*/
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();
// Define an array to return as the change set data.
$returnArray = array();
foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
if (empty($config['versioned']) || !in_array($field, $config['versioned'])) {
continue;
}
$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][] = array(
'log' => $logEntry,
'field' => $field,
);
}
}
}
$returnArray[$field]['previous'] = $changes[0];
$returnArray[$field]['new'] = $value;
}
// For each collection add it to the return array in our custom format.
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
return $returnArray;
}
/**
* New custom function to get information about changes to entity relationships
* Use the PersistentCollection methods to extract the info you want.
*
* @param PersistentCollection $col
* @return array
*/
private function getCollectionChangeSetData(PersistentCollection $col) {
$fieldName = $col->getMapping()['fieldName'];
// http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html
// $col->toArray() returns the onFlush array of collection items;
// $col->getSnapshot() returns the prePersist array of collection items
// $col->getDeleteDiff() returns the deleted items
// $col->getInsertDiff() returns the inserted items
// These methods return persistentcollections. You need to process them to get just the title/name
// of the entity you want.
// Instead of creating two records, you can create an array of added and removed fields.
// Use private a newfunction stripCollectionArray to process the entity into the array
$newValues[$fieldName]['new'] = $this->stripCollectionArray($col->toArray());
$newValues[$fieldName]['previous'] = $this->stripCollectionArray($col->getSnapshot());
return $newValues;
}
/**
* Function to process your entity into the desired format for inserting
* into the LogEntry
*
* @param type $entityArray
* @return type
*/
private function stripCollectionArray($entityArray) {
$returnArr = [];
foreach ($entityArray as $entity) {
$arr = [];
$arr['id'] = $entity->getId();
$arr['class'] = get_class($entity);
if (method_exists($entity, 'getName')) {
$arr['name'] = $entity->getName();
} elseif (method_exists($entity, 'getTitle')) {
$arr['name'] = $entity->getTitle();
} else {
$arr['name'] = get_class($entity);
}
$returnArr[] = $arr;
}
return $returnArr;
}
}
由于我无法对已接受的答案添加评论,所以我会写在这里:)
如果您在同一个刷新中有多个主要实体的持久化,则接受的解决方案将不起作用。 最后一组 ManyToMany 集合将附加到所有持久化实体。 如果您只想选择合适的,则必须检查该集合是否属于已处理的对象。
例如 而不是
// For each collection add it to the return array in our custom format.
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
你可以使用
// For each collection add it to the return array in our custom format.
$objectHash = spl_object_hash($object);
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$collectionOwner = $col->getOwner();
if (spl_object_hash($collectionOwner) === $objectHash) {
$associations = $this->getCollectionChangeSetData($col);
$returnArray = array_merge($returnArray, $associations);
}
}