Browse Source

finalize cron command and jobs

Olivier Massot 3 years ago
parent
commit
3245acfcdf

+ 75 - 42
src/Commands/CronCommand.php

@@ -3,13 +3,14 @@
 namespace App\Commands;
 namespace App\Commands;
 
 
 use App\Service\Cron\CronJobInterface;
 use App\Service\Cron\CronJobInterface;
-use App\Service\Cron\IO\CommandLineIO;
+use App\Service\Cron\UI\ConsoleUI;
 use App\Service\ServiceIterator\CronjobIterator;
 use App\Service\ServiceIterator\CronjobIterator;
 use Psr\Log\LoggerInterface;
 use Psr\Log\LoggerInterface;
 use RuntimeException;
 use RuntimeException;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Command\LockableTrait;
 use Symfony\Component\Console\Command\LockableTrait;
+use Symfony\Component\Console\Helper\FormatterHelper;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
@@ -17,6 +18,11 @@ use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Contracts\Service\Attribute\Required;
 use Symfony\Contracts\Service\Attribute\Required;
 
 
+/**
+ * CLI Command to run the cron-jobs
+ *
+ * @see ~/src/Service/Cron/Readme.md
+ */
 #[AsCommand(
 #[AsCommand(
     name: 'ot:cron',
     name: 'ot:cron',
     description: 'Executes cron jobs'
     description: 'Executes cron jobs'
@@ -27,34 +33,40 @@ class CronCommand extends Command
 
 
     private const ACTION_LIST = 'list';
     private const ACTION_LIST = 'list';
     private const ACTION_RUN = 'run';
     private const ACTION_RUN = 'run';
-    private const ACTION_RUN_ALL = 'run-all';
-    private const ACTIONS = [self::ACTION_LIST, self::ACTION_RUN, self::ACTION_RUN_ALL];
+    private const ACTION_RUN_ALL = 'all';
+
+    private const ACTIONS = [
+        self::ACTION_LIST => 'List registered jobs',
+        self::ACTION_RUN => 'Run the given job',
+        self::ACTION_RUN_ALL => 'Successively run all the registered cron jobs'
+    ];
 
 
-    private InputInterface $input;
     private OutputInterface $output;
     private OutputInterface $output;
-    private bool $silent;
-    private ProgressBar $progressBar;
     private LoggerInterface $logger;
     private LoggerInterface $logger;
     private CronjobIterator $cronjobIterator;
     private CronjobIterator $cronjobIterator;
 
 
     #[Required]
     #[Required]
-    public function setLoggerInterface(LoggerInterface $logger): void
-    {
-        $this->logger = $logger;
-    }
-
+    public function setLoggerInterface(LoggerInterface $logger): void { $this->logger = $logger; }
     #[Required]
     #[Required]
-    public function setCronjobIterator(CronjobIterator $cronjobIterator): void
-    {
-        $this->cronjobIterator = $cronjobIterator;
-    }
+    public function setCronjobIterator(CronjobIterator $cronjobIterator): void { $this->cronjobIterator = $cronjobIterator; }
 
 
+    /**
+     * Configures the command
+     */
     protected function configure(): void
     protected function configure(): void
     {
     {
         $this->addArgument(
         $this->addArgument(
             'action',
             'action',
             InputArgument::REQUIRED,
             InputArgument::REQUIRED,
-            'Action to execute among : ' . implode(', ', self::ACTIONS)
+            'Action to execute among : ' .
+            implode(
+                ', ',
+                array_map(
+                    static function ($v, $k) { return "'" . $k . "' (" . $v . ")"; },
+                    self::ACTIONS,
+                    array_keys(self::ACTIONS)
+                )
+            )
         );
         );
         $this->addArgument(
         $this->addArgument(
             'jobs',
             'jobs',
@@ -65,39 +77,37 @@ class CronCommand extends Command
             'preview',
             'preview',
             'p',
             'p',
             InputOption::VALUE_NONE,
             InputOption::VALUE_NONE,
-            'Only preview the operations instead of executing it'
+            'Only preview the operations instead of executing them'
         );
         );
     }
     }
 
 
