'List registered jobs', self::ACTION_RUN => 'Run the given job', self::ACTION_RUN_ALL => 'Successively run all the registered cron jobs', ]; private OutputInterface $output; private LoggerInterface $logger; private CronjobIterator $cronjobIterator; /** @noinspection PhpUnused */ #[Required] /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */ public function setLoggerInterface(LoggerInterface $cronLogger): void { $this->logger = $cronLogger; } /** @noinspection PhpUnused */ #[Required] public function setCronjobIterator(CronjobIterator $cronjobIterator): void { $this->cronjobIterator = $cronjobIterator; } /** * Configures the command. */ protected function configure(): void { $this->addArgument( 'action', InputArgument::REQUIRED, 'Action to execute among : '. implode( ', ', array_map( static function ($v, $k) { return "'".$k."' (".$v.')'; }, self::ACTIONS, array_keys(self::ACTIONS) ) ) ); $this->addArgument( 'jobs', InputArgument::OPTIONAL, "Name(s) of the cron-job(s) to execute with '".self::ACTION_RUN."' (comma-separated)" ); $this->addOption( 'preview', 'p', InputOption::VALUE_NONE, 'Only preview the operations instead of executing them' ); } /** * Executes the command. */ final protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; $this->configureLoggerFormatter(); /** @var FormatterHelper $formatter */ $formatter = $this->getHelper('formatter'); $action = $input->getArgument('action'); $jobNames = $input->getArgument('jobs'); $preview = $input->getOption('preview'); $jobs = []; if ($preview) { $this->disableLoggerEmailHandler(); } if (!array_key_exists($action, self::ACTIONS)) { $this->output->writeln($formatter->formatBlock('Error: unrecognized action', 'error')); return Command::INVALID; } if ($action === self::ACTION_LIST) { $this->listJobs(); return Command::SUCCESS; } if ($action === self::ACTION_RUN_ALL) { $jobs = $this->cronjobIterator->getAll(); } if ($action === self::ACTION_RUN) { foreach (explode(',', $jobNames) as $name) { try { $jobs[] = $this->cronjobIterator->getByName($name); } catch (\RuntimeException $e) { $this->output->writeln($e->getMessage()); $this->listJobs(); return Command::INVALID; } } } $results = []; foreach ($jobs as $job) { $this->logger->info( 'CronCommand will execute `'.$job->name().'`'.($preview ? ' [PREVIEW MODE]' : '') ); $results[] = $this->runJob($job, $preview); } return (int) max($results); // If there is one failure result, the whole command is shown as a failure too } /** * List all available cron jobs. */ private function listJobs(): void { $availableJobs = $this->cronjobIterator->getAll(); if (empty($availableJobs)) { $this->output->writeln('No cronjob found'); return; } $this->output->writeln('Available cron jobs : '); foreach ($this->cronjobIterator->getAll() as $job) { $this->output->writeln('* '.$job->name()); } } /** * Run one Cronjob. */ private function runJob(CronjobInterface $job, bool $preview = false): int { /** @var FormatterHelper $formatter */ $formatter = $this->getHelper('formatter'); if (!$this->lock($job->name())) { $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); $this->output->writeln( $formatter->formatSection($job->name(), 'Start'.($preview ? ' [PREVIEW MODE]' : '')) ); // Establish communication between job and the console $ui = new ConsoleUI($this->output); $job->setUI($ui); $this->configureLoggerFormatter($job->name()); try { if ($preview) { $job->preview(); } else { $job->execute(); } $t1 = microtime(true); $msg = 'Job has been successfully executed ('.round($t1 - $t0, 2).' sec.)'.($preview ? ' [PREVIEW MODE]' : ''); $this->output->writeln($formatter->formatSection($job->name(), $msg)); $this->logger->info($job->name().' - '.$msg); } catch (\Throwable $e) { $this->logger->critical($e); $this->output->write('An error happened while running the process : '.$e); return Command::FAILURE; } finally { $this->resetLoggerFormatter(); } return Command::SUCCESS; } /** * Modify the RotatingFile logger line format to match the display the current job's name (if any). */ protected function configureLoggerFormatter(?string $jobName = null): void { /* @noinspection PhpPossiblePolymorphicInvocationInspection @phpstan-ignore-next-line */ foreach ($this->logger->getHandlers() as $handler) { if ($handler instanceof RotatingFileHandler) { $format = '[%datetime%] '. ($jobName !== null ? '['.$jobName.'] ' : ''). "%channel%.%level_name%: %message% %context% %extra%\n"; $handler->setFormatter(new LineFormatter($format, 'Y-m-d H:i:s.v')); } } } /** * Alias for `$this->configureLoggerFormatter(null)`. */ protected function resetLoggerFormatter(): void { $this->configureLoggerFormatter(); } protected function disableLoggerEmailHandler(): void { $handlers = []; /* @noinspection PhpPossiblePolymorphicInvocationInspection @phpstan-ignore-next-line */ foreach ($this->logger->getHandlers() as $handler) { if (!$handler instanceof FingersCrossedHandler) { $handlers[] = $handler; } } $this->logger->setHandlers($handlers); // @phpstan-ignore-line } }