improve command move

- allow to delete some entities instead of moving them ;
- trigger event on move / delete entities in order to customize sql
statements
This commit is contained in:
Julien Fastré 2019-08-28 13:39:35 +02:00
parent 0ccc998e52
commit 1d1bafe3d9
6 changed files with 260 additions and 13 deletions

143
Actions/ActionEvent.php Normal file
View File

@ -0,0 +1,143 @@
<?php
/*
* Copyright (C) 2016-2019 Champs-Libres <info@champs-libres.coop>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
/* /*
* Copyright (C) 20169 Champs-Libres <info@champs-libres.coop> * Copyright (C) 2016-2019 Champs-Libres <info@champs-libres.coop>
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\Mapping\ClassMetadata; 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 class PersonMove
@ -34,14 +41,51 @@ class PersonMove
*/ */
protected $em; protected $em;
public function __construct(EntityManagerInterface $em) /**
{ *
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
public function __construct(
EntityManagerInterface $em,
EventDispatcherInterface $eventDispatcher
) {
$this->em = $em; $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 = []; $sqls = [];
$toDelete = \array_merge($deleteEntities, $this->getDeleteEntities());
foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) { foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
if ($metadata->isMappedSuperclass) { if ($metadata->isMappedSuperclass) {
@ -50,11 +94,21 @@ class PersonMove
foreach ($metadata->getAssociationMappings() as $field => $mapping) { foreach ($metadata->getAssociationMappings() as $field => $mapping) {
if ($mapping['targetEntity'] === Person::class) { 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 { } 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 * @return array
*/ */
protected function deleteEntities(): array protected function getDeleteEntities(): array
{ {
return [ return [
AccompanyingPeriod::class AccompanyingPeriod::class

View File

@ -47,3 +47,4 @@ Branche master
- fix error on macro renderPerson / withLink not taken into account - fix error on macro renderPerson / withLink not taken into account
- add a link between accompanying person and user - 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 - 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

View File

@ -1,4 +1,20 @@
<?php <?php
/*
* Copyright (C) 2016-2019 Champs-Libres <info@champs-libres.coop>
*
* 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 <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\Command; namespace Chill\PersonBundle\Command;
@ -11,6 +27,7 @@ use Chill\PersonBundle\Actions\Remove\PersonMove;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
class ChillPersonMoveCommand extends ContainerAwareCommand class ChillPersonMoveCommand extends ContainerAwareCommand
{ {
@ -26,14 +43,22 @@ class ChillPersonMoveCommand extends ContainerAwareCommand
*/ */
protected $em; protected $em;
/**
*
* @var LoggerInterface
*/
protected $chillLogger;
public function __construct( public function __construct(
PersonMove $mover, PersonMove $mover,
EntityManagerInterface $em EntityManagerInterface $em,
LoggerInterface $chillLogger
) { ) {
parent::__construct('chill:person:move'); parent::__construct('chill:person:move');
$this->mover = $mover; $this->mover = $mover;
$this->em = $em; $this->em = $em;
$this->chillLogger = $chillLogger;
} }
protected function configure() 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('to', 't', InputOption::VALUE_REQUIRED, "The person id which will received data")
->addOption('dump-sql', null, InputOption::VALUE_NONE, "dump sql to stdout") ->addOption('dump-sql', null, InputOption::VALUE_NONE, "dump sql to stdout")
->addOption('force', null, InputOption::VALUE_NONE, "execute sql instead of dumping it") ->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) 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\""; $msg = "You must use \"--dump-sql\" or \"--force\"";
throw new RuntimeException($msg); throw new RuntimeException($msg);
} }
@ -72,6 +98,7 @@ class ChillPersonMoveCommand extends ContainerAwareCommand
$repository = $this->em->getRepository(Person::class); $repository = $this->em->getRepository(Person::class);
$from = $repository->find($input->getOption('from')); $from = $repository->find($input->getOption('from'));
$to = $repository->find($input->getOption('to')); $to = $repository->find($input->getOption('to'));
$deleteEntities = $input->getOption('delete-entity');
if ($from === NULL) { if ($from === NULL) {
throw new RuntimeException(sprintf("Person \"from\" with id %d not found", $input->getOption('from'))); 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'))); 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')) { if ($input->getOption('dump-sql')) {
foreach($sqls as $sql) { foreach($sqls as $sql) {
$output->writeln($sql); $output->writeln($sql);
} }
} else { } 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 = $this->em->getConnection();
$connection->beginTransaction(); $connection->beginTransaction();
foreach($sqls as $sql) { foreach($sqls as $sql) {
@ -97,7 +126,25 @@ class ChillPersonMoveCommand extends ContainerAwareCommand
} }
$connection->commit(); $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;
}
} }

View File

@ -2,3 +2,4 @@ services:
Chill\PersonBundle\Actions\Remove\PersonMove: Chill\PersonBundle\Actions\Remove\PersonMove:
arguments: arguments:
$em: '@Doctrine\ORM\EntityManagerInterface' $em: '@Doctrine\ORM\EntityManagerInterface'
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'

View File

@ -13,5 +13,6 @@ services:
arguments: arguments:
$em: '@Doctrine\ORM\EntityManagerInterface' $em: '@Doctrine\ORM\EntityManagerInterface'
$mover: '@Chill\PersonBundle\Actions\Remove\PersonMove' $mover: '@Chill\PersonBundle\Actions\Remove\PersonMove'
$chillLogger: '@chill.main.logger'
tags: tags:
- { name: console.command } - { name: console.command }