-    protected function updateProgression(int $i, int $total): void
-    {
-        if (!$this->progressBar->getMaxSteps() !== $total) {
-            $this->progressBar->setMaxSteps($total);
-        }
-        $this->progressBar->setProgress($i);
-    }
-
+    /**
+     * Executes the command
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @return int
+     */
     final protected function execute(InputInterface $input, OutputInterface $output): int
     final protected function execute(InputInterface $input, OutputInterface $output): int
     {
     {
-        $this->input = $input;
         $this->output = $output;
         $this->output = $output;
+        $formatter = $this->getHelper('formatter');
 
 
         $action = $input->getArgument('action');
         $action = $input->getArgument('action');
         $jobNames = $input->getArgument('jobs');
         $jobNames = $input->getArgument('jobs');
         $preview = $input->getOption('preview');
         $preview = $input->getOption('preview');
+        $jobs = [];
 
 
-        if (!in_array($action, self::ACTIONS, true)) {
-            $this->output->writeln('Error: unrecognized action');
+        if (!array_key_exists($action, self::ACTIONS)) {
+            $this->output->writeln($formatter->formatBlock('Error: unrecognized action', 'error'));
             return Command::INVALID;
             return Command::INVALID;
         }
         }
 
 
-        if ($action === 'list') {
+        if ($action === self::ACTION_LIST) {
             $this->listJobs();
             $this->listJobs();
             return Command::SUCCESS;
             return Command::SUCCESS;
         }
         }
 
 
-        $jobs = [];
-
         if ($action === self::ACTION_RUN_ALL) {
         if ($action === self::ACTION_RUN_ALL) {
             $jobs = $this->cronjobIterator->getAll();
             $jobs = $this->cronjobIterator->getAll();
         }
         }
@@ -114,6 +124,8 @@ class CronCommand extends Command
             }
             }
         }
         }
 
 
+        $this->logger->info('CronCommand will ' . ($preview ? 'preview' : 'execute') . ' ' . implode(', ', $jobs));
+
         foreach ($jobs as $job) {
         foreach ($jobs as $job) {
             $this->runJob($job, $preview);
             $this->runJob($job, $preview);
         }
         }
@@ -121,34 +133,52 @@ class CronCommand extends Command
         return Command::SUCCESS;
         return Command::SUCCESS;
     }
     }
 
 
+    /**
+     * List all available cron jobs
+     */
     private function listJobs(): void {
     private function listJobs(): void {
         $availableJobs = $this->cronjobIterator->getAll();
         $availableJobs = $this->cronjobIterator->getAll();
+
         if (empty($availableJobs)) {
         if (empty($availableJobs)) {
             $this->output->writeln('No cronjob found');
             $this->output->writeln('No cronjob found');
-        } else {
-            $this->output->writeln('Available cron jobs : ');
-            foreach ($this->cronjobIterator->getAll() as $job) {
-                $this->output->writeln('* ' . $job->name());
-            }
+            return;
+        }
+
+        $this->output->writeln('Available cron jobs : ');
+        foreach ($this->cronjobIterator->getAll() as $job) {
+            $this->output->writeln('* ' . $job->name());
         }
         }
     }
     }
 
 
