ORO crm 核心实体中的 ManyToMany 自引用关联

ORO crm ManyToMany self reference association in core entity

想就当前更新的解决方案征求意见。几个条件和用例:

  1. Friends 从 UserBundle
  2. 扩展了核心 UserEntity 的 manytomany属性
  3. 当我们添加好友时,所有者实体也应该出现在子实体中
  4. 当我们删除朋友时,朋友也应该从拥有方的关联中解脱出来

我的解决方案是在onFlush上使用事件订阅器,也许有人有更好的方法?

<?php

namespace App\Bundle\UserBundle\Form\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Oro\Bundle\UserBundle\Entity\User;
use Doctrine\ORM\Event\OnFlushEventArgs;

class UserEventSubscriber implements EventSubscriber
{
    /**
     * @return array
     */
    public function getSubscribedEvents(): array
    {
        return [
            Events::onFlush,
        ];
    }

    /**
     * @param OnFlushEventArgs $args
     */
    public function onFlush(OnFlushEventArgs $args): void
    {
        $uow = $args->getEntityManager()->getUnitOfWork();

        $updates = array_merge(
            $uow->getScheduledCollectionUpdates(),
            $uow->getScheduledCollectionDeletions()
        );

        array_walk(
            $updates,
            fn (PersistentCollection $collection) => $this->processFriendsOnFlush($collection, $args)
        );
    }

    /**
     * @param PersistentCollection $collection
     * @param OnFlushEventArgs     $args
     */
    private function processFriendsOnFlush(PersistentCollection $collection, OnFlushEventArgs $args): void
    {
        $mapping = $collection->getMapping();

        if ($collection->getOwner() instanceof User && $mapping['fieldName'] === 'friends')
        {
            $entityManager = $args->getEntityManager();
            /** @var User $ownerUser */
            $ownerUser = $collection->getOwner();
            $snapshotFriends = $collection->getSnapshot();
            $updatedFriends = $collection->getValues();

            if (count($updatedFriends) > 0 && $snapshotFriends !== $updatedFriends) {
                array_walk($updatedFriends, fn(User $friend) => $this->bindFriendAssociation($entityManager, $ownerUser, $friend));
            }

            if (count($snapshotFriends) > 0) {
                array_walk($snapshotFriends, fn(User $friend) => $this->removeFriendAssociation($entityManager, $ownerUser, $friend));
            }
        }
    }

    /**
     * Bind friend association to keep bidirectional relation on adding new friend user
     *
     * @param EntityManager $entityManager
     * @param User          $ownerUser
     * @param User          $friend
     *
     * @throws \Doctrine\ORM\ORMException
     */
    private function bindFriendAssociation(EntityManager $entityManager, User $ownerUser, User $friend): void
    {
        if (!$friend->getFriends()->contains($ownerUser)) {
            $friend->addFriend($ownerUser);
            $entityManager->persist($friend);
            $entityManager->getUnitOfWork()->computeChangeSet($entityManager->getClassMetadata(get_class($friend)), $friend);
        }
    }

    /**
     * Remove friend user association to keep bidirectional relation on remove friend user from child
     *
     * @param EntityManager $entityManager
     * @param User          $ownerUser
     * @param User          $friend
     *
     * @throws \Doctrine\ORM\ORMException
     */
    private function removeFriendAssociation(EntityManager $entityManager, User $ownerUser, User $friend): void
    {
        if ($friend->getFriends()->contains($ownerUser)) {
            $friend->removeFriend($ownerUser);
            $entityManager->persist($friend);
            $entityManager->getUnitOfWork()->computeChangeSet($entityManager->getClassMetadata(get_class($friend)), $friend);
        }
    }
}


与我添加 ManyToMany 关联的方式相同:

$this->extendExtension->addManyToManyRelation(
            $schema,
            $schema->getTable('oro_user'),
            'friends',
            $schema->getTable('oro_user'),
            ['last_name', 'first_name'],
            ['last_name', 'first_name'],
            ['last_name', 'first_name'],
            [
                'extend' => [
                    'owner' => ExtendScope::OWNER_CUSTOM,
                    'target_title' => ['id'],
                    'target_detailed' => ['id'],
                    'target_grid' => ['id'],
                    'cascade' => ['persist'],
                ],
                'dataaudit' => ['auditable' => true],
            ]
        );

确保关系是双向的,并且您已在实体构造函数中为两个属性初始化 ArrayCollection,如文档所述: https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-bidirectional.

并且您有 add*remove* 方法并正确实施,例如:

    public function addFriend(User $user)
    {
        if (!$this->friends->contains($user)) {
            $this->friends->add($user);
            $user->addFriend($this);
        }

        return $this;
    }

    public function removeFriend(User $user)
    {
        if ($this->friends->contains($user)) {
            $this->friends->removeElement($user);
            $user->removeFriend($this);
        }

        return $this;
    }

如果是OroPlatform生成的代码,你可以通过写一个自定义来影响它Oro\Bundle\EntityExtendBundle\Tools\GeneratorExtensions\AbstractEntityGeneratorExtension。一个很好的例子是 Oro\Bundle\EntitySerializedFieldsBundle\Tools\GeneratorExtensions\SerializedDataGeneratorExtension.

对你来说,它会是这样的:

# ...
public function generate(array $schema, ClassGenerator $class): void
{
    $method = $class->getMethod('addFriend');
    $method->setBody('if (!$this->friends->contains($user)) {
        $this->friends->add($user);
        $user->addFriend($this);
    }');

    $method = $class->getMethod('removeFriend');
    $method->setBody('if ($this->friends->contains($user)) {
        $this->friends->removeElement($user);
        $user->removeFriend($this);
    }');
}
# ...

并且不要忘记使用 oro_entity_extend.entity_generator_extension 标签 register the generator in a service container