【问题标题】:Doctrine 2 - Log changes in manyToMany relationDoctrine 2 - 在多对多关系中记录更改
【发布时间】:2015-08-07 06:27:44
【问题描述】:

我使用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 表来解决它,但这是第二个问题:

自己的多对多问题

当我创建表示没有 @JoinTable 注释的 manyToMany 关系的实体时,我不知道如何编写新实体以使其表现得像旧的 JoinTable 一样。我不想要BC休息。你能给我一个线索,Doctrine 是如何处理这个问题的?

你有什么建议,如何记录多对多关系的变化?

【问题讨论】:

    标签: php doctrine-orm doctrine many-to-many


    【解决方案1】:

    无需创建自己的连接表的解决方案。

    我已经修改了我创建的 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 原则扩展的恢复功能,那么您还需要扩展和覆盖 LogEntryRepository 中的恢复方法。当前的 revert 方法将无法识别 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;
        }
    
    }
    

    【讨论】:

      【解决方案2】:

      由于我无法对已接受的答案添加评论,所以我会写在这里:)

      如果您在同一刷新中有多个主实体的持久性,则接受的解决方案将不起作用。 最后一组 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);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2012-11-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多