From 5ddc0e7a53b390b6e176798b65afdab7ab5ade13 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 22 Jun 2021 15:31:49 +0200 Subject: [PATCH 01/11] Simplify loading of Symfony commands. --- .../Command/ChillPersonMoveCommand.php | 71 +-- .../Command/ImportPeopleFromCSVCommand.php | 484 ++++++++---------- .../ChillPersonExtension.php | 1 - .../ChillPersonBundle/config/services.yaml | 7 + .../config/services/command.yaml | 19 - 5 files changed, 246 insertions(+), 336 deletions(-) delete mode 100644 src/Bundle/ChillPersonBundle/config/services/command.yaml diff --git a/src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php b/src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php index 4f2b2098a..b68997ed6 100644 --- a/src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php +++ b/src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php @@ -1,25 +1,7 @@ - * - * 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; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -28,39 +10,28 @@ use Doctrine\ORM\EntityManagerInterface; use Chill\PersonBundle\Entity\Person; use Symfony\Component\Console\Exception\RuntimeException; use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; -class ChillPersonMoveCommand extends ContainerAwareCommand +final class ChillPersonMoveCommand extends Command { - /** - * - * @var PersonMove - */ - protected $mover; - - /** - * - * @var EntityManagerInterface - */ - protected $em; - - /** - * - * @var LoggerInterface - */ - protected $chillLogger; - + private PersonMove $mover; + + private EntityManagerInterface $em; + + private LoggerInterface $chillLogger; + public function __construct( - PersonMove $mover, + PersonMove $mover, EntityManagerInterface $em, LoggerInterface $chillLogger ) { parent::__construct('chill:person:move'); - + $this->mover = $mover; $this->em = $em; $this->chillLogger = $chillLogger; } - + protected function configure() { $this @@ -73,14 +44,14 @@ class ChillPersonMoveCommand extends ContainerAwareCommand ->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') && FALSE === $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"); @@ -90,7 +61,7 @@ class ChillPersonMoveCommand extends ContainerAwareCommand throw new RuntimeException("The id in \"$name\" field does not contains " . "only digits: $id"); } - } + } } protected function execute(InputInterface $input, OutputInterface $output) @@ -99,16 +70,16 @@ class ChillPersonMoveCommand extends ContainerAwareCommand $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'))); } if ($to === NULL) { throw new RuntimeException(sprintf("Person \"to\" with id %d not found", $input->getOption('to'))); } - + $sqls = $this->mover->getSQL($from, $to, $deleteEntities); - + if ($input->getOption('dump-sql')) { foreach($sqls as $sql) { $output->writeln($sql); @@ -125,25 +96,25 @@ class ChillPersonMoveCommand extends ContainerAwareCommand $connection->executeQuery($sql); } $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/src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php b/src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php index d5e4c597c..35f596295 100644 --- a/src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php +++ b/src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php @@ -1,22 +1,5 @@ - * - * 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; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -40,65 +23,37 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormFactoryInterface; -/** - * Class ImportPeopleFromCSVCommand - * - * @package Chill\PersonBundle\Command - * @author Julien Fastré - */ -class ImportPeopleFromCSVCommand extends Command +final class ImportPeopleFromCSVCommand extends Command { + private InputInterface $input; + + private OutputInterface $output; + + private LoggerInterface $logger; + + private TranslatableStringHelper $helper; + + private EntityManagerInterface $em; + + private EventDispatcherInterface $eventDispatcher; + /** - * @var InputInterface + * The line currently read */ - protected $input; - + private int $line; + /** - * @var OutputInterface + * Where key are column names, and value the custom field slug */ - protected $output; - - /** - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * @var \Chill\MainBundle\Templating\TranslatableStringHelper - */ - protected $helper; - - /** - * @var \Doctrine\Persistence\ObjectManager - */ - protected $em; - - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - - /** - * the line currently read - * - * @var int - */ - protected $line; - - /** - * @var array where key are column names, and value the custom field slug - */ - protected $customFieldMapping = array(); - - /** - * @var CustomFieldProvider - */ - protected $customFieldProvider; - + private array $customFieldMapping = []; + + private CustomFieldProvider $customFieldProvider; + /** * Contains an array of information searched in the file. - * + * * position 0: the information key (which will be used in this process) * position 1: the helper * position 2: the default value @@ -121,19 +76,16 @@ class ImportPeopleFromCSVCommand extends Command ['locality', 'The column header for locality', 'locality'], ['center', 'The column header for center', 'center'] ); - + /** * Different possible format to interpret a date * * @var string */ protected static $defaultDateInterpreter = "%d/%m/%Y|%e/%m/%y|%d/%m/%Y|%e/%m/%Y"; - - /** - * @var FormFactory - */ - protected $formFactory; - + + private FormFactoryInterface $formFactory; + /** * ImportPeopleFromCSVCommand constructor. * @@ -150,7 +102,7 @@ class ImportPeopleFromCSVCommand extends Command EntityManagerInterface $em, CustomFieldProvider $customFieldProvider, EventDispatcherInterface $eventDispatcher, - FormFactory $formFactory + FormFactoryInterface $formFactory ) { $this->logger = $logger; $this->helper = $helper; @@ -158,10 +110,10 @@ class ImportPeopleFromCSVCommand extends Command $this->customFieldProvider = $customFieldProvider; $this->eventDispatcher = $eventDispatcher; $this->formFactory = $formFactory; - + parent::__construct('chill:person:import'); } - + /** * */ @@ -171,10 +123,10 @@ class ImportPeopleFromCSVCommand extends Command ->addArgument('csv_file', InputArgument::REQUIRED, "The CSV file to import") ->setDescription("Import people from a csv file") ->setHelp(<<addArgument('locale', InputArgument::REQUIRED, + ->addArgument('locale', InputArgument::REQUIRED, "The locale to use in displaying translatable strings from entities") ->addOption( - 'force-center', - null, + 'force-center', + null, InputOption::VALUE_REQUIRED, "The id of the center" ) @@ -197,10 +149,10 @@ EOF "Persist people in the database (default is not to persist people)" ) ->addOption( - 'delimiter', - 'd', - InputOption::VALUE_OPTIONAL, - "The delimiter character of the csv file", + 'delimiter', + 'd', + InputOption::VALUE_OPTIONAL, + "The delimiter character of the csv file", ",") ->addOption( 'enclosure', @@ -236,33 +188,33 @@ EOF "The path of the file to load the matching between label in CSV and answers" ) ; - + // mapping columns foreach (self::$mapping as $m) { $this->addOptionShortcut($m[0], $m[1], $m[2]); } - + // other information $this->addOptionShortcut('birthdate_format', 'Format preference for ' - . 'birthdate. See help for date formats preferences.', + . 'birthdate. See help for date formats preferences.', self::$defaultDateInterpreter); $this->addOptionShortcut('opening_date_format', 'Format preference for ' . 'opening date. See help for date formats preferences.', self::$defaultDateInterpreter); $this->addOptionShortcut('closing_date_format', 'Format preference for ' - . 'closing date. See help for date formats preferences.', + . 'closing date. See help for date formats preferences.', self::$defaultDateInterpreter); - + // mapping column to custom fields - $this->addOption('custom-field', NULL, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + $this->addOption('custom-field', NULL, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, "Mapping a column to a custom fields key. Example: 1=cf_slug"); $this->addOption('skip-interactive-field-mapping', null, InputOption::VALUE_NONE, "Do not ask for interactive mapping"); } - + /** * This function is a shortcut to addOption. - * + * * @param string $name * @param string $description * @param string $default @@ -271,10 +223,10 @@ EOF protected function addOptionShortcut($name, $description, $default) { $this->addOption($name, null, InputOption::VALUE_OPTIONAL, $description, $default); - + return $this; } - + /** * @param InputInterface $input * @param OutputInterface $output @@ -285,17 +237,17 @@ EOF $this->input = $input; $this->output = $output; $this->logger = new ConsoleLogger($output); - + $csv = $this->openCSV(); - + // getting the first row if (($row = fgetcsv( - $csv, - $input->getOption('length'), - $input->getOption('delimiter'), - $input->getOption('enclosure'), + $csv, + $input->getOption('length'), + $input->getOption('delimiter'), + $input->getOption('enclosure'), $input->getOption('escape'))) !== false) { - + try { $this->matchColumnToCustomField($row); } finally { @@ -303,33 +255,33 @@ EOF fclose($csv); } } - + // load the matching between csv and label $this->loadAnswerMatching(); } - + /** * @param $row */ protected function matchColumnToCustomField($row) { - + $cfMappingsOptions = $this->input->getOption('custom-field'); /* @var $em \Doctrine\Persistence\ObjectManager */ $em = $this->em; - + foreach($cfMappingsOptions as $cfMappingStringOption) { list($rowNumber, $cfSlug) = preg_split('|=|', $cfMappingStringOption); - + // check that the column exists, getting the column name $column = $row[$rowNumber]; - + if (empty($column)) { $message = "The column with row $rowNumber is empty."; $this->logger->error($message); throw new \RuntimeException($message); } - + // check a custom field exists try { $customField = $em->createQuery("SELECT cf " @@ -357,15 +309,15 @@ EOF . "Stopping this command."); throw new \RuntimeException("The custom field with slug $cfSlug could not be found. " . "Stopping this command."); - } - + } + $this->logger->notice(sprintf("Matched custom field %s (question : '%s') on column %d (displayed in the file as '%s')", $customField->getSlug(), $this->helper->localize($customField->getName()), $rowNumber, $column)); - + $this->customFieldMapping[$rowNumber] = $customField; } } - + /** * Load the mapping between answer in CSV and value in choices from a json file */ @@ -383,7 +335,7 @@ EOF } } } - + /** * */ @@ -392,14 +344,14 @@ EOF if ($this->input->hasOption('dump-choice-matching') && !empty($this->input->getOption('dump-choice-matching'))) { $this->logger->debug("Dump the matching between answer and choices"); $str = json_encode($this->cacheAnswersMapping, JSON_PRETTY_PRINT); - + $fs = new Filesystem(); $filename = $this->input->getOption('dump-choice-matching'); - + $fs->dumpFile($filename, $str); } } - + /** * @param InputInterface $input * @param OutputInterface $output @@ -410,22 +362,22 @@ EOF { $this->input = $input; $this->output = $output; - + $this->logger->debug("Setting locale to ".$input->getArgument('locale')); setlocale(LC_TIME, $input->getArgument('locale')); - + // opening csv as resource $csv = $this->openCSV(); - + $num = 0; $line = $this->line = 1; - + try { while (($row = fgetcsv( - $csv, - $input->getOption('length'), - $input->getOption('delimiter'), - $input->getOption('enclosure'), + $csv, + $input->getOption('length'), + $input->getOption('delimiter'), + $input->getOption('enclosure'), $input->getOption('escape'))) !== false) { $this->logger->debug("Processing line ".$this->line); if ($line === 1 ) { @@ -434,11 +386,11 @@ EOF $headers = $this->processingHeaders($row); } else { $person = $this->createPerson($row, $headers); - + if (count($this->customFieldMapping) > 0) { $this->processingCustomFields($person, $row); } - + $event = new Event(); $event->person = $person; $event->rawHeaders = $rawHeaders; @@ -449,21 +401,21 @@ EOF $event->input = $this->input; $event->output = $this->output; $event->helperSet = $this->getHelperSet(); - + $this->eventDispatcher->dispatch('chill_person.person_import', $event); - - if ($this->input->getOption('force') === TRUE + + if ($this->input->getOption('force') === TRUE && $event->skipPerson === false) { $this->em->persist($person); } - + $num ++; } $line ++; $this->line++; } - + if ($this->input->getOption('force') === true) { $this->logger->debug('persisting entitites'); $this->em->flush(); @@ -475,9 +427,9 @@ EOF $this->dumpAnswerMatching(); } } - + /** - * + * * @return resource * @throws \RuntimeException */ @@ -485,23 +437,23 @@ EOF { $fs = new Filesystem(); $filename = $this->input->getArgument('csv_file'); - + if (!$fs->exists($filename)) { throw new \RuntimeException("The file does not exists or you do not " . "have the right to read it."); } - + $resource = fopen($filename, 'r'); - + if ($resource == FALSE) { throw new \RuntimeException("The file '$filename' could not be opened."); } - + return $resource; } - + /** - * + * * @param type $firstRow * @return array where keys are column number, and value is information mapped */ @@ -510,11 +462,11 @@ EOF $availableOptions = array_map(function($m) { return $m[0]; }, self::$mapping); $matchedColumnHeaders = array(); $headers = array(); - + foreach($availableOptions as $option) { $matchedColumnHeaders[$option] = $this->input->getOption($option); } - + foreach($firstRow as $key => $content) { $content = trim($content); if (in_array($content, $matchedColumnHeaders)) { @@ -524,11 +476,11 @@ EOF } else { $this->logger->notice("Column with content '$content' is ignored"); } - } - + } + return $headers; } - + /** * * @param array $row @@ -541,21 +493,21 @@ EOF // trying to get the opening date $openingDateString = trim($row[array_search('opening_date', $headers)]); $openingDate = $this->processDate($openingDateString, $this->input->getOption('opening_date_format')); - + $person = $openingDate instanceof \DateTime ? new Person($openingDate) : new Person(); // add the center $center = $this->getCenter($row, $headers); - + if ($center === null) { throw new \Exception("center not found"); } - + $person->setCenter($center); - + foreach($headers as $column => $info) { - + $value = trim($row[$column]); - + switch($info) { case 'firstname': $person->setFirstName($value); @@ -582,12 +534,12 @@ EOF $person->setEmail($value); break; case 'phonenumber': - $person->setPhonenumber($value); + $person->setPhonenumber($value); break; case 'mobilenumber': $person->setMobilenumber($value); break; - + // we just keep the column number for those data case 'postalcode': $postalCodeValue = $value; @@ -600,33 +552,33 @@ EOF break; } } - + // handle address if (\in_array('postalcode', $headers)) { - + if (! empty($postalCodeValue)) { - + $address = new Address(); $postalCode = $this->guessPostalCode($postalCodeValue, $localityValue ?? ''); - + if ($postalCode === null) { throw new \Exception("The locality is not found"); } - + $address->setPostcode($postalCode); - + if (\in_array('street1', $headers)) { $address->setStreetAddress1($street1Value); } $address->setValidFrom(new \DateTime('today')); - + $person->addAddress($address); } } - + return $person; } - + /** * @param $row * @param $headers @@ -640,7 +592,7 @@ EOF } else { $columnCenter = \array_search('center', $headers); $centerName = \trim($row[$columnCenter]); - + try { return $this->em->createQuery('SELECT c FROM ChillMainBundle:Center c ' . 'WHERE c.name = :center_name') @@ -654,7 +606,7 @@ EOF } } } - + /** * @param $centerName * @return Center|mixed|null|object @@ -664,68 +616,68 @@ EOF if (!\array_key_exists('_center_picked', $this->cacheAnswersMapping)) { $this->cacheAnswersMapping['_center_picked'] = []; } - + if (\array_key_exists($centerName, $this->cacheAnswersMapping['_center_picked'])) { $id = $this->cacheAnswersMapping['_center_picked'][$centerName]; - + return $this->em->getRepository(Center::class) ->find($id); } - + $centers = $this->em->createQuery("SELECT c FROM ChillMainBundle:Center c " . "ORDER BY SIMILARITY(c.name, :center_name) DESC") ->setParameter('center_name', $centerName) ->setMaxResults(10) ->getResult() ; - + if (count($centers) > 1) { if (\strtolower($centers[0]->getName()) === \strtolower($centerName)) { return $centers[0]; } } - + $centersByName = []; - $names = \array_map(function(Center $c) use (&$centersByName) { + $names = \array_map(function(Center $c) use (&$centersByName) { $n = $c->getName(); $centersByName[$n] = $c; return $n; - + }, $centers); $names[] = "none of them"; - + $helper = $this->getHelper('question'); - $question = new ChoiceQuestion(sprintf("Which center match the name \"%s\" ? (default to \"%s\")", $centerName, $names[0]), + $question = new ChoiceQuestion(sprintf("Which center match the name \"%s\" ? (default to \"%s\")", $centerName, $names[0]), $names, 0); - + $answer = $helper->ask($this->input, $this->output, $question); - + if ($answer === 'none of them') { $questionCreate = new ConfirmationQuestion("Would you like to create it ?", false); $create = $helper->ask($this->input, $this->output, $questionCreate); - + if ($create) { $center = (new Center()) ->setName($centerName) ; - + if ($this->input->getOption('force') === TRUE) { $this->em->persist($center); $this->em->flush(); } - + return $center; } } - + $center = $centersByName[$answer]; - + $this->cacheAnswersMapping['_center_picked'][$centerName] = $center->getId(); - + return $center; } - + /** * @param $postalCode * @param $locality @@ -736,15 +688,15 @@ EOF if (!\array_key_exists('_postal_code_picked', $this->cacheAnswersMapping)) { $this->cacheAnswersMapping['_postal_code_picked'] = []; } - + if (\array_key_exists($postalCode, $this->cacheAnswersMapping['_postal_code_picked'])) { if (\array_key_exists($locality, $this->cacheAnswersMapping['_postal_code_picked'][$postalCode])) { $id = $this->cacheAnswersMapping['_postal_code_picked'][$postalCode][$locality]; - + return $this->em->getRepository(PostalCode::class)->find($id); } } - + $postalCodes = $this->em->createQuery("SELECT pc FROM ".PostalCode::class." pc " . "WHERE pc.code = :postal_code " . "ORDER BY SIMILARITY(pc.name, :locality) DESC " @@ -754,48 +706,48 @@ EOF ->setParameter('locality', $locality) ->getResult() ; - + if (count($postalCodes) >= 1) { - if ($postalCodes[0]->getCode() === $postalCode + if ($postalCodes[0]->getCode() === $postalCode && $postalCodes[0]->getName() === $locality) { return $postalCodes[0]; } } - + if (count($postalCodes) === 0) { return null; } - + $postalCodeByName = []; $names = \array_map(function(PostalCode $pc) use (&$postalCodeByName) { $n = $pc->getName(); $postalCodeByName[$n] = $pc; - + return $n; }, $postalCodes); $names[] = 'none of them'; - + $helper = $this->getHelper('question'); $question = new ChoiceQuestion(sprintf("Which postal code match the " - . "name \"%s\" with postal code \"%s\" ? (default to \"%s\")", - $locality, $postalCode, $names[0]), + . "name \"%s\" with postal code \"%s\" ? (default to \"%s\")", + $locality, $postalCode, $names[0]), $names, 0); - + $answer = $helper->ask($this->input, $this->output, $question); - + if ($answer === 'none of them') { return null; } - + $pc = $postalCodeByName[$answer]; - - $this->cacheAnswersMapping['_postal_code_picked'][$postalCode][$locality] = + + $this->cacheAnswersMapping['_postal_code_picked'][$postalCode][$locality] = $pc->getId(); - + return $pc; } - + /** * @param Person $person * @param $value @@ -804,29 +756,29 @@ EOF protected function processBirthdate(Person $person, $value) { if (empty($value)) { return; } - + $date = $this->processDate($value, $this->input->getOption('birthdate_format')); - + if ($date instanceof \DateTime) { // we correct birthdate if the date is in the future // the most common error is to set date 100 years to late (ex. 2063 instead of 1963) if ($date > new \DateTime('yesterday')) { $date = $date->sub(new \DateInterval('P100Y')); } - + $person->setBirthdate($date); - + return; } - + // if we arrive here, we could not process the date $this->logger->warning(sprintf( "Line %d : the birthdate could not be interpreted. Was %s.", $this->line, $value)); - + } - + /** * @param Person $person * @param $value @@ -835,39 +787,39 @@ EOF protected function processClosingDate(Person $person, $value) { if (empty($value)) { return; } - + // we skip if the opening date is now (or after yesterday) /* @var $period \Chill\PersonBundle\Entity\AccompanyingPeriod */ $period = $person->getCurrentAccompanyingPeriod(); - + if ($period->getOpeningDate() > new \DateTime('yesterday')) { $this->logger->debug(sprintf("skipping a closing date because opening date is after yesterday (%s)", $period->getOpeningDate()->format('Y-m-d'))); return; } - - + + $date = $this->processDate($value, $this->input->getOption('closing_date_format')); - + if ($date instanceof \DateTime) { // we correct birthdate if the date is in the future // the most common error is to set date 100 years to late (ex. 2063 instead of 1963) if ($date > new \DateTime('yesterday')) { $date = $date->sub(new \DateInterval('P100Y')); } - + $period->setClosingDate($date); $person->close(); return; } - + // if we arrive here, we could not process the date $this->logger->warning(sprintf( "Line %d : the closing date could not be interpreted. Was %s.", $this->line, $value)); } - + /** * @param Person $person * @param $row @@ -875,27 +827,27 @@ EOF */ protected function processingCustomFields(Person $person, $row) { - + /* @var $cfProvider \Chill\CustomFieldsBundle\Service\CustomFieldProvider */ $cfProvider = $this->customFieldProvider; $cfData = array(); - + /* @var $$customField \Chill\CustomFieldsBundle\Entity\CustomField */ foreach($this->customFieldMapping as $rowNumber => $customField) { $builder = $this->formFactory->createBuilder(); $cfProvider->getCustomFieldByType($customField->getType()) ->buildForm($builder, $customField); $form = $builder->getForm(); - + // get the type of the form $type = get_class($form->get($customField->getSlug()) ->getConfig()->getType()->getInnerType()); - $this->logger->debug(sprintf("Processing a form of type %s", + $this->logger->debug(sprintf("Processing a form of type %s", $type)); switch ($type) { case \Symfony\Component\Form\Extension\Core\Type\TextType::class: - $cfData[$customField->getSlug()] = + $cfData[$customField->getSlug()] = $this->processTextType($row[$rowNumber], $form, $customField); break; case \Symfony\Component\Form\Extension\Core\Type\ChoiceType::class: @@ -903,12 +855,12 @@ EOF $cfData[$customField->getSlug()] = $this->processChoiceType($row[$rowNumber], $form, $customField); } - + } - + $person->setCFData($cfData); } - + /** * Process a text type on a custom field * @@ -917,24 +869,24 @@ EOF * @return type */ protected function processTextType( - $value, - \Symfony\Component\Form\FormInterface $form, + $value, + \Symfony\Component\Form\FormInterface $form, \Chill\CustomFieldsBundle\Entity\CustomField $cf ) { $form->submit(array($cf->getSlug() => $value)); - + $value = $form->getData()[$cf->getSlug()]; - + $this->logger->debug(sprintf("Found value : %s for custom field with question " . "'%s'", $value, $this->helper->localize($cf->getName()))); - + return $value; } - + protected $cacheAnswersMapping = array(); - - + + /** * Process a custom field choice. * @@ -949,14 +901,14 @@ EOF */ protected function processChoiceType( $value, - \Symfony\Component\Form\FormInterface $form, + \Symfony\Component\Form\FormInterface $form, \Chill\CustomFieldsBundle\Entity\CustomField $cf ) { // getting the possible answer and their value : $view = $form->get($cf->getSlug())->createView(); $answers = $this->collectChoicesAnswers($view->vars['choices']); - + // if we do not have any answer on the question, throw an error. if (count($answers) === 0) { $message = sprintf( @@ -964,34 +916,34 @@ EOF $this->helper->localize($cf->getName()), $cf->getSlug() ); - + $this->logger->error($message, array( 'method' => __METHOD__, 'slug' => $cf->getSlug(), 'question' => $this->helper->localize($cf->getName()) )); - + throw new \RuntimeException($message); } - + if ($view->vars['required'] === false) { $answers[null] = '** no answer'; } - + // the answer does not exists in cache. Try to find it, or asks the user if (!isset($this->cacheAnswersMapping[$cf->getSlug()][$value])) { - + // try to find the answer (with array_keys and a search value $values = array_keys( - array_map(function($label) { return trim(strtolower($label)); }, $answers), + array_map(function($label) { return trim(strtolower($label)); }, $answers), trim(strtolower($value)), true ); - + if (count($values) === 1) { // we could guess an answer ! $this->logger->info("This question accept multiple answers"); - $this->cacheAnswersMapping[$cf->getSlug()][$value] = + $this->cacheAnswersMapping[$cf->getSlug()][$value] = $view->vars['multiple'] == false ? $values[0] : array($values[0]); $this->logger->info(sprintf("Guessed that value '%s' match with key '%s' " . "because the CSV and the label are equals.", @@ -1021,20 +973,20 @@ EOF array_keys($matchingTableRowAnswer) ); $question->setErrorMessage("This choice is not possible"); - + if ($view->vars['multiple']) { $this->logger->debug("this question is multiple"); $question->setMultiselect(true); } - + $selected = $this->getHelper('question')->ask($this->input, $this->output, $question); - $this->output->writeln(sprintf('You have selected "%s"', - is_array($answers[$matchingTableRowAnswer[$selected]]) ? + $this->output->writeln(sprintf('You have selected "%s"', + is_array($answers[$matchingTableRowAnswer[$selected]]) ? implode(',', $answers[$matchingTableRowAnswer[$selected]]) : $answers[$matchingTableRowAnswer[$selected]]) ); - + // recording value in cache $this->cacheAnswersMapping[$cf->getSlug()][$value] = $matchingTableRowAnswer[$selected]; $this->logger->debug(sprintf("Setting the value '%s' in cache for customfield '%s' and answer '%s'", @@ -1045,19 +997,19 @@ EOF $value)); } } - + $form->submit(array($cf->getSlug() => $this->cacheAnswersMapping[$cf->getSlug()][$value])); $value = $form->getData()[$cf->getSlug()]; - + $this->logger->debug(sprintf( - "Found value : %s for custom field with question '%s'", - is_array($value) ? implode(',', $value) : $value, + "Found value : %s for custom field with question '%s'", + is_array($value) ? implode(',', $value) : $value, $this->helper->localize($cf->getName())) ); - + return $value; } - + /** * Recursive method to collect the possibles answer from a ChoiceType (or * its inherited types). @@ -1069,7 +1021,7 @@ EOF private function collectChoicesAnswers($choices) { $answers = array(); - + /* @var $choice \Symfony\Component\Form\ChoiceList\View\ChoiceView */ foreach($choices as $choice) { if ($choice instanceof \Symfony\Component\Form\ChoiceList\View\ChoiceView) { @@ -1085,10 +1037,10 @@ EOF )); } } - + return $answers; } - + /** * @param $value * @param $formats @@ -1097,13 +1049,13 @@ EOF protected function processDate($value, $formats) { $possibleFormats = explode("|", $formats); - + foreach($possibleFormats as $format) { $this->logger->debug("Trying format $format", array(__METHOD__)); $dateR = strptime($value, $format); - + if (is_array($dateR) && $dateR['unparsed'] === '') { - $string = sprintf("%04d-%02d-%02d %02d:%02d:%02d", + $string = sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($dateR['tm_year']+1900), ($dateR['tm_mon']+1), ($dateR['tm_mday']), @@ -1112,19 +1064,19 @@ EOF ($dateR['tm_sec'])); $date = \DateTime::createFromFormat("Y-m-d H:i:s", $string); $this->logger->debug(sprintf("Interpreting %s as date %s", $value, $date->format("Y-m-d H:i:s"))); - + return $date; } } - + // if we arrive here, we could not process the date $this->logger->debug(sprintf( "Line %d : a date could not be interpreted. Was %s.", $this->line, $value)); - + return false; } - - + + } diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 79dae3255..f52564cfa 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -69,7 +69,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/search.yaml'); $loader->load('services/menu.yaml'); $loader->load('services/privacyEvent.yaml'); - $loader->load('services/command.yaml'); $loader->load('services/actions.yaml'); $loader->load('services/form.yaml'); $loader->load('services/alt_names.yaml'); diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml index 7f70a02dd..463ad0141 100644 --- a/src/Bundle/ChillPersonBundle/config/services.yaml +++ b/src/Bundle/ChillPersonBundle/config/services.yaml @@ -12,6 +12,13 @@ services: tags: - { name: 'serializer.normalizer', priority: 64 } + Chill\PersonBundle\Command\: + resource: '../Command/' + autowire: true + autoconfigure: true + tags: + - { name: console.command } + chill.person.form.type.select2maritalstatus: class: Chill\PersonBundle\Form\Type\Select2MaritalStatusType arguments: diff --git a/src/Bundle/ChillPersonBundle/config/services/command.yaml b/src/Bundle/ChillPersonBundle/config/services/command.yaml deleted file mode 100644 index 685a348dd..000000000 --- a/src/Bundle/ChillPersonBundle/config/services/command.yaml +++ /dev/null @@ -1,19 +0,0 @@ -services: - Chill\PersonBundle\Command\ChillPersonMoveCommand: - arguments: - $em: '@Doctrine\ORM\EntityManagerInterface' - $mover: '@Chill\PersonBundle\Actions\Remove\PersonMove' - $chillLogger: '@chill.main.logger' - tags: - - { name: console.command } - - Chill\PersonBundle\Command\ImportPeopleFromCSVCommand: - arguments: - $logger: '@logger' - $helper: '@Chill\MainBundle\Templating\TranslatableStringHelper' - $em: '@Doctrine\ORM\EntityManagerInterface' - $customFieldProvider: '@Chill\CustomFieldsBundle\Service\CustomFieldProvider' - $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' - $formFactory: '@form.factory' - tags: - - { name: console.command } From 2667867bd042167863e1a49fbade006bfacfb985 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Wed, 23 Jun 2021 22:28:05 +0200 Subject: [PATCH 02/11] Update composer - Bump league/php. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fb0d2dcd1..b5af76421 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "knplabs/knp-time-bundle": "^1.12", "symfony/intl": "4.*", "symfony/swiftmailer-bundle": "^3.5", - "league/csv": "^9.6", + "league/csv": "^9.7.1", "phpoffice/phpspreadsheet": "^1.16", "symfony/browser-kit": "^5.2", "symfony/css-selector": "^5.2", From dc26ca7e7708e5ccb42ba800c06d57bc7f82e1ac Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Wed, 23 Jun 2021 16:39:40 +0200 Subject: [PATCH 03/11] Add missing getId() method. --- src/Bundle/ChillPersonBundle/Entity/SocialWork/Goal.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Goal.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Goal.php index 996fcf3eb..633df147c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Goal.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Goal.php @@ -46,6 +46,11 @@ class Goal $this->results = new ArrayCollection(); } + public function getId(): int + { + return $this->id; + } + public function getTitle(): array { return $this->title; From df1f44d81453f1a60b3a46d9fa32f5264ec9fe7b Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Wed, 23 Jun 2021 16:40:04 +0200 Subject: [PATCH 04/11] Let SocialAction::defaultNotificationDelay be nullable. --- .../Entity/SocialWork/SocialAction.php | 2 +- .../migrations/Version20210623142046.php | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210623142046.php diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php index 35abe392f..506a736d8 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php @@ -47,7 +47,7 @@ class SocialAction private $children; /** - * @ORM\Column(type="dateinterval") + * @ORM\Column(type="dateinterval", nullable=true) */ private $defaultNotificationDelay; diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210623142046.php b/src/Bundle/ChillPersonBundle/migrations/Version20210623142046.php new file mode 100644 index 000000000..8baffb867 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210623142046.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE chill_person_social_action ALTER defaultnotificationdelay DROP NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_social_action ALTER defaultNotificationDelay SET NOT NULL'); + } +} From 6e37e7feacd22f0c7721cc934274f0c57b287f04 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Wed, 23 Jun 2021 16:40:43 +0200 Subject: [PATCH 05/11] Let repositories implements ObjectRepository. --- .../SocialWork/EvaluationRepository.php | 39 ++++++++++++++++++- .../Repository/SocialWork/GoalRepository.php | 38 +++++++++++++++++- .../SocialWork/ResultRepository.php | 38 +++++++++++++++++- .../SocialWork/SocialActionRepository.php | 37 +++++++++++++++++- .../SocialWork/SocialIssueRepository.php | 29 ++++++-------- 5 files changed, 160 insertions(+), 21 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php index f554e4e8c..d0f11c2d3 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php @@ -5,8 +5,9 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ObjectRepository; -final class EvaluationRepository +final class EvaluationRepository implements ObjectRepository { private EntityRepository $repository; @@ -14,4 +15,40 @@ final class EvaluationRepository { $this->repository = $entityManager->getRepository(Evaluation::class); } + + public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?Evaluation + { + return $this->repository->find($id, $lockMode, $lockVersion); + } + + /** + * @return array + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Evaluation + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return Evaluation::class; + } + + } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php index bd7c6a738..cb2815d89 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php @@ -5,8 +5,9 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Goal; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ObjectRepository; -final class GoalRepository +final class GoalRepository implements ObjectRepository { private EntityRepository $repository; @@ -14,4 +15,39 @@ final class GoalRepository { $this->repository = $entityManager->getRepository(Goal::class); } + + public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?Goal + { + return $this->repository->find($id, $lockMode, $lockVersion); + } + + /** + * @return array + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Goal + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return Goal::class; + } + } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php index 1c4d8d013..047e4325f 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php @@ -5,8 +5,9 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Result; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ObjectRepository; -final class ResultRepository +final class ResultRepository implements ObjectRepository { private EntityRepository $repository; @@ -14,4 +15,39 @@ final class ResultRepository { $this->repository = $entityManager->getRepository(Result::class); } + + public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?Result + { + return $this->repository->find($id, $lockMode, $lockVersion); + } + + /** + * @return array + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Result + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return Result::class; + } + } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php index 3c86d2397..a81789c0a 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php @@ -5,8 +5,9 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ObjectRepository; -final class SocialActionRepository +final class SocialActionRepository implements ObjectRepository { private EntityRepository $repository; @@ -14,4 +15,38 @@ final class SocialActionRepository { $this->repository = $entityManager->getRepository(SocialAction::class); } + + public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?SocialAction + { + return $this->repository->find($id, $lockMode, $lockVersion); + } + + /** + * @return array + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?SocialAction + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return SocialAction::class; + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php index 4dc5ecc67..b0324ba33 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php @@ -1,9 +1,10 @@ repository = $entityManager->getRepository(SocialIssue::class); } - /** - * {@inheritDoc} - */ - public function find($id) + public function find($id, ?int $lockMode = null, ?int $lockVersion = null): ?SocialIssue { - return $this->repository->find($id); + return $this->repository->find($id, $lockMode, $lockVersion); } /** - * {@inheritDoc} + * @return array */ - public function findAll() + public function findAll(): array { return $this->repository->findAll(); } /** - * {@inheritDoc} + * @return array */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } - /** - * {@inheritDoc} - */ - public function findOneBy(array $criteria) + public function findOneBy(array $criteria, ?array $orderBy = null): ?SocialIssue { - return $this->findOneBy($criteria); + return $this->repository->findOneBy($criteria, $orderBy); } /** - * {@inheritDoc} + * @return class-string */ - public function getClassName() + public function getClassName(): string { return SocialIssue::class; } From 141aabcddccd1a80f731ccae8d125a20ff5a8ef8 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 22 Jun 2021 15:40:49 +0200 Subject: [PATCH 06/11] feat: Add new command. --- .../Command/ImportSocialWorkMetadata.php | 63 ++++ .../Service/Import/ChillImporter.php | 10 + .../Service/Import/SocialWorkMetadata.php | 289 ++++++++++++++++++ .../Import/SocialWorkMetadataInterface.php | 9 + .../ChillPersonBundle/config/services.yaml | 7 +- 5 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillPersonBundle/Command/ImportSocialWorkMetadata.php create mode 100644 src/Bundle/ChillPersonBundle/Service/Import/ChillImporter.php create mode 100644 src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php create mode 100644 src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadataInterface.php diff --git a/src/Bundle/ChillPersonBundle/Command/ImportSocialWorkMetadata.php b/src/Bundle/ChillPersonBundle/Command/ImportSocialWorkMetadata.php new file mode 100644 index 000000000..e42f0ef26 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Command/ImportSocialWorkMetadata.php @@ -0,0 +1,63 @@ +importer = $socialWorkMetadata; + } + + protected function configure() + { + $this + ->setName('chill:person:import-socialwork') + ->addOption('filepath', 'f', InputOption::VALUE_REQUIRED, 'The file to import.') + ->addOption('language', 'l', InputOption::VALUE_OPTIONAL, 'The default language'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filepath = $input->getOption('filepath'); + + try { + $csv = Reader::createFromPath($filepath); + } catch (Throwable $e) { + throw new Exception('Error while loading CSV.',0, $e); + } + + $csv->setDelimiter(';'); + + return true === $this->importer->import($csv) ? + 0: + 1; + } +} diff --git a/src/Bundle/ChillPersonBundle/Service/Import/ChillImporter.php b/src/Bundle/ChillPersonBundle/Service/Import/ChillImporter.php new file mode 100644 index 000000000..4360a7cfa --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/Import/ChillImporter.php @@ -0,0 +1,10 @@ +socialIssueRepository = $socialIssueRepository; + $this->socialActionRepository = $socialActionRepository; + $this->goalRepository = $goalRepository; + $this->resultRepository = $resultRepository; + $this->evaluationRepository = $evaluationRepository; + $this->entityManager = $entityManager; + } + + public function import(iterable $dataset): bool + { + foreach ($dataset as $row) { + $this->import1( + array_map( + static fn (string $column): ?string => '' === $column ? null : $column, + array_map('trim', $row) + ) + ); + } + + return true; + } + + private function import1(array $row): void + { + // Structure: + // Index 0: SocialIssue + // Index 1: SocialIssue.children + // Index 2: SocialAction + // Index 3: SocialAction.children + // Index 4: Goal + // Index 5: Result + // Index 6: Evaluation + + $socialIssue = $this->handleSocialIssue($row[0], $row[1]); + + $socialAction = $this->handleSocialAction($row[2], $row[3], $socialIssue); + + $goal = $this->handleGoal($row[4], $socialAction); + + $result = $this->handleResult($row[5], $socialAction, $goal); + + $eval = $this->handleEvaluation($row[6], $socialAction); + + $this->entityManager->flush(); + } + + private function handleSocialIssue(?string $socialIssueTitle = null, ?string $socialIssueChildrenTitle = null): SocialIssue + { + if (null !== $socialIssueChildrenTitle) { + /** @var SocialIssue $socialIssueChildren */ + $socialIssueChildren = $this->getOrCreateEntity($this->socialIssueRepository, 'title', ['fr' => $socialIssueChildrenTitle]); + $socialIssueChildren->setTitle(['fr' => $socialIssueChildrenTitle]); + + $this->entityManager->persist($socialIssueChildren); + } + + /** @var SocialIssue $socialIssue */ + $socialIssue = $this->getOrCreateEntity($this->socialIssueRepository, 'title', ['fr' => $socialIssueTitle]); + $socialIssue->setTitle(['fr' => $socialIssueTitle]); + + if (null !== $socialIssueChildrenTitle) { + $socialIssue->addChild($socialIssueChildren); + } + + $this->entityManager->persist($socialIssue); + + return null === $socialIssueChildrenTitle ? $socialIssue : $socialIssueChildren; + } + + private function handleSocialAction(?string $socialActionTitle, ?string $socialActionChildrenTitle, SocialIssue $socialIssue): SocialAction + { + if (null !== $socialActionChildrenTitle) { + /** @var SocialAction $socialActionChildren */ + $socialActionChildren = $this->getOrCreateEntity($this->socialActionRepository, 'title', ['fr' => $socialActionChildrenTitle]); + $socialActionChildren->setTitle(['fr' => $socialActionChildrenTitle]); + + $this->entityManager->persist($socialActionChildren); + } + + /** @var SocialIssue $socialIssue */ + $socialAction = $this->getOrCreateEntity($this->socialActionRepository, 'title', ['fr' => $socialActionTitle]); + $socialAction->setTitle(['fr' => $socialActionTitle]); + + if (null !== $socialActionChildrenTitle) { + $socialActionChildren->setIssue($socialIssue); + $this->entityManager->persist($socialActionChildren); + + $socialAction->addChild($socialActionChildren); + } else { + $socialAction->setIssue($socialIssue); + } + + $this->entityManager->persist($socialAction); + + return null === $socialActionChildrenTitle ? $socialAction : $socialActionChildren; + } + + private function handleGoal(?string $goalTitle = null, ?SocialAction $socialAction = null): ?Goal + { + if (null === $goalTitle) { + return null; + } + + /** @var Goal $goal */ + $goal = $this->getOrCreateEntity($this->goalRepository, 'title', ['fr' => $goalTitle]); + $goal->setTitle(['fr' => $goalTitle]); + + if (null !== $socialAction) { + $goal->addSocialAction($socialAction); + } + + $this->entityManager->persist($goal); + + return $goal; + } + + private function handleResult(?string $resultTitle = null, SocialAction $socialAction, ?Goal $goal = null): ?Result + { + if (null === $resultTitle) { + return null; + } + + /** @var Result $result */ + $result = $this->getOrCreateEntity($this->resultRepository, 'title', ['fr' => $resultTitle]); + $result->setTitle(['fr' => $resultTitle]); + + if (null !== $goal) { + $result->addGoal($goal); + } + + // Why this cannot be found from the Goal entity? + $result->addSocialAction($socialAction); + + $this->entityManager->persist($result); + + return $result; + } + + private function handleEvaluation(?string $evaluationTitle, SocialAction $socialAction): ?Evaluation + { + if (null === $evaluationTitle) { + return null; + } + + /** @var Evaluation $eval */ + $eval = $this->getOrCreateEntity($this->evaluationRepository, 'title', ['fr' => $evaluationTitle]); + $eval->setTitle(['fr' => $evaluationTitle]); + $eval->setSocialAction($socialAction); + + $this->entityManager->persist($eval); + + return $eval; + } + + private function findByJson(ObjectRepository $repository, string $field, array $jsonCriterias): array + { + $qb = $this + ->entityManager + ->createQueryBuilder() + ->select('s') + ->from($repository->getClassName(), 's'); + + $expr = $qb->expr(); + + $temporaryJsonCriterias = $jsonParameters = []; + + foreach ($jsonCriterias as $key => $value) { + $temporaryJsonCriterias[] = [$field, $key, $value, sprintf(':placeholder_%s_%s', $field, $key)]; + } + + $jsonParameters = array_reduce( + $temporaryJsonCriterias, + static function (array $carry, array $row): array + { + [,, $value, $placeholder] = $row; + + return array_merge( + $carry, + [ + $placeholder => sprintf('"%s"', $value), + ] + ); + }, + [] + ); + + $jsonPredicates = array_map( + static function (array $row) use ($expr): Comparison + { + [$field, $key,, $placeholder] = $row; + + $left = sprintf( + "GET_JSON_FIELD_BY_KEY(s.%s, '%s')", + $field, + $key + ); + + return $expr + ->eq( + $left, + $placeholder + ); + }, + $temporaryJsonCriterias + ); + + $query = $qb + ->select('s') + ->where(...$jsonPredicates) + ->setParameters($jsonParameters) + ->getQuery(); + + return $query->getResult(); + } + + private function getOrCreateEntity(ObjectRepository $repository, string $field, array $jsonCriterias = []) + { + $results = $this + ->findByJson( + $repository, + $field, + $jsonCriterias + ); + + switch (true) { + case count($results) === 0: + $entity = $repository->getClassName(); + $entity = new $entity(); + break; + case count($results) === 1; + $entity = current($results); + break; + case count($results) > 1; + throw new Exception( + sprintf( + 'More than one entity(%s) found.', + $repository->getClassName() + ) + ); + } + + return $entity; + } + + +} diff --git a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadataInterface.php b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadataInterface.php new file mode 100644 index 000000000..015ef7485 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadataInterface.php @@ -0,0 +1,9 @@ + Date: Thu, 24 Jun 2021 15:24:14 +0200 Subject: [PATCH 07/11] (to rebase/fixup later) Fields must be nullable for import to success. --- .../Entity/SocialWork/Evaluation.php | 2 +- .../migrations/Version20210624131722.php | 29 +++++++++++++++++++ .../migrations/Version20210624131723.php | 29 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210624131722.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210624131723.php diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php index 5a877e264..e08a6de77 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php @@ -23,7 +23,7 @@ class Evaluation private $title = []; /** - * @ORM\Column(type="dateinterval") + * @ORM\Column(type="dateinterval", nullable=true) */ private $delay; diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210624131722.php b/src/Bundle/ChillPersonBundle/migrations/Version20210624131722.php new file mode 100644 index 000000000..ebeb33e50 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210624131722.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE chill_person_social_work_evaluation ALTER delay DROP NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_social_work_evaluation ALTER delay SET NOT NULL'); + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210624131723.php b/src/Bundle/ChillPersonBundle/migrations/Version20210624131723.php new file mode 100644 index 000000000..d36d23733 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210624131723.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE chill_person_social_work_evaluation ALTER notificationdelay DROP NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_social_work_evaluation ALTER notificationdelay SET NOT NULL'); + } +} From 18d0ad67d6fb41d05bb8d07bc504e721bc3b83d1 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Thu, 24 Jun 2021 15:24:29 +0200 Subject: [PATCH 08/11] (to rebase/fixup later) Update command based on feedback. --- .../ChillPersonBundle/Service/Import/SocialWorkMetadata.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php index 0fff36d0f..b5ade864c 100644 --- a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php +++ b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php @@ -149,7 +149,10 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface $goal->setTitle(['fr' => $goalTitle]); if (null !== $socialAction) { + $socialAction->addGoal($goal); $goal->addSocialAction($socialAction); + + $this->entityManager->persist($socialAction); } $this->entityManager->persist($goal); From 3b5ef53b9b2e176dc08e9eea65315a5d1256063e Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Thu, 24 Jun 2021 15:46:04 +0200 Subject: [PATCH 09/11] (to rebase/fixup later) Let Result be associated to a SocialAction. --- .../Service/Import/SocialWorkMetadata.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php index b5ade864c..48dff99bc 100644 --- a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php +++ b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php @@ -160,7 +160,7 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface return $goal; } - private function handleResult(?string $resultTitle = null, SocialAction $socialAction, ?Goal $goal = null): ?Result + private function handleResult(?string $resultTitle = null, ?SocialAction $socialAction = null, ?Goal $goal = null): ?Result { if (null === $resultTitle) { return null; @@ -172,12 +172,15 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface if (null !== $goal) { $result->addGoal($goal); + } else { + $result->addSocialAction($socialAction); } - // Why this cannot be found from the Goal entity? $result->addSocialAction($socialAction); + $socialAction->addResult($result); $this->entityManager->persist($result); + $this->entityManager->persist($socialAction); return $result; } From 4a2ada784a97d7a867d83070e00be6c8934adc17 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Thu, 24 Jun 2021 15:57:03 +0200 Subject: [PATCH 10/11] (to rebase/fixup later) Let Goal be associated to a Result. --- .../ChillPersonBundle/Service/Import/SocialWorkMetadata.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php index 48dff99bc..0896021f1 100644 --- a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php +++ b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php @@ -172,6 +172,9 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface if (null !== $goal) { $result->addGoal($goal); + $goal->addResult($result); + + $this->entityManager->persist($goal); } else { $result->addSocialAction($socialAction); } From d71c3f310e8b93fd1afabd55052d30927c53c984 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Thu, 24 Jun 2021 16:01:41 +0200 Subject: [PATCH 11/11] (to rebase/fixup later) Fix documentation. --- .../Service/Import/SocialWorkMetadata.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php index 0896021f1..4683344dd 100644 --- a/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php +++ b/src/Bundle/ChillPersonBundle/Service/Import/SocialWorkMetadata.php @@ -66,10 +66,10 @@ final class SocialWorkMetadata implements SocialWorkMetadataInterface private function import1(array $row): void { // Structure: - // Index 0: SocialIssue - // Index 1: SocialIssue.children - // Index 2: SocialAction - // Index 3: SocialAction.children + // Index 0: SocialIssue.parent + // Index 1: SocialIssue + // Index 2: SocialAction.parent + // Index 3: SocialAction // Index 4: Goal // Index 5: Result // Index 6: Evaluation