Wanted to ask opinion on solution for current update. Few conditions and use case:
My solution is to use Event subscriber on onFlush even, maybe anyone has a better approach?
<?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);
}
}
}
Same how I add ManyToMany association:
$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],
]
);
Make sure the relation is bidirectional, and you have initialized the ArrayCollection for both properties in a entity constructor, as the documentation states: https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-bidirectional.
And you have add*
, remove*
methods with the proper implementation, like:
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;
}
If it's the code generated by OroPlatform, you can affect it by writing a custom Oro\Bundle\EntityExtendBundle\Tools\GeneratorExtensions\AbstractEntityGeneratorExtension
. A good example is Oro\Bundle\EntitySerializedFieldsBundle\Tools\GeneratorExtensions\SerializedDataGeneratorExtension.
For you, it would be something like:
# ...
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);
}');
}
# ...
And don't forget to register the generator in a service container with the oro_entity_extend.entity_generator_extension
tag.