Passed
Push — master ( b67a30...b7883b )
by Kevin
09:53 queued 08:10
created

ScheduleListCommand   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Test Coverage

Coverage 94.44%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 102
c 1
b 0
f 0
dl 0
loc 218
ccs 102
cts 108
cp 0.9444
rs 9.36

11 Methods

Rating   Name   Duplication   Size   Complexity  
A extensionHighlightMap() 0 7 1
A renderFrequency() 0 7 2
A getScheduleIssues() 0 7 3
A renderExtenstions() 0 15 3
B renderDetail() 0 42 8
A execute() 0 27 5
A __construct() 0 6 1
A configure() 0 5 1
A renderIssues() 0 14 4
A getTaskIssues() 0 13 4
A renderTable() 0 30 6
1
<?php
2
3
namespace Zenstruck\ScheduleBundle\Command;
4
5
use Lorisleiva\CronTranslator\CronTranslator;
6
use Symfony\Component\Console\Command\Command;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Symfony\Component\Console\Style\SymfonyStyle;
10
use Zenstruck\ScheduleBundle\Schedule;
11
use Zenstruck\ScheduleBundle\Schedule\Extension;
12
use Zenstruck\ScheduleBundle\Schedule\Extension\ExtensionHandlerRegistry;
13
use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner;
14
use Zenstruck\ScheduleBundle\Schedule\Task;
15
use Zenstruck\ScheduleBundle\Schedule\Task\CommandTask;
16
17
/**
18
 * @author Kevin Bond <[email protected]>
19
 */
20
final class ScheduleListCommand extends Command
21
{
22
    protected static $defaultName = 'schedule:list';
23
24
    private $scheduleRunner;
25
    private $handlerRegistry;
26
27 4
    public function __construct(ScheduleRunner $scheduleRunner, ExtensionHandlerRegistry $handlerRegistry)
28
    {
29 4
        $this->scheduleRunner = $scheduleRunner;
30 4
        $this->handlerRegistry = $handlerRegistry;
31
32 4
        parent::__construct();
33 4
    }
34
35 4
    protected function configure(): void
36
    {
37
        $this
38 4
            ->setDescription('List configured scheduled tasks')
39 4
            ->addOption('detail', null, null, 'Show detailed task list')
40
        ;
41 4
    }
42
43 4
    protected function execute(InputInterface $input, OutputInterface $output): int
44
    {
45 4
        $schedule = $this->scheduleRunner->buildSchedule();
46
47 4
        if (0 === \count($schedule->all())) {
48 1
            throw new \RuntimeException('No scheduled tasks configured.');
49
        }
50
51 3
        $io = new SymfonyStyle($input, $output);
52
53 3
        $io->title(\sprintf('<info>%d</info> Scheduled Tasks Configured', \count($schedule->all())));
54
55 3
        $exit = $input->getOption('detail') ? $this->renderDetail($schedule, $io) : $this->renderTable($schedule, $io);
56
57 3
        $this->renderExtenstions($io, 'Schedule', $schedule->getExtensions());
58
59 3
        $scheduleIssues = \iterator_to_array($this->getScheduleIssues($schedule));
60
61 3
        if ($issueCount = \count($scheduleIssues)) {
62 2
            $io->warning(\sprintf('%d issue%s with schedule:', $issueCount, $issueCount > 1 ? 's' : ''));
63
64 2
            $exit = 1;
65
        }
66
67 3
        $this->renderIssues($io, ...$scheduleIssues);
68
69 3
        return $exit;
70
    }
71
72 1
    private function renderDetail(Schedule $schedule, SymfonyStyle $io): int
73
    {
74 1
        $exit = 0;
75
76 1
        foreach ($schedule->all() as $i => $task) {
77 1
            $io->section(\sprintf('(%d/%d) %s: %s', $i + 1, \count($schedule->all()), $task->getType(), $task));
78
79 1
            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...
80 1
                $io->comment("Arguments: <comment>{$task->getArguments()}</comment>");
81
            }
82
83
            // BC - Symfony 4.4 added SymfonyStyle::definitionList()
84 1
            if (\method_exists($io, 'definitionList')) {
85 1
                $io->definitionList(
86 1
                    ['Class' => \get_class($task)],
87 1
                    ['Frequency' => $this->renderFrequency($task)],
88 1
                    ['Next Run' => $task->getNextRun()->format('D, M d, Y @ g:i (e O)')]
89
                );
90
            } else {
91
                $io->listing([
92
                    'Class: '.\get_class($task),
93
                    'Frequency: '.$this->renderFrequency($task),
94
                    'Next Run: '.$task->getNextRun()->format('D, M d, Y @ g:i (e O)'),
95
                ]);
96
            }
97
98 1
            $this->renderExtenstions($io, 'Task', $task->getExtensions());
99
100 1
            $issues = \iterator_to_array($this->getTaskIssues($task));
101
102 1
            if ($issueCount = \count($issues)) {
103 1
                $io->warning(\sprintf('%d issue%s with this task:', $issueCount, $issueCount > 1 ? 's' : ''));
104
            }
105
106 1
            $this->renderIssues($io, ...$issues);
107
108 1
            if ($issueCount > 0) {
109 1
                $exit = 1;
110
            }
111
        }
112
113 1
        return $exit;
114
    }
115
116 2
    private function renderTable(Schedule $schedule, SymfonyStyle $io): int
117
    {
118
        /** @var \Throwable[] $taskIssues */
119 2
        $taskIssues = [];
120 2
        $rows = [];
121
122 2
        foreach ($schedule->all() as $task) {
123 2
            $issues = \iterator_to_array($this->getTaskIssues($task));
124 2
            $taskIssues[] = $issues;
125
126 2
            $rows[] = [
127 2
                \count($issues) ? "<error>[!] {$task->getType()}</error>" : $task->getType(),
128 2
                $this->getHelper('formatter')->truncate($task, 50),
129 2
                \count($task->getExtensions()),
130 2
                $this->renderFrequency($task),
131 2
                $task->getNextRun()->format(DATE_ATOM),
132
            ];
133
        }
134
135 2
        $taskIssues = \array_merge([], ...$taskIssues);
136
137 2
        $io->table(['Type', 'Task', 'Extensions', 'Frequency', 'Next Run'], $rows);
138
139 2
        if ($issueCount = \count($taskIssues)) {
140 2
            $io->warning(\sprintf('%d task issue%s:', $issueCount, $issueCount > 1 ? 's' : ''));
141
        }
142
143 2
        $this->renderIssues($io, ...$taskIssues);
144
145 2
        return \count($taskIssues) ? 1 : 0;
146
    }
147
148
    /**
149
     * @param Extension[] $extensions
150
     */
151 3
    private function renderExtenstions(SymfonyStyle $io, string $type, array $extensions): void
152
    {
153 3
        if (0 === $count = \count($extensions)) {
154 1
            return;
155
        }
156
157 2
        $io->comment(\sprintf('<info>%d</info> %s Extension%s:', $count, $type, $count > 1 ? 's' : ''));
158 2
        $io->listing(\array_map(
159
            function (Extension $extension) {
160 2
                return \sprintf('%s <comment>(%s)</comment>',
161 2
                    \strtr($extension, self::extensionHighlightMap()),
162 2
                    \get_class($extension)
163
                );
164 2
            },
165 2
            $extensions
166
        ));
167 2
    }
168
169
    /**
170
     * @return \Throwable[]
171
     */
172 3
    private function getScheduleIssues(Schedule $schedule): \Generator
173
    {
174 3
        foreach ($schedule->getExtensions() as $extension) {
175
            try {
176 2
                $this->handlerRegistry->handlerFor($extension);
177 2
            } catch (\Throwable $e) {
178 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...
179
            }
180
        }
181 3
    }
182
183 2
    private static function extensionHighlightMap(): array
184
    {
185
        return [
186 2
            Extension::TASK_SUCCESS => \sprintf('<info>%s</info>', Extension::TASK_SUCCESS),
187 2
            Extension::SCHEDULE_SUCCESS => \sprintf('<info>%s</info>', Extension::SCHEDULE_SUCCESS),
188 2
            Extension::TASK_FAILURE => \sprintf('<error>%s</error>', Extension::TASK_FAILURE),
189 2
            Extension::SCHEDULE_FAILURE => \sprintf('<error>%s</error>', Extension::SCHEDULE_FAILURE),
190
        ];
191
    }
192
193
    /**
194
     * @return \Throwable[]
195
     */
196 3
    private function getTaskIssues(Task $task): \Generator
197
    {
198
        try {
199 3
            $this->scheduleRunner->runnerFor($task);
200 3
        } catch (\Throwable $e) {
201 3
            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...
202
        }
203
204 3
        foreach ($task->getExtensions() as $extension) {
205
            try {
206 2
                $this->handlerRegistry->handlerFor($extension);
207 2
            } catch (\Throwable $e) {
208 2
                yield $e;
209
            }
210
        }
211 3
    }
212
213 3
    private function renderIssues(SymfonyStyle $io, \Throwable ...$issues): void
214
    {
215 3
        foreach ($issues as $issue) {
216 3
            if (OutputInterface::VERBOSITY_NORMAL === $io->getVerbosity()) {
217 2
                $io->error($issue->getMessage());
218
219 2
                continue;
220
            }
221
222
            // BC - Symfony 4.4 deprecated Application::renderException()
223 1
            if (\method_exists($this->getApplication(), 'renderThrowable')) {
224 1
                $this->getApplication()->renderThrowable($issue, $io);
225
            } else {
226
                $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

226
                $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...
227
            }
228
        }
229 3
    }
230
231 3
    private function renderFrequency(Task $task): string
232
    {
233 3
        if (!\class_exists(CronTranslator::class)) {
234
            return $task->getExpression();
235
        }
236
237 3
        return CronTranslator::translate($task->getExpression())." ({$task->getExpression()})";
238
    }
239
}
240