diff --git a/Actions/ActionEvent.php b/Actions/ActionEvent.php new file mode 100644 index 000000000..5ac3055cb --- /dev/null +++ b/Actions/ActionEvent.php @@ -0,0 +1,143 @@ + + * + * 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; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Event triggered when an entity attached to a person is removed. + * + * + */ +class ActionEvent extends Event +{ + const DELETE = 'CHILL_PERSON.DELETE_ASSOCIATED_ENTITY'; + const MOVE = 'CHILL_PERSON.MOVE_ASSOCIATED_ENTITY'; + + /** + * + * @var int + */ + protected $personId; + + /** + * the FQDN class name as recorded in doctrine + * + * @var string + */ + protected $entity; + + /** + * an array of key value data to describe the movement + * + * @var array + */ + protected $metadata; + + /** + * the sql statement + * + * @var string + */ + protected $sqlStatement; + + /** + * + * @var string[] + */ + protected $preSql = []; + + /** + * + * @var string[] + */ + protected $postSql = []; + + public function __construct($personId, $entity, $sqlStatement, $metadata = []) + { + $this->personId = $personId; + $this->entity = $entity; + $this->sqlStatement = $sqlStatement; + $this->metadata = $metadata; + } + + /** + * + * @return string[] + */ + public function getPreSql(): array + { + return $this->preSql; + } + + /** + * + * @return string[] + */ + public function getPostSql(): array + { + return $this->postSql; + } + + /* + * Add Sql which will be executed **before** the delete statement + */ + public function addPreSql(string $preSql) + { + $this->preSql[] = $preSql; + return $this; + } + + /** + * Add Sql which will be executed **after** the delete statement + * + * @param type $postSql + * @return $this + */ + public function addPostSql(string $postSql) + { + $this->postSql[] = $postSql; + return $this; + } + + public function getPersonId(): int + { + return $this->personId; + } + + /** + * get the entity name, as recorded in doctrine + * + * @return string + */ + public function getEntity(): string + { + return $this->entity; + } + + public function getSqlStatement() + { + return $this->sqlStatement; + } + + public function getMetadata() + { + return $this->metadata; + } + +} diff --git a/Actions/Remove/PersonMove.php b/Actions/Remove/PersonMove.php index 112d8daa4..fb7e6b9b4 100644 --- a/Actions/Remove/PersonMove.php +++ b/Actions/Remove/PersonMove.php @@ -1,6 +1,6 @@ + * Copyright (C) 2016-2019 Champs-Libres * * 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 @@ -21,9 +21,16 @@ use Doctrine\ORM\EntityManagerInterface; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Chill\PersonBundle\Actions\ActionEvent; /** - * Move all person to a new one, and delete the old record. + * Move or delete entities associated to a person to a new one, and delete the + * old person. The data associated to a person (birthdate, name, ...) are left + * untouched on the "new one". + * + * See `getSql` for details. + * * */ class PersonMove @@ -34,14 +41,51 @@ class PersonMove */ protected $em; - public function __construct(EntityManagerInterface $em) - { + /** + * + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + public function __construct( + EntityManagerInterface $em, + EventDispatcherInterface $eventDispatcher + ) { $this->em = $em; + $this->eventDispatcher = $eventDispatcher; } - public function getSQL(Person $from, Person $to) + /** + * Return the sql used to move or delete entities associated to a person to + * a new one, and delete the old person. The data associated to a person + * (birthdate, name, ...) are left untouched on the "new one". + * + * The accompanying periods associated to a person are always removed. The other + * associated entity are updated: the new person id is associated to the entity. + * + * Optionnaly, you can ask for removing entity by passing them in $deleteEntities + * parameters. + * + * The following events are triggered: + * - `'CHILL_PERSON.DELETE_ASSOCIATED_ENTITY'` is triggered when an entity + * will be removed ; + * - `'CHILL_PERSON.MOVE_ASSOCIATED_ENTITY'` is triggered when an entity + * will be moved ; + * + * Those events have the following metadata: + * + * - 'original_action' : always 'move' ; + * - 'to': the person id to move ; + * + * @param Person $from + * @param Person $to + * @param array $deleteEntities + * @return type + */ + public function getSQL(Person $from, Person $to, array $deleteEntities = []) { $sqls = []; + $toDelete = \array_merge($deleteEntities, $this->getDeleteEntities()); foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) { if ($metadata->isMappedSuperclass) { @@ -50,11 +94,21 @@ class PersonMove foreach ($metadata->getAssociationMappings() as $field => $mapping) { if ($mapping['targetEntity'] === Person::class) { - if (\in_array($metadata->getName(), $this->deleteEntities())) { - $sqls[] = $this->createDeleteSQL($metadata, $from, $field); + + if (\in_array($metadata->getName(), $toDelete)) { + $sql = $this->createDeleteSQL($metadata, $from, $field); + $event = new ActionEvent($from->getId(), $metadata->getName(), $sql, + ['to' => $to->getId(), 'original_action' => 'move']); + $this->eventDispatcher->dispatch(ActionEvent::DELETE, $event); + } else { - $sqls[] = $this->createMoveSQL($metadata, $from, $to, $field); + $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, $event->getPreSql(), [$event->getSqlStatement()], $event->getPostSql()); } } } @@ -110,7 +164,7 @@ class PersonMove * * @return array */ - protected function deleteEntities(): array + protected function getDeleteEntities(): array { return [ AccompanyingPeriod::class diff --git a/CHANGELOG.md b/CHANGELOG.md index 298b375a6..3b1d70e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,3 +47,4 @@ Branche master - fix error on macro renderPerson / withLink not taken into account - add a link between accompanying person and user - add an icon when the file is opened / closed in result list, and in person rendering macro +- improve command to move person and all data: allow to delete some entities during move and add events diff --git a/Command/ChillPersonMoveCommand.php b/Command/ChillPersonMoveCommand.php index 6ab2c8979..4f2b2098a 100644 --- a/Command/ChillPersonMoveCommand.php +++ b/Command/ChillPersonMoveCommand.php @@ -1,4 +1,20 @@ + * + * 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\Command; @@ -11,6 +27,7 @@ use Chill\PersonBundle\Actions\Remove\PersonMove; use Doctrine\ORM\EntityManagerInterface; use Chill\PersonBundle\Entity\Person; use Symfony\Component\Console\Exception\RuntimeException; +use Psr\Log\LoggerInterface; class ChillPersonMoveCommand extends ContainerAwareCommand { @@ -26,14 +43,22 @@ class ChillPersonMoveCommand extends ContainerAwareCommand */ protected $em; + /** + * + * @var LoggerInterface + */ + protected $chillLogger; + public function __construct( PersonMove $mover, - EntityManagerInterface $em + EntityManagerInterface $em, + LoggerInterface $chillLogger ) { parent::__construct('chill:person:move'); $this->mover = $mover; $this->em = $em; + $this->chillLogger = $chillLogger; } protected function configure() @@ -45,12 +70,13 @@ class ChillPersonMoveCommand extends ContainerAwareCommand ->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") + ->addOption('delete-entity', null, InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, "entity to delete", []) ; } protected function interact(InputInterface $input, OutputInterface $output) { - if (FALSE === ($input->hasOption('dump-sql') || $input->hasOption('force'))) { + if (FALSE === $input->hasOption('dump-sql') && FALSE === $input->hasOption('force')) { $msg = "You must use \"--dump-sql\" or \"--force\""; throw new RuntimeException($msg); } @@ -72,6 +98,7 @@ class ChillPersonMoveCommand extends ContainerAwareCommand $repository = $this->em->getRepository(Person::class); $from = $repository->find($input->getOption('from')); $to = $repository->find($input->getOption('to')); + $deleteEntities = $input->getOption('delete-entity'); if ($from === NULL) { throw new RuntimeException(sprintf("Person \"from\" with id %d not found", $input->getOption('from'))); @@ -80,13 +107,15 @@ class ChillPersonMoveCommand extends ContainerAwareCommand throw new RuntimeException(sprintf("Person \"to\" with id %d not found", $input->getOption('to'))); } - $sqls = $this->mover->getSQL($from, $to); + $sqls = $this->mover->getSQL($from, $to, $deleteEntities); if ($input->getOption('dump-sql')) { foreach($sqls as $sql) { $output->writeln($sql); } } else { + $ctxt = $this->buildLoggingContext($from, $to, $deleteEntities, $sqls); + $this->chillLogger->notice("Trying to move a person from command line", $ctxt); $connection = $this->em->getConnection(); $connection->beginTransaction(); foreach($sqls as $sql) { @@ -97,7 +126,25 @@ class ChillPersonMoveCommand extends ContainerAwareCommand } $connection->commit(); + $this->chillLogger->notice("Move a person from command line succeeded", $ctxt); } } + + protected function buildLoggingContext(Person $from, Person $to, $deleteEntities, $sqls) + { + $ctxt = [ + 'from' => $from->getId(), + 'to' => $to->getId() + ]; + + foreach ($deleteEntities as $key => $de) { + $ctxt['delete_entity_'.$key] = $de; + } + foreach ($sqls as $key => $sql) { + $ctxt['sql_'.$key] = $sql; + } + + return $ctxt; + } } diff --git a/Resources/config/services/actions.yml b/Resources/config/services/actions.yml index 22b50b1f8..e4c6c6621 100644 --- a/Resources/config/services/actions.yml +++ b/Resources/config/services/actions.yml @@ -1,4 +1,5 @@ services: Chill\PersonBundle\Actions\Remove\PersonMove: arguments: - $em: '@Doctrine\ORM\EntityManagerInterface' \ No newline at end of file + $em: '@Doctrine\ORM\EntityManagerInterface' + $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' \ No newline at end of file diff --git a/Resources/config/services/command.yml b/Resources/config/services/command.yml index fec8bdfd4..59a2f984a 100644 --- a/Resources/config/services/command.yml +++ b/Resources/config/services/command.yml @@ -13,5 +13,6 @@ services: arguments: $em: '@Doctrine\ORM\EntityManagerInterface' $mover: '@Chill\PersonBundle\Actions\Remove\PersonMove' + $chillLogger: '@chill.main.logger' tags: - { name: console.command }