Passed
Pull Request — master (#3)
by Kevin
02:01
created

ScheduleListCommand   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Test Coverage

Coverage 93.55%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 45
eloc 118
c 2
b 0
f 1
dl 0
loc 251
ccs 116
cts 124
cp 0.9355
rs 8.8

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A execute() 0 27 6
A configure() 0 6 1
A extensionHighlightMap() 0 7 1
A renderFrequency() 0 7 2
A getScheduleIssues() 0 7 3
A renderExtenstions() 0 15 3
B renderDetail() 0 37 9
A renderDefinitionList() 0 13 2
A renderIssues() 0 14 4
B getTaskIssues() 0 25 7
A renderTable() 0 32 6

How to fix   Complexity   

Complex Class

Complex classes like ScheduleListCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ScheduleListCommand, and based on these observations, apply Extract Interface, too.

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

260
                $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...
261
            }
262
        }
263 5
    }
264
265 5
    private function renderFrequency(Task $task): string
266
    {
267 5
        if (!\class_exists(CronTranslator::class)) {
268
            return $task->getExpression();
269
        }
270
271 5
        return \sprintf('%s (%s)', $task->getExpression(), CronTranslator::translate($task->getExpression()));
272
    }
273
}
274