diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveAccompanyingPeriodParticipationHandler.php b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveAccompanyingPeriodParticipationHandler.php new file mode 100644 index 000000000..83d8f201a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveAccompanyingPeriodParticipationHandler.php @@ -0,0 +1,35 @@ +getId(), $from->getId(), $to->getId()); + + $deleteSql = sprintf(<<getId()); + + return [$sqlInsert, $deleteSql]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveCenterHistoryHandler.php b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveCenterHistoryHandler.php new file mode 100644 index 000000000..25767d175 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveCenterHistoryHandler.php @@ -0,0 +1,45 @@ +getId(), $from->getId(), $to->getId()); + + $updateSql = sprintf(<<getId()); + } + +} diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveHouseholdHandler.php b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveHouseholdHandler.php new file mode 100644 index 000000000..07035f569 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/Handler/PersonMoveHouseholdHandler.php @@ -0,0 +1,38 @@ +getId(), $from->getId(), $to->getId()); + + $deleteSql = sprintf(<<getId()); + + return [$sqlInsert, $deleteSql]; + } + +} diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php index 76a6f5b8d..b0530d6c3 100644 --- a/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php @@ -35,22 +35,11 @@ use function in_array; */ class PersonMove { - /** - * @var EntityManagerInterface - */ - protected $em; - - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - public function __construct( - EntityManagerInterface $em, - EventDispatcherInterface $eventDispatcher + private EntityManagerInterface $em, + private PersonMoveManager $personMoveManager, + private EventDispatcherInterface $eventDispatcher ) { - $this->em = $em; - $this->eventDispatcher = $eventDispatcher; } /** @@ -88,9 +77,16 @@ class PersonMove } foreach ($metadata->getAssociationMappings() as $field => $mapping) { + + if ($this->personMoveManager->hasHandler($metadata->getName(), $field)) { + $sqls = array_merge($sqls, $this->personMoveManager->getSqls($metadata->getName(), $field, $from, $to)); + continue; + } + if (in_array($mapping['sourceEntity'], $this->getIgnoredEntities(), true)) { continue; } + if (Person::class === $mapping['targetEntity'] and true === $mapping['isOwningSide']) { if (in_array($mapping['sourceEntity'], $toDelete, true)) { $sql = $this->createDeleteSQL($metadata, $from, $field); @@ -101,18 +97,10 @@ class PersonMove ['to' => $to->getId(), 'original_action' => 'move'] ); $this->eventDispatcher->dispatch(ActionEvent::DELETE, $event); + $sqls = array_merge($sqls, $event->getPreSql(), [$event->getSqlStatement()], $event->getPostSql()); } else { - $sql = $this->createMoveSQL($metadata, $from, $to, $field); - $event = new ActionEvent( - $from->getId(), - $metadata->getName(), - $sql, - ['to' => $to->getId(), 'original_action' => 'move'] - ); - $this->eventDispatcher->dispatch(ActionEvent::MOVE, $event); + $sqls = array_merge($sqls, $this->createMoveSQLs($metadata, $from, $to, $field)); } - - $sqls = array_merge($sqls, $event->getPreSql(), [$event->getSqlStatement()], $event->getPostSql()); } } } @@ -150,7 +138,7 @@ class PersonMove ); } - private function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string + private function createMoveSQLs($metadata, Person $from, Person $to, $field): array { $mapping = $metadata->getAssociationMapping($field); @@ -160,9 +148,34 @@ class PersonMove $tableName = ''; if (array_key_exists('joinTable', $mapping)) { + // there is a join_table: we have to find conflict $tableName = (null !== ($mapping['joinTable']['schema'] ?? null) ? $mapping['joinTable']['schema'] . '.' : '') . $mapping['joinTable']['name']; + $sqlInsert = sprintf( + "INSERT INTO %s (%s, %s) SELECT %d, %s FROM %s WHERE %s = %d ON CONFLICT DO NOTHING", + $tableName, + $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id + $mapping['joinTable']['joinColumns'][0]['name'], // something_else_id + $to->getId(), + $mapping['joinTable']['joinColumns'][0]['name'], // something_else_id + $tableName, + $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id + $from->getId() + + ); + $deleteSql = sprintf( + "DELETE FROM %s WHERE %s = %d", + $tableName, + $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id + $from->getId() + + ); + + return [ + $sqlInsert, $deleteSql + ]; + foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) { $sets[] = sprintf('%s = %d', $columns['name'], $to->getId()); } @@ -176,18 +189,17 @@ class PersonMove $sets[] = sprintf('%s = %d', $columns['name'], $to->getId()); } - foreach ($mapping['joinColumns'] as $columns) { $conditions[] = sprintf('%s = %d', $columns['name'], $from->getId()); } } - return sprintf( + return [sprintf( 'UPDATE %s SET %s WHERE %s', $tableName, implode(' ', $sets), implode(' AND ', $conditions) - ); + )]; } /** @@ -198,7 +210,6 @@ class PersonMove { return [ Person\PersonCenterHistory::class, - HouseholdMember::class, AccompanyingPeriodParticipation::class, AccompanyingPeriod\AccompanyingPeriodWork::class, Relationship::class diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveManager.php b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveManager.php new file mode 100644 index 000000000..a7bd1188a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveManager.php @@ -0,0 +1,48 @@ + + */ + private iterable $handlers, + ) + { + } + + /** + * @param class-string $className + * @param string $field + * @return bool + */ + public function hasHandler(string $className, string $field): bool + { + foreach ($this->handlers as $handler) { + if ($handler->supports($className, $field)) { + return true; + } + } + + return false; + } + + /** + * @param class-string $className + * @return array + */ + public function getSqls(string $className, string $field, Person $from, Person $to): array + { + foreach ($this->handlers as $handler) { + if ($handler->supports($className, $field)) { + return $handler->getSqls($className, $field, $from, $to); + } + } + return []; + } + +} diff --git a/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveSqlHandlerInterface.php b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveSqlHandlerInterface.php new file mode 100644 index 000000000..9958f59df --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Actions/Remove/PersonMoveSqlHandlerInterface.php @@ -0,0 +1,19 @@ + + */ + public function getSqls(string $className, string $field, Person $from, Person $to): array; +} diff --git a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php index 4233a7914..43c38aa36 100644 --- a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php +++ b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle; +use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface; use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass; use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface; use Chill\PersonBundle\Widget\PersonListWidgetFactory; @@ -29,5 +30,7 @@ class ChillPersonBundle extends Bundle $container->addCompilerPass(new AccompanyingPeriodTimelineCompilerPass()); $container->registerForAutoconfiguration(AccompanyingPeriodInfoUnionQueryPartInterface::class) ->addTag('chill_person.accompanying_period_info_part'); + $container->registerForAutoconfiguration(PersonMoveSqlHandlerInterface::class) + ->addTag('chill_person.person_move_handler'); } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Action/Remove/PersonMoveTest.php b/src/Bundle/ChillPersonBundle/Tests/Action/Remove/PersonMoveTest.php new file mode 100644 index 000000000..88869bf7d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Action/Remove/PersonMoveTest.php @@ -0,0 +1,138 @@ + + */ + private static $entitiesToDelete = []; + + public function setUp(): void + { + self::bootKernel(); + $this->em = self::$container->get(EntityManagerInterface::class); + $this->personMoveManager = self::$container->get(PersonMoveManager::class); + $this->eventDispatcher = self::$container->get(EventDispatcherInterface::class); + } + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (self::$entitiesToDelete as list($class, $id)) { + $entity = $em->find($class, $id); + + if (null !== $entity) { + $em->remove($entity); + } + } + + $em->flush(); + } + + /** + * @dataProvider dataProviderMovePerson + */ + public function testMovePerson(Person $personA, Person $personB, string $message): void + { + $move = new PersonMove($this->em, $this->personMoveManager, $this->eventDispatcher); + $sqls = $move->getSQL($personA, $personB); + //$conn = $this->em->getConnection(); + $this->em->getConnection()->transactional(function (Connection $conn) use ($personA, $personB, $sqls) { + foreach ($sqls as $sql) { + $conn->executeStatement($sql); + } + }); + + $personA = $this->em->find(Person::class, $personA->getId()); + $personB = $this->em->find(Person::class, $personB->getId()); + + self::assertNull($personA?->getId(), $message); + self::assertNotNull($personB?->getId(), $message); + } + + public function dataProviderMovePerson(): iterable + { + $this->setUp(); + + $personA = new Person(); + $personB = new Person(); + + $this->em->persist($personA); + $this->em->persist($personB); + + self::$entitiesToDelete[] = [Person::class, $personA]; + self::$entitiesToDelete[] = [Person::class, $personB]; + + yield [$personA, $personB, "move 2 people without any associated data"]; + + $personA = new Person(); + $personB = new Person(); + + $activity = new Activity(); + $activity->setDate(new \DateTime('today')); + $activity->addPerson($personA); + $activity->addPerson($personB); + + $this->em->persist($personA); + $this->em->persist($personB); + $this->em->persist($activity); + + self::$entitiesToDelete[] = [Person::class, $personA]; + self::$entitiesToDelete[] = [Person::class, $personB]; + self::$entitiesToDelete[] = [Activity::class, $activity]; + + yield [$personA, $personB, "move 2 people having an activity"]; + + $personA = new Person(); + $personB = new Person(); + $household = new Household(); + $household->addMember( + $memberA = (new HouseholdMember())->setPerson($personA)->setShareHousehold(true) + ->setStartDate(new \DateTimeImmutable('2023-01-01')) + ); + $household->addMember( + $memberB = (new HouseholdMember())->setPerson($personB)->setShareHousehold(true) + ->setStartDate(new \DateTimeImmutable('2023-01-01')) + ); + + + + $this->em->persist($personA); + $this->em->persist($personB); + $this->em->persist($household); + $this->em->persist($memberA); + $this->em->persist($memberB); + + self::$entitiesToDelete[] = [Person::class, $personA]; + self::$entitiesToDelete[] = [Person::class, $personB]; + self::$entitiesToDelete[] = [HouseholdMember::class, $memberA]; + self::$entitiesToDelete[] = [HouseholdMember::class, $memberB]; + self::$entitiesToDelete[] = [Household::class, $household]; + + yield [$personA, $personB, "move 2 people having the same household at the same time"]; + + $this->em->flush(); + $this->em->clear(); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/actions.yaml b/src/Bundle/ChillPersonBundle/config/services/actions.yaml index e4c6c6621..d6e2c80a5 100644 --- a/src/Bundle/ChillPersonBundle/config/services/actions.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/actions.yaml @@ -1,5 +1,13 @@ services: - Chill\PersonBundle\Actions\Remove\PersonMove: + _defaults: + autowire: true + autoconfigure: true + + Chill\PersonBundle\Actions\Remove\PersonMove: ~ + + Chill\PersonBundle\Actions\Remove\PersonMoveManager: arguments: - $em: '@Doctrine\ORM\EntityManagerInterface' - $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' \ No newline at end of file + $handlers: !tagged_iterator chill_person.person_move_handler + + Chill\PersonBundle\Actions\Remove\Handler\: + resource: '../../Actions/Remove/Handler'