+    /**
+     * Run one Cronjob
+     *
+     * @param CronJobInterface $job
+     * @param bool $preview
+     * @return int
+     */
     private function runJob(CronjobInterface $job, bool $preview = false): int
     private function runJob(CronjobInterface $job, bool $preview = false): int
     {
     {
+        $formatter = $this->getHelper('formatter');
+
         if (!$this->lock($job->name())) {
         if (!$this->lock($job->name())) {
-            $this->output->writeln('The command ' . $job->name() . ' is already running in another process. Abort.');
-            return Command::SUCCESS;
+            $msg = 'The command ' . $job->name() . ' is already running in another process. Abort.';
+            $this->output->writeln($formatter->formatBlock($msg, 'error'));
+            $this->logger->error($msg);
+            return Command::FAILURE;
         }
         }
 
 
         $t0 = microtime(true);
         $t0 = microtime(true);
 
 
-        $this->output->writeln("Started : " . $job->name());
+        $this->output->writeln(
+            $formatter->formatSection($job->name(),"Start")
+        );
         if ($preview) {
         if ($preview) {
             $this->output->writeln('PREVIEW MODE');
             $this->output->writeln('PREVIEW MODE');
         }
         }
 
 
-        $io = new CommandLineIO($job->name(), $this->logger, $this->output);
-        $job->setIO($io);
+        $ui = new ConsoleUI($this->output);
+        $job->setUI($ui);
 
 
         try {
         try {
             if ($preview) {
             if ($preview) {
@@ -163,7 +193,10 @@ class CronCommand extends Command
         }
         }
 
 
         $t1 = microtime(true);
         $t1 = microtime(true);
-        $this->output->writeln($job->name() . " has been successfully executed [" . ($t1 - $t0) . " sec.]");
+
+        $msg = "Job has been successfully executed [" . ($t1 - $t0) . " sec.]";
+        $this->output->writeln($formatter->formatSection($job->name(), $msg));
+        $this->logger->info($msg);
 
 
         return Command::SUCCESS;
         return Command::SUCCESS;
     }
     }

+ 11 - 15
src/Service/Cron/BaseCronJob.php

@@ -2,27 +2,27 @@
 
 
 namespace App\Service\Cron;
 namespace App\Service\Cron;
 
 
-use App\Service\Cron\IO\CronIOInterface;
-use App\Service\Cron\IO\SilentIO;
+use App\Service\Cron\UI\CronUIInterface;
+use App\Service\Cron\UI\SilentUI;
 use App\Service\Utils\StringsUtils;
 use App\Service\Utils\StringsUtils;
+use JetBrains\PhpStorm\Pure;
 use Psr\Log\LoggerInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Contracts\Service\Attribute\Required;
 use Symfony\Contracts\Service\Attribute\Required;
 
 
 /**
 /**
- * Base class for the Cronjobs defined in \App\Service\Cron\Job
+ * Base class for the Cron-jobs defined in \App\Service\Cron\Job
  *
  *
- * This class shouldn't implement directly the CronjobInterface because it shouldn't be injected into the
+ * This class shouldn't implement directly the CronjobInterface because it shall not be injected itself into the
  * CronjobIterator, but all its subclasses should.
  * CronjobIterator, but all its subclasses should.
  */
  */
 abstract class BaseCronJob
 abstract class BaseCronJob
 {
 {
-    protected CronIOInterface $io;
-    protected int $status = CronjobInterface::STATUS_READY;
+    protected CronUIInterface $ui;
 
 
-    #[Required]
-    public function setLoggerInterface(LoggerInterface $logger): void
+    #[Pure]
+    public function __construct()
     {
     {
-        $this->io = new SilentIO($this->name(), $logger);
+        $this->ui = new SilentUI();
     }
     }
 
 
     final public function name(): string {
     final public function name(): string {
@@ -32,11 +32,7 @@ abstract class BaseCronJob
         );
         );
     }
     }
 
 
-    final public function getStatus(): int {
-        return $this->status;
-    }
-
-    public function setIO(CronIOInterface $io): void {
-        $this->io = $io;
+    public function setUI(CronUIInterface $ui): void {
+        $this->ui = $ui;
     }
     }
 }
 }

+ 7 - 12
src/Service/Cron/CronjobInterface.php

@@ -2,22 +2,17 @@
 
 
 namespace App\Service\Cron;
 namespace App\Service\Cron;
 
 
-use App\Service\Cron\IO\CronIOInterface;
+use App\Service\Cron\UI\CronUIInterface;
 
 
+/**
+ * A cron-job
+ *
+ * @see ~/src/Service/Cron/Readme.md
+ */
 interface CronjobInterface
 interface CronjobInterface
 {
 {
-    public const STATUS_READY = 0;
-    public const STATUS_PENDING = 1;
-    public const STATUS_SUCCESS = 2;
-    public const STATUS_SUCCESS_WITH_WARNINGS = 3;
-    public const STATUS_ERROR = 4;
-
     public function name(): string;
     public function name(): string;
-
-    public function setIO(CronIOInterface $io): void;
-
+    public function setUI(CronUIInterface $io): void;
     public function preview(): void;
     public function preview(): void;
     public function execute(): void;
     public function execute(): void;
-
-    public function getStatus(): int;
 }
 }

