简体   繁体   English

学说2 - 记录manyToMany关系中的变化

[英]Doctrine 2 - Log changes in manyToMany relation

I use Loggable behavioral extension to log changes in my entities. 我使用Loggable行为扩展来记录我的实体中的更改。 I want to log changes in manyToMany relations too. 我想记录manyToMany关系中的更改。 I want to show to user this kind of change log: 我想向用户显示这种更改日志:

+--------------------------------------------------+
| Article "My Article" change log:                 |
+-------+------------+-----------------------------+
| Who   | When       | What                        |
+-------+------------+-----------------------------+
| Admin | 2015-07-01 | Removed tags "tag1", "tag2" |
| Admin | 2015-07-01 | Added tags "tag3"           |
+-------+------------+-----------------------------+

Event problem 事件问题

I think, Doctrine doesn't fire events when manyToMany relation changes , so Loggable (listening doctrine events) doesn't save log entry. 我认为, 当manyToMany关系发生变化时Doctrine不会触发事件 ,因此Loggable(监听学说事件)不会保存日志条目。 I can work around it by creating my own manyToMany table, but here's go the second problem: 我可以通过创建自己的manyToMany表来解决它,但这是第二个问题:

Own ManyToMany problem 拥有ManyToMany问题

When I create entity representing manyToMany relation without @JoinTable annotation, I don't know, how to write the new entity to behave like the old JoinTable one. 当我在没有@JoinTable注释的情况下创建表示manyToMany关系的实体时,我不知道如何编写新实体以像旧的JoinTable一样。 I want no BC break. 我不希望BC休息。 Can you give me a clue, how Doctrine handles this? 你能给我一个线索,Doctrine如何处理这个问题?

Do you have any recommendation, how to log changes in manyToMany relations? 您有什么建议,如何记录manyToMany关系中的变化?

Solution without creating your own join tables. 无需创建自己的连接表的解决方案。

I have modified the LoggableListener that I created to override the Gedmo LoggableListener, my version works, play around with this till you get it working. 我已经修改了我创建的LoggableListener来覆盖Gedmo LoggableListener,我的版本可以工作,玩这个直到你让它工作。

Basically, extend the Gedmo LoggableListener with your own version and override /add a few modified functions: 基本上,使用您自己的版本扩展Gedmo LoggableListener并覆盖/添加一些修改的函数:

prePersistLogEntry is enabled to allow you to modify the logEntry if you want to. 启用prePersistLogEntry以允许您根据需要修改logEntry。 My logEntry entities contain a user entity and the users Full Name instead of their username. 我的logEntry实体包含用户实体和用户全名而不是用户名。

getCollectionsChangeSetData is a new function to extract the collection and get access to the Doctrine PersistentCollections methods. getCollectionsChangeSetData是一个新函数,用于提取集合并访问Doctrine PersistentCollections方法。 [ http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html] [ http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html]

stripCollectionArray new function to extract the desired information from the collection entities and insert them into a php array for persisting to the LogEntry. stripCollectionArray用于从集合实体中提取所需信息的新函数,并将它们插入到php数组中以持久保存到LogEntry。

For information, if you are planning to user the revert functionality of the Loggable doctrine extension then you will also need to extend and override the revert method in the LogEntryRepository. 有关信息,如果您计划使用Loggable doctrine扩展的还原功能,那么您还需要扩展和覆盖LogEntryRepository中的revert方法。 The current revert method will not recognise the id from the ManyToMany associations saved in the LogEntry. 当前的revert方法无法识别LogEntry中保存的ManyToMany关联中的id。 That is why stripCollectionArray function also saves the 'id' and 'class' values to the LogEntry. 这就是为什么stripCollectionArray函数还将'id'和'class'值保存到LogEntry。

Good Luck. 祝好运。

<?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;
    }

}

Since i cannot add a comment to the accepted answer, i ll write here :) 由于我无法在接受的答案中添加评论,我会在这里写:)

Accepted solution will not work if you have multiple persists of your main entity in same flush. 如果您的主实体在同一次刷新中有多个持久性,则接受的解决方案将不起作用。 The last set of ManyToMany collection will be attached to all persisted entities. 最后一组ManyToMany集合将附加到所有持久化实体。 If you want to pick only the approriate you will have to check if the collection belongs to the processed object. 如果您只想选择approriate,则必须检查该集合是否属于已处理对象。

For example instead of 例如,而不是

 // 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);
}   

you can use 您可以使用

// 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);
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM