Подтвердить что ты не робот

Doctrine 2 - Log изменяет отношение manyToMany

Я использую Логическое расширение поведения для регистрации изменений в моих сущностях. Я хочу также вносить изменения в отношения manyToMany. Я хочу показать пользователю такой журнал изменений:

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

Проблема с событием

Думаю, Doctrine не срабатывает при изменении отношения manyToMany, поэтому Loggable (прослушивание доктрин) не сохраняет запись в журнале. Я могу обойти это, создав свою собственную таблицу ManyToMany, но вот вторая проблема:

Собственная проблема ManyToMany

Когда я создаю объект, представляющий отношение manyToMany без аннотации @JoinTable, я не знаю, как написать новый объект, чтобы вести себя как старый JoinTable. Я не хочу разорвать БК. Не могли бы вы дать мне понять, как это делает Доктрина?

Есть ли у вас рекомендации, как регистрировать изменения в отношениях manyToMany?

4b9b3361

Ответ 1

Решение без создания ваших собственных таблиц соединений.

Я изменил LoggableListener, который я создал, чтобы переопределить Gedmo LoggableListener, моя версия работает, играйте с этим, пока не получите его работу.

В основном расширьте Gedmo LoggableListener вашей собственной версией и переопределите/добавьте несколько измененных функций:

prePersistLogEntry позволяет вам изменять logEntry, если хотите. Мои объекты logEntry содержат пользовательский объект и полное имя пользователя вместо имени пользователя.

getCollectionsChangeSetData​​strong > - это новая функция для извлечения коллекции и получения доступа к методам Doctrine PersistentCollections. [http://www.doctrine-project.org/api/orm/2.1/class-Doctrine.ORM.PersistentCollection.html]

stripCollectionArray новая функция для извлечения искомой информации из объектов коллекции и вставки их в массив php для сохранения в LogEntry.

Для получения информации, если вы планируете пользователю вернуть функциональность расширения доктрины Loggable, вам также потребуется расширить и переопределить метод возврата в LogEntryRepository. Текущий метод возврата не будет распознавать идентификатор из ассоциаций ManyToMany, сохраненных в LogEntry. Вот почему функция 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 <[email protected]>
 */
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);
    }
}