+ 0 - 49
src/Service/Cron/IO/BaseIO.php

@@ -1,49 +0,0 @@
-<?php
-
-namespace App\Service\Cron\IO;
-
-use Psr\Log\LoggerInterface;
-
-abstract class BaseIO implements CronIOInterface
-{
-    public function __construct(
-        protected string $jobName,
-        private LoggerInterface $logger,
-    ) {}
-
-    private function formatLogMessage(string $message): string
-    {
-        return date('c') . ' - ' . $this->jobName . ' - ' . $message;
-    }
-    protected function debug(string $message): void {
-        $this->logger->debug($this->formatLogMessage($message));
-    }
-    protected function info(string $message): void {
-        $this->logger->info($this->formatLogMessage($message));
-    }
-    protected function warning(string $message): void {
-        $this->logger->warning($this->formatLogMessage($message));
-    }
-    protected function error(string $message): void {
-        $this->logger->error($this->formatLogMessage($message));
-    }
-    protected function critical(string $message): void {
-        $this->logger->critical($this->formatLogMessage($message));
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function print(string $message): void
-    {
-        $this->logger->info('[' . $this->jobName . '] ' . $message);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function progress(int $i, int $total): void
-    {
-        // Do nothing
-    }
-}

+ 0 - 7
src/Service/Cron/IO/SilentIO.php

@@ -1,7 +0,0 @@
-<?php
-
-namespace App\Service\Cron\IO;
-
-class SilentIO extends BaseIO
-{
-}

+ 43 - 16
src/Service/Cron/Job/CleanTempFiles.php

@@ -10,6 +10,7 @@ use App\Service\Storage\LocalStorage;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\Criteria;
 use Doctrine\Common\Collections\Criteria;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\EntityManagerInterface;
+use JetBrains\PhpStorm\Pure;
 
 
 /**
 /**
  * Cronjob that delete temporary files older than N days
  * Cronjob that delete temporary files older than N days
@@ -33,36 +34,46 @@ class CleanTempFiles extends BaseCronJob implements CronjobInterface
 
 
     /**
     /**
      * @param FileRepository $fileRepository
      * @param FileRepository $fileRepository
+     * @param EntityManagerInterface $em
      * @param AccessRepository $accessRepository
      * @param AccessRepository $accessRepository
      * @param LocalStorage $storage
      * @param LocalStorage $storage
      */
      */
+    #[Pure]
     public function __construct(
     public function __construct(
         private FileRepository $fileRepository,
         private FileRepository $fileRepository,
         private EntityManagerInterface $em,
         private EntityManagerInterface $em,
         private AccessRepository $accessRepository,
         private AccessRepository $accessRepository,
         private LocalStorage $storage
         private LocalStorage $storage
-    ) {}
+    ) {
+        parent::__construct();
+    }
 
 
+    /**
+     * Preview the result of the execution, without actually deleting anything
+     */
     public function preview(): void
     public function preview(): void
     {
     {
         $files = $this->listFilesToDelete();
         $files = $this->listFilesToDelete();
         $total = count($files);
         $total = count($files);
-        $this->io->print($total . " temporary files to be removed");
-        $this->io->print("> Printing the first and last 50 :");
+        $this->ui->print($total . " temporary files to be removed");
+        $this->ui->print("> Printing the first and last 50 :");
 
 
         $i = 0;
         $i = 0;
         foreach ($files as $file) {
         foreach ($files as $file) {
             $i++;
             $i++;
             if ($i > 50 && ($total - $i) > 50) { continue; }
             if ($i > 50 && ($total - $i) > 50) { continue; }
             if (($total - $i) === 50) {
             if (($total - $i) === 50) {
-                $this->io->print('  (...)');
+                $this->ui->print('  (...)');
             }
             }
-            $this->io->print('  * ' . $file->getPath());
+            $this->ui->print('  * ' . $file->getPath());
         }
         }
 
 
         $this->purgeDb(false);
         $this->purgeDb(false);
     }
     }
 
 
+    /**
+     * Proceed to the deletion of the files and the purge of the DB
+     */
     public function execute(): void
     public function execute(): void
     {
     {
         $files = $this->listFilesToDelete();
         $files = $this->listFilesToDelete();
@@ -72,11 +83,17 @@ class CleanTempFiles extends BaseCronJob implements CronjobInterface
         $this->purgeDb();
         $this->purgeDb();
     }
     }
 
 
+    /**
+     * List the files to delete in the DB
+     *
+     * @return Collection
+     * @throws \Exception
+     */
     private function listFilesToDelete(): Collection {
     private function listFilesToDelete(): Collection {
 
 
         $maxDate = new \DateTime(self::DELETE_OLDER_THAN . ' days ago');
         $maxDate = new \DateTime(self::DELETE_OLDER_THAN . ' days ago');
 
 
-        $this->io->print('List temporary files created before ' . $maxDate->format('c'));
+        $this->ui->print('List temporary files created before ' . $maxDate->format('c'));
 
 
         $criteria = new Criteria();
         $criteria = new Criteria();
         $criteria->where(
         $criteria->where(
@@ -92,9 +109,14 @@ class CleanTempFiles extends BaseCronJob implements CronjobInterface
         return $this->fileRepository->matching($criteria);
         return $this->fileRepository->matching($criteria);
     }
     }
 
 
+    /**
+     * Delete the files
+     *
+     * @param Collection $files
+     */
     private function deleteFiles(Collection $files): void {
     private function deleteFiles(Collection $files): void {
         $total = count($files);
         $total = count($files);
-        $this->io->print($total . " temporary files to be removed");
+        $this->ui->print($total . " temporary files to be removed");
 
 
         $author = $this->accessRepository->find(self::AUTHOR);
         $author = $this->accessRepository->find(self::AUTHOR);
 
 
@@ -102,28 +124,33 @@ class CleanTempFiles extends BaseCronJob implements CronjobInterface
             throw new \RuntimeException('Access ' . self::AUTHOR . ' could not be found');
             throw new \RuntimeException('Access ' . self::AUTHOR . ' could not be found');
         }
         }
 
 
-        $this->io->print('Deleting files...');
+        $this->ui->print('Deleting files...');
         $i = 0;
         $i = 0;
-        $this->io->progress(0, $total);
+        $this->ui->progress(0, $total);
         foreach ($files as $file) {
         foreach ($files as $file) {
             try {
             try {
                 $i++;
                 $i++;
-                $this->io->progress($i, $total);
+                $this->ui->progress($i, $total);
                 $this->storage->delete($file, $author);
                 $this->storage->delete($file, $author);
             } catch (\RuntimeException $e) {
             } catch (\RuntimeException $e) {
-                $this->io->print('ERROR : ' . $e->getMessage());
+                $this->ui->print('ERROR : ' . $e->getMessage());
             }
             }
         }
         }
-        $this->io->print(''); // to force a line break after the progression bar
 
 
-        $this->io->print($i . ' files deleted');
+        $this->ui->print($i . ' files deleted');
     }
     }
 
 
+    /**
+     * Purge the DB from temporary file records older than N days
+     *
+     * @param bool $commit
+     * @throws \Exception
+     */
     private function purgeDb(bool $commit = true): void {
     private function purgeDb(bool $commit = true): void {
 
 
         $maxDate = new \DateTime(self::PURGE_RECORDS_OLDER_THAN . ' days ago');
         $maxDate = new \DateTime(self::PURGE_RECORDS_OLDER_THAN . ' days ago');
 
 
-        $this->io->print('Purge DB from records of files deleted before ' . $maxDate->format('c'));
+        $this->ui->print('Purge DB from records of files deleted before ' . $maxDate->format('c'));
 
 
         $this->em->beginTransaction();
         $this->em->beginTransaction();
 
 
@@ -139,12 +166,12 @@ class CleanTempFiles extends BaseCronJob implements CronjobInterface
 
 
         if ($commit) {
         if ($commit) {
             $this->em->commit();
             $this->em->commit();
-            $this->io->print('DB purged - ' . $purged . ' records permanently deleted');
+            $this->ui->print('DB purged - ' . $purged . ' records permanently deleted');
             return;
             return;
         }
         }
 
 
         $this->em->rollback();
         $this->em->rollback();
-        $this->io->print('DB purged - ' . $purged . ' records would be permanently deleted');
+        $this->ui->print('DB purged - ' . $purged . ' records would be permanently deleted');
     }
     }
 
 
 }
 }

+ 33 - 19
src/Service/Cron/Job/DolibarrSync.php

@@ -2,15 +2,11 @@
 
 
 namespace App\Service\Cron\Job;
 namespace App\Service\Cron\Job;
 
 
-use App\Repository\Access\AccessRepository;
-use App\Repository\Core\FileRepository;
 use App\Service\Cron\BaseCronJob;
 use App\Service\Cron\BaseCronJob;
 use App\Service\Cron\CronjobInterface;
 use App\Service\Cron\CronjobInterface;
 use App\Service\Dolibarr\DolibarrSyncService;
 use App\Service\Dolibarr\DolibarrSyncService;
-use App\Service\Storage\LocalStorage;
-use Doctrine\Common\Collections\Collection;
-use Doctrine\Common\Collections\Criteria;
-use Doctrine\ORM\EntityManagerInterface;
+use Closure;
+use JetBrains\PhpStorm\Pure;
 
 
 /**
 /**
  * Push the latest data from the Opentalent DB to dolibarr
  * Push the latest data from the Opentalent DB to dolibarr
@@ -20,42 +16,60 @@ class DolibarrSync extends BaseCronJob implements CronjobInterface
     /**
     /**
      * How many operations are shown each time the preview choice is made
      * How many operations are shown each time the preview choice is made
      */
      */
