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; |
||
14 | use Zenstruck\ScheduleBundle\Schedule\Extension\ExtensionHandlerRegistry; |
||
15 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner; |
||
16 | use Zenstruck\ScheduleBundle\Schedule\Task; |
||
17 | use Zenstruck\ScheduleBundle\Schedule\Task\CommandTask; |
||
18 | |||
19 | /** |
||
20 | * @author Kevin Bond <[email protected]> |
||
21 | */ |
||
22 | final class ScheduleListCommand extends Command |
||
23 | { |
||
24 | protected static $defaultName = 'schedule:list'; |
||
25 | |||
26 | private $scheduleRunner; |
||
27 | private $handlerRegistry; |
||
28 | |||
29 | 8 | public function __construct(ScheduleRunner $scheduleRunner, ExtensionHandlerRegistry $handlerRegistry) |
|
30 | { |
||
31 | 8 | $this->scheduleRunner = $scheduleRunner; |
|
32 | 8 | $this->handlerRegistry = $handlerRegistry; |
|
33 | |||
34 | 8 | parent::__construct(); |
|
35 | 8 | } |
|
36 | |||
37 | 8 | protected function configure(): void |
|
38 | { |
||
39 | $this |
||
40 | 8 | ->setDescription('List configured scheduled tasks') |
|
41 | 8 | ->addOption('detail', null, null, 'Show detailed task list') |
|
42 | 8 | ->setHelp(<<<EOF |
|
43 | 8 | Exit code 0: no issues. |
|
44 | Exit code 1: some issues. |
||
45 | EOF |
||
46 | ) |
||
47 | ; |
||
48 | 8 | } |
|
49 | |||
50 | 8 | protected function execute(InputInterface $input, OutputInterface $output): int |
|
51 | { |
||
52 | 8 | $schedule = $this->scheduleRunner->buildSchedule(); |
|
53 | |||
54 | 8 | if (0 === \count($schedule->all())) { |
|
55 | 1 | throw new \RuntimeException('No scheduled tasks configured.'); |
|
56 | } |
||
57 | |||
58 | 7 | $io = new SymfonyStyle($input, $output); |
|
59 | |||
60 | 7 | $io->title(\sprintf('<info>%d</info> Scheduled Task%s Configured', \count($schedule->all()), \count($schedule->all()) > 1 ? 's' : '')); |
|
61 | |||
62 | 7 | $exit = $input->getOption('detail') ? $this->renderDetail($schedule, $io) : $this->renderTable($schedule, $io); |
|
63 | |||
64 | 7 | $this->renderExtenstions($io, 'Schedule', $schedule->getExtensions()); |
|
65 | |||
66 | 7 | $scheduleIssues = \iterator_to_array($this->getScheduleIssues($schedule)); |
|
67 | |||
68 | 7 | if ($issueCount = \count($scheduleIssues)) { |
|
69 | 2 | $io->warning(\sprintf('%d issue%s with schedule:', $issueCount, $issueCount > 1 ? 's' : '')); |
|
70 | |||
71 | 2 | $exit = 1; |
|
72 | } |
||
73 | |||
74 | 7 | $this->renderIssues($io, ...$scheduleIssues); |
|
75 | |||
76 | 7 | return $exit; |
|
77 | } |
||
78 | |||
79 | 2 | private function renderDetail(Schedule $schedule, SymfonyStyle $io): int |
|
80 | { |
||
81 | 2 | $exit = 0; |
|
82 | |||
83 | 2 | foreach ($schedule->all() as $i => $task) { |
|
84 | 2 | $io->section(\sprintf('(%d/%d) %s: %s', $i + 1, \count($schedule->all()), $task->getType(), $task)); |
|
85 | |||
86 | 2 | if ($task instanceof CommandTask && $arguments = $task->getArguments()) { |
|
87 | 1 | $io->comment("Arguments: <comment>{$task->getArguments()}</comment>"); |
|
88 | } |
||
89 | |||
90 | 2 | $details = [['Class' => \get_class($task)]]; |
|
91 | 2 | $details[] = [$task->getExpression()->isHashed() ? 'Calculated Frequency' : 'Frequency' => $this->renderFrequency($task)]; |
|
92 | |||
93 | 2 | if ($task->getExpression()->isHashed()) { |
|
94 | 1 | $details[] = ['Raw Frequency' => $task->getExpression()->getRawValue()]; |
|
95 | } |
||
96 | |||
97 | 2 | $details[] = ['Next Run' => $task->getNextRun()->format('D, M d, Y @ g:i (e O)')]; |
|
98 | |||
99 | 2 | $this->renderDefinitionList($io, $details); |
|
100 | 2 | $this->renderExtenstions($io, 'Task', $task->getExtensions()); |
|
101 | |||
102 | 2 | $issues = \iterator_to_array($this->getTaskIssues($task)); |
|
103 | |||
104 | 2 | if ($issueCount = \count($issues)) { |
|
105 | 1 | $io->warning(\sprintf('%d issue%s with this task:', $issueCount, $issueCount > 1 ? 's' : '')); |
|
106 | } |
||
107 | |||
108 | 2 | $this->renderIssues($io, ...$issues); |
|
109 | |||
110 | 2 | if ($issueCount > 0) { |
|
111 | 1 | $exit = 1; |
|
112 | } |
||
113 | } |
||
114 | |||
115 | 2 | return $exit; |
|
116 | } |
||
117 | |||
118 | /** |
||
119 | * BC - Symfony 4.4 added SymfonyStyle::definitionList(). |
||
120 | */ |
||
121 | 2 | private function renderDefinitionList(SymfonyStyle $io, array $list): void |
|
122 | { |
||
123 | 2 | if (\method_exists($io, 'definitionList')) { |
|
124 | 2 | $io->definitionList(...$list); |
|
125 | |||
126 | 2 | return; |
|
127 | } |
||
128 | |||
129 | $io->listing(\array_map( |
||
130 | function (array $line) { |
||
131 | return \sprintf('<info>%s:</info> %s', \array_keys($line)[0], \array_values($line)[0]); |
||
132 | }, |
||
133 | $list |
||
134 | )); |
||
135 | } |
||
136 | |||
137 | 6 | private function renderTable(Schedule $schedule, SymfonyStyle $io): int |
|
138 | { |
||
139 | /** @var \Throwable[] $taskIssues */ |
||
140 | 6 | $taskIssues = []; |
|
141 | 6 | $rows = []; |
|
142 | |||
143 | 6 | foreach ($schedule->all() as $task) { |
|
144 | 6 | $issues = \iterator_to_array($this->getTaskIssues($task)); |
|
145 | 6 | $taskIssues[] = $issues; |
|
146 | |||
147 | 6 | $rows[] = [ |
|
148 | 6 | \count($issues) ? "<error>[!] {$task->getType()}</error>" : $task->getType(), |
|
149 | 6 | $this->getHelper('formatter')->truncate($task, 50), |
|
150 | 6 | \count($task->getExtensions()), |
|
151 | 6 | $this->renderFrequency($task), |
|
152 | 6 | $task->getNextRun()->format(DATE_ATOM), |
|
153 | ]; |
||
154 | } |
||
155 | |||
156 | 6 | $taskIssues = \array_merge([], ...$taskIssues); |
|
157 | |||
158 | 6 | $io->table(['Type', 'Task', 'Extensions', 'Frequency', 'Next Run'], $rows); |
|
159 | |||
160 | 6 | if ($issueCount = \count($taskIssues)) { |
|
161 | 3 | $io->warning(\sprintf('%d task issue%s:', $issueCount, $issueCount > 1 ? 's' : '')); |
|
162 | } |
||
163 | |||
164 | 6 | $this->renderIssues($io, ...$taskIssues); |
|
165 | |||
166 | 6 | $io->note('For more details, run php bin/console schedule:list --detail'); |
|
167 | |||
168 | 6 | return \count($taskIssues) ? 1 : 0; |
|
169 | } |
||
170 | |||
171 | /** |
||
172 | * @param Extension[] $extensions |
||
173 | */ |
||
174 | 7 | private function renderExtenstions(SymfonyStyle $io, string $type, array $extensions): void |
|
175 | { |
||
176 | 7 | if (0 === $count = \count($extensions)) { |
|
177 | 5 | return; |
|
178 | } |
||
179 | |||
180 | 2 | $io->comment(\sprintf('<info>%d</info> %s Extension%s:', $count, $type, $count > 1 ? 's' : '')); |
|
181 | 2 | $io->listing(\array_map( |
|
182 | function (Extension $extension) { |
||
183 | 2 | return \sprintf('%s <comment>(%s)</comment>', |
|
184 | 2 | \strtr($extension, self::extensionHighlightMap()), |
|
185 | 2 | \get_class($extension) |
|
186 | ); |
||
187 | 2 | }, |
|
188 | 2 | $extensions |
|
189 | )); |
||
190 | 2 | } |
|
191 | |||
192 | /** |
||
193 | * @return \Throwable[] |
||
194 | */ |
||
195 | 7 | private function getScheduleIssues(Schedule $schedule): \Generator |
|
196 | { |
||
197 | 7 | foreach ($schedule->getExtensions() as $extension) { |
|
198 | try { |
||
199 | 2 | $this->handlerRegistry->handlerFor($extension); |
|
200 | 2 | } catch (\Throwable $e) { |
|
201 | 2 | yield $e; |
|
202 | } |
||
203 | } |
||
204 | 7 | } |
|
205 | |||
206 | 2 | private static function extensionHighlightMap(): array |
|
207 | { |
||
208 | return [ |
||
209 | 2 | Extension::TASK_SUCCESS => \sprintf('<info>%s</info>', Extension::TASK_SUCCESS), |
|
210 | 2 | Extension::SCHEDULE_SUCCESS => \sprintf('<info>%s</info>', Extension::SCHEDULE_SUCCESS), |
|
211 | 2 | Extension::TASK_FAILURE => \sprintf('<error>%s</error>', Extension::TASK_FAILURE), |
|
212 | 2 | Extension::SCHEDULE_FAILURE => \sprintf('<error>%s</error>', Extension::SCHEDULE_FAILURE), |
|
213 | ]; |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * @return \Throwable[] |
||
218 | */ |
||
219 | 7 | private function getTaskIssues(Task $task): \Generator |
|
220 | { |
||
221 | try { |
||
222 | 7 | $this->scheduleRunner->runnerFor($task); |
|
223 | 4 | } catch (\Throwable $e) { |
|
224 | 4 | yield $e; |
|
225 | } |
||
226 | |||
227 | 7 | if ($task instanceof CommandTask && $application = $this->getApplication()) { |
|
228 | try { |
||
229 | 4 | $definition = $task->createCommand($application)->getDefinition(); |
|
230 | 1 | $definition->addOptions($application->getDefinition()->getOptions()); |
|
231 | 1 | $input = new StringInput($task->getArguments()); |
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
232 | |||
233 | 1 | $input->bind($definition); |
|
234 | 4 | } catch (\Throwable $e) { |
|
235 | 4 | yield $e; |
|
236 | } |
||
237 | } |
||
238 | |||
239 | 7 | foreach ($task->getExtensions() as $extension) { |
|
240 | try { |
||
241 | 2 | $this->handlerRegistry->handlerFor($extension); |
|
242 | 2 | } catch (\Throwable $e) { |
|
243 | 2 | yield $e; |
|
244 | } |
||
245 | } |
||
246 | 7 | } |
|
247 | |||
248 | 7 | private function renderIssues(SymfonyStyle $io, \Throwable ...$issues): void |
|
249 | { |
||
250 | 7 | foreach ($issues as $issue) { |
|
251 | 4 | if (OutputInterface::VERBOSITY_NORMAL === $io->getVerbosity()) { |
|
252 | 3 | $io->error($issue->getMessage()); |
|
253 | |||
254 | 3 | continue; |
|
255 | } |
||
256 | |||
257 | // BC - Symfony 4.4 deprecated Application::renderException() |
||
258 | 1 | if (\method_exists($this->getApplication(), 'renderThrowable')) { |
|
259 | 1 | $this->getApplication()->renderThrowable($issue, $io); |
|
260 | } else { |
||
261 | $this->getApplication()->renderException($issue, $io); |
||
262 | } |
||
263 | } |
||
264 | 7 | } |
|
265 | |||
266 | 7 | private function renderFrequency(Task $task): string |
|
267 | { |
||
268 | 7 | $expression = (string) $task->getExpression(); |
|
269 | |||
270 | 7 | if (!\class_exists(CronTranslator::class)) { |
|
271 | return $expression; |
||
272 | } |
||
273 | |||
274 | try { |
||
275 | 7 | return \sprintf('%s (%s)', $expression, CronTranslator::translate($expression)); |
|
276 | 1 | } catch (CronParsingException $e) { |
|
277 | 1 | return $expression; |
|
278 | } |
||
279 | } |
||
280 | } |
||
281 |