Passed
Push — master ( 8ef5c6...9a5374 )
by Kevin
02:26
created

ScheduleListCommand::execute()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 31
rs 8.8333
ccs 16
cts 16
cp 1
nc 9
nop 2
cc 7
crap 7
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
        if (0 === $exit) {
77 3
            $io->success('No schedule or task issues.');
78
        }
79
80 7
        return $exit;
81
    }
82
83 2
    private function renderDetail(Schedule $schedule, SymfonyStyle $io): int
84
    {
85 2
        $exit = 0;
86
87 2
        foreach ($schedule->all() as $i => $task) {
88 2
            $io->section(\sprintf('(%d/%d) %s: %s', $i + 1, \count($schedule->all()), $task->getType(), $task));
89
90 2
            $details = [['Class' => \get_class($task)]];
91
92 2
            if ($task instanceof CommandTask && $arguments = $task->getArguments()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
93 1
                $details[] = ['Command Arguments' => $task->getArguments()];
94
            }
95
96 2
            $details[] = [$task->getExpression()->isHashed() ? 'Calculated Frequency' : 'Frequency' => $this->renderFrequency($task)];
97
98 2
            if ($task->getExpression()->isHashed()) {
99 1
                $details[] = ['Raw Frequency' => $task->getExpression()->getRawValue()];
100
            }
101
102 2
            $details[] = ['Next Run' => $task->getNextRun()->format('D, M d, Y @ g:i (e O)')];
103
104 2
            $this->renderDefinitionList($io, $details);
105 2
            $this->renderExtenstions($io, 'Task', $task->getExtensions());
106
107 2
            $issues = \iterator_to_array($this->getTaskIssues($task));
108
109 2
            if ($issueCount = \count($issues)) {
110 1
                $io->warning(\sprintf('%d issue%s with this task:', $issueCount, $issueCount > 1 ? 's' : ''));
111
            }
112
113 2
            $this->renderIssues($io, ...$issues);
114
115 2
            if ($issueCount > 0) {
116 1
                $exit = 1;
117
            }
118
        }
119
120 2
        return $exit;
121
    }
122
123
    /**
124
     * BC - Symfony 4.4 added SymfonyStyle::definitionList().
125
     */
126 2
    private function renderDefinitionList(SymfonyStyle $io, array $list): void
127
    {
128 2
        if (\method_exists($io, 'definitionList')) {
129 2
            $io->definitionList(...$list);
130
131 2
            return;
132
        }
133
134
        $io->listing(\array_map(
135
            function (array $line) {
136
                return \sprintf('<info>%s:</info> %s', \array_keys($line)[0], \array_values($line)[0]);
137
            },
138
            $list
139
        ));
140
    }
141
142 6
    private function renderTable(Schedule $schedule, SymfonyStyle $io): int
143
    {
144
        /** @var \Throwable[] $taskIssues */
145 6
        $taskIssues = [];
146 6
        $rows = [];
147
148 6
        foreach ($schedule->all() as $task) {
149 6
            $issues = \iterator_to_array($this->getTaskIssues($task));
150 6
            $taskIssues[] = $issues;
151
152 6
            $rows[] = [
153 6
                \count($issues) ? "<error>[!] {$task->getType()}</error>" : $task->getType(),
154 6
                $this->getHelper('formatter')->truncate($task, 50),
155 6
                \count($task->getExtensions()),
156 6
                $this->renderFrequency($task),
157 6
                $task->getNextRun()->format(DATE_ATOM),
158
            ];
159
        }
160
161 6
        $taskIssues = \array_merge([], ...$taskIssues);
162
163 6
        $io->table(['Type', 'Task', 'Extensions', 'Frequency', 'Next Run'], $rows);
164
165 6
        if ($issueCount = \count($taskIssues)) {
166 3
            $io->warning(\sprintf('%d task issue%s:', $issueCount, $issueCount > 1 ? 's' : ''));
167
        }
168
169 6
        $this->renderIssues($io, ...$taskIssues);
170
171 6
        $io->note('For more details, run php bin/console schedule:list --detail');
172
173 6
        return \count($taskIssues) ? 1 : 0;
174
    }
175
176
    /**
177
     * @param Extension[] $extensions
178
     */