-    const PREVIEW_CHUNK = 20;
+    public const PREVIEW_CHUNK = 20;
 
 
+    #[Pure]
     public function __construct(
     public function __construct(
         private DolibarrSyncService $dolibarrSyncService
         private DolibarrSyncService $dolibarrSyncService
-    ) {}
-
-    public function getProgressCallback(): \Closure
-    {
-        return function($i, $total) {
-            $this->io->progress($i, $total);
-        };
+    ) {
+        parent::__construct();
     }
     }
 
 
+    /**
+     * Previews the sync operation without actually modifying anything
+     *
+     * @throws \Exception
+     */
     public function preview(): void
     public function preview(): void
     {
     {
         $operations = $this->dolibarrSyncService->scan($this->getProgressCallback());
         $operations = $this->dolibarrSyncService->scan($this->getProgressCallback());
 
 
         foreach ($operations as $i => $iValue) {
         foreach ($operations as $i => $iValue) {
-            $this->io->print($i . '. ' . $iValue->getLabel());
+            $this->ui->print($i . '. ' . $iValue->getLabel());
             foreach ($iValue->getChangeLog() as $message) {
             foreach ($iValue->getChangeLog() as $message) {
-                $this->io->print('   ' . $message);
+                $this->ui->print('   ' . $message);
             }
             }
         }
         }
     }
     }
 
 
