diff --git a/Actions/Remove/PersonMove.php b/Actions/Remove/PersonMove.php new file mode 100644 index 000000000..112d8daa4 --- /dev/null +++ b/Actions/Remove/PersonMove.php @@ -0,0 +1,130 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Actions\Remove; + +use Doctrine\ORM\EntityManagerInterface; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Doctrine\ORM\Mapping\ClassMetadata; + +/** + * Move all person to a new one, and delete the old record. + * + */ +class PersonMove +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + public function getSQL(Person $from, Person $to) + { + $sqls = []; + + foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) { + if ($metadata->isMappedSuperclass) { + continue; + } + + foreach ($metadata->getAssociationMappings() as $field => $mapping) { + if ($mapping['targetEntity'] === Person::class) { + if (\in_array($metadata->getName(), $this->deleteEntities())) { + $sqls[] = $this->createDeleteSQL($metadata, $from, $field); + } else { + $sqls[] = $this->createMoveSQL($metadata, $from, $to, $field); + } + } + } + } + + $personMetadata = $this->em->getClassMetadata(Person::class); + $sqls[] = sprintf("DELETE FROM %s WHERE id = %d", + $this->getTableName($personMetadata), + $from->getId()); + + return $sqls ?? []; + } + + protected function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string + { + $mapping = $metadata->getAssociationMapping($field); + + // Set part of the query, aka in "UPDATE table SET " + $sets = []; + foreach ($mapping["joinColumns"] as $columns) { + $sets[] = sprintf("%s = %d", $columns["name"], $to->getId()); + } + + $conditions = []; + foreach ($mapping["joinColumns"] as $columns) { + $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId()); + } + + return \sprintf("UPDATE %s SET %s WHERE %s", + $this->getTableName($metadata), + \implode(" ", $sets), + \implode(" AND ", $conditions) + ); + } + + protected function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string + { + $mapping = $metadata->getAssociationMapping($field); + + $conditions = []; + foreach ($mapping["joinColumns"] as $columns) { + $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId()); + } + + return \sprintf("DELETE FROM %s WHERE %s", + $this->getTableName($metadata), + \implode(" AND ", $conditions) + ); + } + + /** + * return an array of classes where entities should be deleted + * instead of moved + * + * @return array + */ + protected function deleteEntities(): array + { + return [ + AccompanyingPeriod::class + ]; + } + + /** + * get the full table name with schema if it does exists + */ + private function getTableName(ClassMetadata $metadata): string + { + return empty($metadata->getSchemaName()) ? + $metadata->getTableName() : + $metadata->getSchemaName().".".$metadata->getTableName(); + } + +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8755d02..4266450b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,3 +39,5 @@ Branche master ============== - Update address validation +- Add command to move person and all data of a person to a new one, and delete the old one. + diff --git a/Command/ChillPersonMoveCommand.php b/Command/ChillPersonMoveCommand.php new file mode 100644 index 000000000..6ab2c8979 --- /dev/null +++ b/Command/ChillPersonMoveCommand.php @@ -0,0 +1,103 @@ +mover = $mover; + $this->em = $em; + } + + protected function configure() + { + $this + ->setName('chill:person:move') + ->setDescription('Move all the associated entities on a "from" person to a "to" person and remove the old person') + ->addOption('from', 'f', InputOption::VALUE_REQUIRED, "The person id to delete, all associated data will be moved before") + ->addOption('to', 't', InputOption::VALUE_REQUIRED, "The person id which will received data") + ->addOption('dump-sql', null, InputOption::VALUE_NONE, "dump sql to stdout") + ->addOption('force', null, InputOption::VALUE_NONE, "execute sql instead of dumping it") + ; + } + + protected function interact(InputInterface $input, OutputInterface $output) + { + if (FALSE === ($input->hasOption('dump-sql') || $input->hasOption('force'))) { + $msg = "You must use \"--dump-sql\" or \"--force\""; + throw new RuntimeException($msg); + } + + foreach (["from", "to"] as $name) { + if (empty($input->getOption($name))) { + throw new RuntimeException("You must set a \"$name\" option"); + } + $id = $input->getOption($name); + if (\ctype_digit($id) === FALSE) { + throw new RuntimeException("The id in \"$name\" field does not contains " + . "only digits: $id"); + } + } + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $repository = $this->em->getRepository(Person::class); + $from = $repository->find($input->getOption('from')); + $to = $repository->find($input->getOption('to')); + + if ($from === NULL) { + throw new RuntimeException(sprintf("Person \"from\" with id %d not found", $input->getOption('from'))); + } + if ($to === NULL) { + throw new RuntimeException(sprintf("Person \"to\" with id %d not found", $input->getOption('to'))); + } + + $sqls = $this->mover->getSQL($from, $to); + + if ($input->getOption('dump-sql')) { + foreach($sqls as $sql) { + $output->writeln($sql); + } + } else { + $connection = $this->em->getConnection(); + $connection->beginTransaction(); + foreach($sqls as $sql) { + if ($output->isVerbose()) { + $output->writeln($sql); + } + $connection->executeQuery($sql); + } + $connection->commit(); + + } + } + +} diff --git a/DependencyInjection/ChillPersonExtension.php b/DependencyInjection/ChillPersonExtension.php index 4034af894..41a7ab71f 100644 --- a/DependencyInjection/ChillPersonExtension.php +++ b/DependencyInjection/ChillPersonExtension.php @@ -64,6 +64,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/menu.yml'); $loader->load('services/privacyEvent.yml'); $loader->load('services/command.yml'); + $loader->load('services/actions.yml'); if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') { $loader->load('services/exports_accompanying_period.yml'); diff --git a/Resources/config/services/actions.yml b/Resources/config/services/actions.yml new file mode 100644 index 000000000..22b50b1f8 --- /dev/null +++ b/Resources/config/services/actions.yml @@ -0,0 +1,4 @@ +services: + Chill\PersonBundle\Actions\Remove\PersonMove: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' \ No newline at end of file diff --git a/Resources/config/services/command.yml b/Resources/config/services/command.yml index c99e488a4..fec8bdfd4 100644 --- a/Resources/config/services/command.yml +++ b/Resources/config/services/command.yml @@ -8,3 +8,10 @@ services: $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' tags: - { name: console.command } + + Chill\PersonBundle\Command\ChillPersonMoveCommand: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + $mover: '@Chill\PersonBundle\Actions\Remove\PersonMove' + tags: + - { name: console.command }