zenstruck /
schedule-bundle
| 1 | <?php |
||||||
| 2 | |||||||
| 3 | namespace Zenstruck\ScheduleBundle\Command; |
||||||
| 4 | |||||||
| 5 | use Lorisleiva\CronTranslator\CronParsingException; |
||||||
| 6 | use Lorisleiva\CronTranslator\CronTranslator; |
||||||
| 7 | use Symfony\Component\Console\Command\Command; |
||||||
| 8 | use Symfony\Component\Console\Input\InputInterface; |
||||||
| 9 | use Symfony\Component\Console\Input\StringInput; |
||||||
| 10 | use Symfony\Component\Console\Output\OutputInterface; |
||||||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; |
||||||
| 12 | use Zenstruck\ScheduleBundle\Schedule; |
||||||
| 13 | use Zenstruck\ScheduleBundle\Schedule\Extension\ExtensionHandlerRegistry; |
||||||
| 14 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner; |
||||||
| 15 | use Zenstruck\ScheduleBundle\Schedule\Task; |
||||||
| 16 | use Zenstruck\ScheduleBundle\Schedule\Task\CommandTask; |
||||||
| 17 | |||||||
| 18 | /** |
||||||
| 19 | * @author Kevin Bond <[email protected]> |
||||||
| 20 | */ |
||||||
| 21 | final class ScheduleListCommand extends Command |
||||||
| 22 | { |
||||||
| 23 | protected static $defaultName = 'schedule:list'; |
||||||
| 24 | |||||||
| 25 | /** @var ScheduleRunner */ |
||||||
| 26 | private $scheduleRunner; |
||||||
| 27 | |||||||
| 28 | 10 | /** @var ExtensionHandlerRegistry */ |
|||||
| 29 | private $handlerRegistry; |
||||||
| 30 | 10 | ||||||
| 31 | 10 | public function __construct(ScheduleRunner $scheduleRunner, ExtensionHandlerRegistry $handlerRegistry) |
|||||
| 32 | { |
||||||
| 33 | 10 | $this->scheduleRunner = $scheduleRunner; |
|||||
| 34 | 10 | $this->handlerRegistry = $handlerRegistry; |
|||||
| 35 | |||||||
| 36 | 10 | parent::__construct(); |
|||||
| 37 | } |
||||||
| 38 | |||||||
| 39 | 10 | protected function configure(): void |
|||||
| 40 | 10 | { |
|||||
| 41 | 10 | $this |
|||||
| 42 | 10 | ->setDescription('List configured scheduled tasks') |
|||||
| 43 | ->addOption('detail', null, null, 'Show detailed task list') |
||||||
| 44 | ->setHelp(<<<EOF |
||||||
| 45 | Exit code 0: no issues. |
||||||
| 46 | Exit code 1: some issues. |
||||||
| 47 | 10 | EOF |
|||||
| 48 | ) |
||||||
| 49 | 10 | ; |
|||||
| 50 | } |
||||||
| 51 | 10 | ||||||
| 52 | protected function execute(InputInterface $input, OutputInterface $output): int |
||||||
| 53 | 10 | { |
|||||
| 54 | 1 | $schedule = $this->scheduleRunner->buildSchedule(); |
|||||
| 55 | |||||||
| 56 | if (0 === \count($schedule->all())) { |
||||||
| 57 | 9 | throw new \RuntimeException('No scheduled tasks configured.'); |
|||||
| 58 | } |
||||||
| 59 | 9 | ||||||
| 60 | $io = new SymfonyStyle($input, $output); |
||||||
| 61 | 9 | ||||||
| 62 | $io->title(\sprintf('<info>%d</info> Scheduled Task%s Configured', \count($schedule->all()), \count($schedule->all()) > 1 ? 's' : '')); |
||||||
| 63 | 9 | ||||||
| 64 | $exit = $input->getOption('detail') ? $this->renderDetail($schedule, $io) : $this->renderTable($schedule, $io); |
||||||
| 65 | 9 | ||||||
| 66 | $this->renderExtenstions($io, 'Schedule', $schedule->getExtensions()); |
||||||
| 67 | 9 | ||||||
| 68 | 3 | $scheduleIssues = \iterator_to_array($this->getScheduleIssues($schedule), false); |
|||||
| 69 | |||||||
| 70 | 3 | if ($issueCount = \count($scheduleIssues)) { |
|||||
| 71 | $io->warning(\sprintf('%d issue%s with schedule:', $issueCount, $issueCount > 1 ? 's' : '')); |
||||||
| 72 | |||||||
| 73 | 9 | $exit = 1; |
|||||
| 74 | } |
||||||
| 75 | 9 | ||||||
| 76 | 4 | $this->renderIssues($io, ...$scheduleIssues); |
|||||
| 77 | |||||||
| 78 | if (0 === $exit) { |
||||||
| 79 | 9 | $io->success('No schedule or task issues.'); |
|||||
| 80 | } |
||||||
| 81 | |||||||
| 82 | 3 | return $exit; |
|||||
| 83 | } |
||||||
| 84 | 3 | ||||||
| 85 | private function renderDetail(Schedule $schedule, SymfonyStyle $io): int |
||||||
| 86 | 3 | { |
|||||
| 87 | 3 | $exit = 0; |
|||||
| 88 | |||||||
| 89 | 3 | foreach ($schedule->all() as $i => $task) { |
|||||
| 90 | $io->section(\sprintf('(%d/%d) %s', $i + 1, \count($schedule->all()), $task)); |
||||||
| 91 | 3 | ||||||
| 92 | 1 | $details = []; |
|||||
| 93 | |||||||
| 94 | foreach ($task->getContext() as $key => $value) { |
||||||
| 95 | 3 | $details[] = [$key => $value]; |
|||||
| 96 | 3 | } |
|||||
| 97 | |||||||
| 98 | 3 | $details[] = ['Task ID' => $task->getId()]; |
|||||
| 99 | $details[] = ['Task Class' => \get_class($task)]; |
||||||
| 100 | 3 | ||||||
| 101 | 1 | $details[] = [$task->getExpression()->isHashed() ? 'Calculated Frequency' : 'Frequency' => $this->renderFrequency($task)]; |
|||||
| 102 | |||||||
| 103 | if ($task->getExpression()->isHashed()) { |
||||||
| 104 | 3 | $details[] = ['Raw Frequency' => $task->getExpression()->getRawValue()]; |
|||||
| 105 | } |
||||||
| 106 | 3 | ||||||
| 107 | 3 | $details[] = ['Next Run' => $task->getNextRun()->format('D, M d, Y @ g:i (e O)')]; |
|||||
| 108 | |||||||
| 109 | 3 | $this->renderDefinitionList($io, $details); |
|||||
| 110 | $this->renderExtenstions($io, 'Task', $task->getExtensions()); |
||||||
| 111 | 3 | ||||||
| 112 | 1 | $issues = \iterator_to_array($this->getTaskIssues($task), false); |
|||||
| 113 | |||||||
| 114 | if ($issueCount = \count($issues)) { |
||||||
| 115 | 3 | $io->warning(\sprintf('%d issue%s with this task:', $issueCount, $issueCount > 1 ? 's' : '')); |
|||||
| 116 | } |
||||||
| 117 | 3 | ||||||
| 118 | 1 | $this->renderIssues($io, ...$issues); |
|||||
| 119 | |||||||
| 120 | if ($issueCount > 0) { |
||||||
| 121 | $exit = 1; |
||||||
| 122 | 3 | } |
|||||
| 123 | } |
||||||
| 124 | |||||||
| 125 | return $exit; |
||||||
| 126 | } |
||||||
| 127 | |||||||
| 128 | 3 | /** |
|||||
| 129 | * BC - Symfony 4.4 added SymfonyStyle::definitionList(). |
||||||
| 130 | 3 | * |
|||||
| 131 | 3 | * @param array<string[]> $list |
|||||
| 132 | */ |
||||||
| 133 | 3 | private function renderDefinitionList(SymfonyStyle $io, array $list): void |
|||||
| 134 | { |
||||||
| 135 | if (\method_exists($io, 'definitionList')) { |
||||||
| 136 | $io->definitionList(...$list); |
||||||
| 137 | |||||||
| 138 | return; |
||||||
| 139 | } |
||||||
| 140 | |||||||
| 141 | $io->listing(\array_map( |
||||||
| 142 | function(array $line) { |
||||||
| 143 | return \sprintf('<info>%s:</info> %s', \array_keys($line)[0], \array_values($line)[0]); |
||||||
| 144 | 7 | }, |
|||||
| 145 | $list |
||||||
| 146 | )); |
||||||
| 147 | 7 | } |
|||||
| 148 | 7 | ||||||
| 149 | private function renderTable(Schedule $schedule, SymfonyStyle $io): int |
||||||
| 150 | 7 | { |
|||||
| 151 | 7 | /** @var array<\Throwable[]> $taskIssues */ |
|||||
| 152 | 7 | $taskIssues = []; |
|||||
| 153 | $rows = []; |
||||||
| 154 | 7 | ||||||
| 155 | 7 | foreach ($schedule->all() as $task) { |
|||||
| 156 | 7 | $issues = \iterator_to_array($this->getTaskIssues($task), false); |
|||||
| 157 | 7 | $taskIssues[] = $issues; |
|||||
| 158 | 7 | ||||||
| 159 | 7 | $rows[] = [ |
|||||
| 160 | \count($issues) ? "<error>[!] {$task->getType()}</error>" : $task->getType(), |
||||||
| 161 | $this->getHelper('formatter')->truncate($task->getDescription(), 50), |
||||||
| 162 | \count($task->getExtensions()), |
||||||
| 163 | 7 | $this->renderFrequency($task), |
|||||
| 164 | $task->getNextRun()->format(\DATE_ATOM), |
||||||
| 165 | 7 | ]; |
|||||
| 166 | } |
||||||
| 167 | 7 | ||||||
| 168 | 3 | $taskIssues = \array_merge([], ...$taskIssues); |
|||||
| 169 | |||||||
| 170 | $io->table(['Type', 'Description', 'Extensions', 'Frequency', 'Next Run'], $rows); |
||||||
| 171 | 7 | ||||||
| 172 | if ($issueCount = \count($taskIssues)) { |
||||||
| 173 | 7 | $io->warning(\sprintf('%d task issue%s:', $issueCount, $issueCount > 1 ? 's' : '')); |
|||||
| 174 | } |
||||||
| 175 | 7 | ||||||
| 176 | $this->renderIssues($io, ...$taskIssues); |
||||||
| 177 | |||||||
| 178 | $io->note('For more details, run php bin/console schedule:list --detail'); |
||||||
| 179 | |||||||
| 180 | return \count($taskIssues) ? 1 : 0; |
||||||
| 181 | 9 | } |
|||||
| 182 | |||||||
| 183 | 9 | /** |
|||||
| 184 | 7 | * @param object[] $extensions |
|||||
| 185 | */ |
||||||
| 186 | private function renderExtenstions(SymfonyStyle $io, string $type, array $extensions): void |
||||||
| 187 | 2 | { |
|||||
| 188 | 2 | if (0 === $count = \count($extensions)) { |
|||||
| 189 | return; |
||||||
| 190 | 2 | } |
|||||
| 191 | 2 | ||||||
| 192 | 2 | $io->comment(\sprintf('<info>%d</info> %s Extension%s:', $count, $type, $count > 1 ? 's' : '')); |
|||||
| 193 | 2 | $io->listing(\array_map( |
|||||
| 194 | function(object $extension) { |
||||||
| 195 | if (\method_exists($extension, '__toString')) { |
||||||
| 196 | return \sprintf('%s <comment>(%s)</comment>', |
||||||
| 197 | \strtr($extension, self::extensionHighlightMap()), |
||||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||||
| 198 | 2 | \get_class($extension) |
|||||
| 199 | 2 | ); |
|||||
| 200 | } |
||||||
| 201 | 2 | ||||||
| 202 | return \get_class($extension); |
||||||
| 203 | }, |
||||||
| 204 | $extensions |
||||||
| 205 | )); |
||||||
| 206 | 9 | } |
|||||
| 207 | |||||||
| 208 | 9 | /** |
|||||
| 209 | * @return \Traversable<\Throwable> |
||||||
| 210 | 2 | */ |
|||||
| 211 | 2 | private function getScheduleIssues(Schedule $schedule): iterable |
|||||
| 212 | 2 | { |
|||||
| 213 | foreach ($schedule->getExtensions() as $extension) { |
||||||
| 214 | try { |
||||||
| 215 | $this->handlerRegistry->handlerFor($extension); |
||||||
| 216 | } catch (\Throwable $e) { |
||||||
| 217 | 9 | yield $e; |
|||||
| 218 | } |
||||||
| 219 | 9 | } |
|||||
| 220 | 9 | ||||||
| 221 | // check for duplicated task ids |
||||||
| 222 | $tasks = []; |
||||||
| 223 | 9 | ||||||
| 224 | 9 | foreach ($schedule->all() as $task) { |
|||||
| 225 | $tasks[$task->getId()][] = $task; |
||||||
| 226 | 9 | } |
|||||
| 227 | 9 | ||||||
| 228 | foreach ($tasks as $taskGroup) { |
||||||
| 229 | $count = \count($taskGroup); |
||||||
| 230 | 1 | ||||||
| 231 | if (1 === $count) { |
||||||
| 232 | 1 | continue; |
|||||
| 233 | } |
||||||
| 234 | 9 | ||||||
| 235 | $task = $taskGroup[0]; |
||||||
| 236 | 2 | ||||||
| 237 | yield new \LogicException(\sprintf('Task "%s" (%s) is duplicated %d times. Make their descriptions unique to fix.', $task, $task->getExpression(), $count)); |
||||||
| 238 | } |
||||||
| 239 | 2 | } |
|||||
| 240 | 2 | ||||||
| 241 | 2 | /** |
|||||
| 242 | 2 | * @return array<string,string> |
|||||
| 243 | */ |
||||||
| 244 | private static function extensionHighlightMap(): array |
||||||
| 245 | { |
||||||
| 246 | return [ |
||||||
| 247 | Task::SUCCESS => \sprintf('<info>%s</info>', Task::SUCCESS), |
||||||
| 248 | Schedule::SUCCESS => \sprintf('<info>%s</info>', Schedule::SUCCESS), |
||||||
| 249 | 9 | Task::FAILURE => \sprintf('<error>%s</error>', Task::FAILURE), |
|||||
| 250 | Schedule::FAILURE => \sprintf('<error>%s</error>', Schedule::FAILURE), |
||||||
| 251 | ]; |
||||||
| 252 | 9 | } |
|||||
| 253 | 4 | ||||||
| 254 | 4 | /** |
|||||
| 255 | * @return \Traversable<\Throwable> |
||||||
| 256 | */ |
||||||
| 257 | 9 | private function getTaskIssues(Task $task): iterable |
|||||
| 258 | { |
||||||
| 259 | 4 | try { |
|||||
| 260 | 1 | $this->scheduleRunner->runnerFor($task); |
|||||
| 261 | 1 | } catch (\Throwable $e) { |
|||||
| 262 | yield $e; |
||||||
| 263 | 1 | } |
|||||
| 264 | 4 | ||||||
| 265 | 4 | if ($task instanceof CommandTask && $application = $this->getApplication()) { |
|||||
| 266 | try { |
||||||
| 267 | $definition = $task->createCommand($application)->getDefinition(); |
||||||
| 268 | $definition->addOptions($application->getDefinition()->getOptions()); |
||||||
| 269 | 9 | $input = new StringInput($task->getArguments()); |
|||||
| 270 | |||||||
| 271 | 2 | $input->bind($definition); |
|||||
| 272 | 2 | } catch (\Throwable $e) { |
|||||
| 273 | 2 | yield $e; |
|||||
| 274 | } |
||||||
| 275 | } |
||||||
| 276 | 9 | ||||||
| 277 | foreach ($task->getExtensions() as $extension) { |
||||||
| 278 | 9 | try { |
|||||
| 279 | $this->handlerRegistry->handlerFor($extension); |
||||||
| 280 | 9 | } catch (\Throwable $e) { |
|||||
| 281 | 5 | yield $e; |
|||||
| 282 | 4 | } |
|||||
| 283 | } |
||||||
| 284 | 4 | } |
|||||
| 285 | |||||||
| 286 | private function renderIssues(SymfonyStyle $io, \Throwable ...$issues): void |
||||||
| 287 | { |
||||||
| 288 | 1 | foreach ($issues as $issue) { |
|||||
| 289 | 1 | if (OutputInterface::VERBOSITY_NORMAL === $io->getVerbosity()) { |
|||||
| 290 | $io->error($issue->getMessage()); |
||||||
| 291 | |||||||
| 292 | continue; |
||||||
| 293 | } |
||||||
| 294 | 9 | ||||||
| 295 | if (!$application = $this->getApplication()) { |
||||||
| 296 | 9 | $io->error((string) $issue); |
|||||
| 297 | |||||||
| 298 | 9 | continue; |
|||||
| 299 | } |
||||||
| 300 | 9 | ||||||
| 301 | // BC - Symfony 4.4 deprecated Application::renderException() |
||||||
| 302 | if (\method_exists($application, 'renderThrowable')) { |
||||||
| 303 | $application->renderThrowable($issue, $io); |
||||||
| 304 | } else { |
||||||
| 305 | 9 | $application->renderException($issue, $io); |
|||||
|
0 ignored issues
–
show
The method
renderException() does not exist on Symfony\Component\Console\Application.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 306 | } |
||||||
| 307 | } |
||||||
| 308 | } |
||||||
| 309 | |||||||
| 310 | private function renderFrequency(Task $task): string |
||||||
| 311 | { |
||||||
| 312 | $expression = (string) $task->getExpression(); |
||||||
| 313 | |||||||
| 314 | if (!\class_exists(CronTranslator::class)) { |
||||||
| 315 | return $expression; |
||||||
| 316 | } |
||||||
| 317 | |||||||
| 318 | try { |
||||||
| 319 | return \sprintf('%s (%s)', $expression, CronTranslator::translate($expression)); |
||||||
| 320 | } catch (CronParsingException $e) { |
||||||
| 321 | return $expression; |
||||||
| 322 | } |
||||||
| 323 | } |
||||||
| 324 | } |
||||||
| 325 |