+    /**
+     * Executes the sync
+     *
+     * @throws \Exception
+     */
     public function execute(): void
     public function execute(): void
     {
     {
         $operations = $this->dolibarrSyncService->scan($this->getProgressCallback());
         $operations = $this->dolibarrSyncService->scan($this->getProgressCallback());
 
 
-        $this->io->print("Executing...");
+        $this->ui->print("Executing...");
 
 
         $operations = $this->dolibarrSyncService->execute($operations, $this->getProgressCallback());
         $operations = $this->dolibarrSyncService->execute($operations, $this->getProgressCallback());
 
 
         $successes = count(array_filter($operations, static function ($o) { return $o->getStatus() === $o::STATUS_DONE; } ));
         $successes = count(array_filter($operations, static function ($o) { return $o->getStatus() === $o::STATUS_DONE; } ));
         $errors = count(array_filter($operations, static function ($o) { return $o->getStatus() === $o::STATUS_ERROR; } ));
         $errors = count(array_filter($operations, static function ($o) { return $o->getStatus() === $o::STATUS_ERROR; } ));
-        $this->io->print($successes . " operations successfully executed");
-        $this->io->print($errors . " errors");
+        $this->ui->print($successes . " operations successfully executed");
+        $this->ui->print($errors . " errors");
+    }
+
+    /**
+     * Pass a callback to update the progress bar state from DolibarrSyncService
+     *
+     * @return Closure
+     */
+    public function getProgressCallback(): Closure
+    {
+        return function($i, $total) {
+            $this->ui->progress($i, $total);
+        };
     }
     }
 }
 }

+ 1 - 1
src/Service/Cron/Readme.md

@@ -4,7 +4,7 @@ This namespace group the cronjobs that can be executed with
 
 
     ot:cron [options] <action> [<jobs>]
     ot:cron [options] <action> [<jobs>]
 
 
-Each of the cronjobs are in App\Service\Cron\Job and shall implement the CronjobInterface.
+Each of the cronjobs are in `App\Service\Cron\Job` and shall implement the CronjobInterface.
 
 
 The job name is the class name, formatted in SnakeCase with hyphen `-`
 The job name is the class name, formatted in SnakeCase with hyphen `-`
 
 

+ 12 - 8
src/Service/Cron/IO/CommandLineIO.php → src/Service/Cron/UI/ConsoleUI.php

@@ -1,21 +1,23 @@
 <?php
 <?php
 
 
-namespace App\Service\Cron\IO;
+namespace App\Service\Cron\UI;
 
 
 use Psr\Log\LoggerInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
 
-class CommandLineIO extends BaseIO
+/**
+ * Console user interface
+ *
+ * Use it to make communicate cron-jobs with command output
+ */
+class ConsoleUI implements CronUIInterface
 {
 {
     private ProgressBar $progressBar;
     private ProgressBar $progressBar;
 
 
     public function __construct(
     public function __construct(
-        string $jobName,
-        LoggerInterface $logger,
         private OutputInterface $output
         private OutputInterface $output
     ) {
     ) {
-        parent::__construct($jobName, $logger);
         $this->progressBar = new ProgressBar($output, 0);
         $this->progressBar = new ProgressBar($output, 0);
     }
     }
 
 
@@ -32,9 +34,11 @@ class CommandLineIO extends BaseIO
      */
      */
     public function progress(int $i, int $total): void
     public function progress(int $i, int $total): void
     {
     {
-        if (!$this->progressBar->getMaxSteps() !== $total) {
-            $this->progressBar->setMaxSteps($total);
-        }
+        $this->progressBar->setMaxSteps($total);
         $this->progressBar->setProgress($i);
         $this->progressBar->setProgress($i);
+
+        if ($i === $total) {
+            $this->print(''); // to force a line break after the progression bar ends
+        }
     }
     }
 }
 }

+ 5 - 2
src/Service/Cron/IO/CronIOInterface.php → src/Service/Cron/UI/CronUIInterface.php

@@ -1,8 +1,11 @@
 <?php
 <?php
 
 
-namespace App\Service\Cron\IO;
+namespace App\Service\Cron\UI;
 
 
-interface CronIOInterface
+/**
+ * Establish communication between cron-jobs and the CLI output
+ */
+interface CronUIInterface
 {
 {
     /**
     /**
      * Print a simple line of text
      * Print a simple line of text

+ 28 - 0
src/Service/Cron/UI/SilentUI.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Service\Cron\UI;
+
+/**
+ * Silent user interface
+ *
+ * This is the default interface between cron-jobs and commands, and it does nothing,
+ * but it can be replaced by other more verbose interfaces if needed.
+ */
+class SilentUI implements CronUIInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public function print(string $message): void
+    {
+        // Do nothing
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function progress(int $i, int $total): void
+    {
+        // Do nothing
+    }
+}