179 7
    private function renderExtenstions(SymfonyStyle $io, string $type, array $extensions): void
180
    {
181 7
        if (0 === $count = \count($extensions)) {
182 5
            return;
183
        }
184
185 2
        $io->comment(\sprintf('<info>%d</info> %s Extension%s:', $count, $type, $count > 1 ? 's' : ''));
186 2
        $io->listing(\array_map(
187
            function (Extension $extension) {
188 2
                return \sprintf('%s <comment>(%s)</comment>',
189 2
                    \strtr($extension, self::extensionHighlightMap()),
190 2
                    \get_class($extension)
191
                );
192 2
            },
193 2
            $extensions
194
        ));
195 2
    }
196
197
    /**
198
     * @return \Throwable[]
199
     */
200 7
    private function getScheduleIssues(Schedule $schedule): \Generator
201
    {
202 7
        foreach ($schedule->getExtensions() as $extension) {
203
            try {
204 2
                $this->handlerRegistry->handlerFor($extension);
205 2
            } catch (\Throwable $e) {
206 2
                yield $e;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $e returns the type Generator which is incompatible with the documented return type Throwable[].
Loading history...
207
            }
208
        }
209 7
    }
210
211 2
    private static function extensionHighlightMap(): array
212
    {
213
        return [
214 2
            Extension::TASK_SUCCESS => \sprintf('<info>%s</info>', Extension::TASK_SUCCESS),
215 2
            Extension::SCHEDULE_SUCCESS => \sprintf('<info>%s</info>', Extension::SCHEDULE_SUCCESS),
216 2
            Extension::TASK_FAILURE => \sprintf('<error>%s</error>', Extension::TASK_FAILURE),
217 2
            Extension::SCHEDULE_FAILURE => \sprintf('<error>%s</error>', Extension::SCHEDULE_FAILURE),
218
        ];
219
    }
220
221
    /**
222
     * @return \Throwable[]
223
     */
224 7
    private function getTaskIssues(Task $task): \Generator
225
    {
226
        try {
227 7
            $this->scheduleRunner->runnerFor($task);
228 4
        } catch (\Throwable $e) {
229 4
            yield $e;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $e returns the type Generator which is incompatible with the documented return type Throwable[].
Loading history...
230
        }
231
232 7
        if ($task instanceof CommandTask && $application = $this->getApplication()) {
233
            try {
234 4
                $definition = $task->createCommand($application)->getDefinition();
235 1
                $definition->addOptions($application->getDefinition()->getOptions());
236 1
                $input = new StringInput($task->getArguments());
237
238 1
                $input->bind($definition);
239 4
            } catch (\Throwable $e) {
240 4
                yield $e;
241
            }
242
        }
243
244 7
        foreach ($task->getExtensions() as $extension) {
245
            try {
246 2
                $this->handlerRegistry->handlerFor($extension);
247 2
            } catch (\Throwable $e) {
248 2
                yield $e;
249
            }
250
        }
251 7
    }
252
253 7
    private function renderIssues(SymfonyStyle $io, \Throwable ...$issues): void
254
    {
255 7
        foreach ($issues as $issue) {
256 4
            if (OutputInterface::VERBOSITY_NORMAL === $io->getVerbosity()) {
257 3
                $io->error($issue->getMessage());
258
259 3
                continue;
260
            }
261
262
            // BC - Symfony 4.4 deprecated Application::renderException()
263 1
            if (\method_exists($this->getApplication(), 'renderThrowable')) {
264 1
                $this->getApplication()->renderThrowable($issue, $io);
265
            } else {
266
                $this->getApplication()->renderException($issue, $io);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

266
                $this->getApplication()->/** @scrutinizer ignore-call */ renderException($issue, $io);

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...
267
            }
268
        }
269 7
    }
270
271 7
    private function renderFrequency(Task $task): string
272
    {
273 7
        $expression = (string) $task->getExpression();
274
275 7
        if (!\class_exists(CronTranslator::class)) {
276
            return $expression;
277
        }
278
279
        try {
280 7
            return \sprintf('%s (%s)', $expression, CronTranslator::translate($expression));
281 1
        } catch (CronParsingException $e) {
282 1
            return $expression;
283
        }
284
    }
285
}
286