Passed
Push — master ( 3ce531...7a6df5 )
by Kevin
02:21
created

ScheduleListCommand   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 283
Duplicated Lines 0 %

Test Coverage

Coverage 95%

Importance

Changes 3
Bugs 0 Features 2
Metric Value
wmc 49
eloc 135
c 3
b 0
f 2
dl 0
loc 283
ccs 133
cts 140
cp 0.95
rs 8.48

12 Methods

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

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

288
                $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...
289
            }
290
        }
291 9
    }
292
293 9
    private function renderFrequency(Task $task): string
294
    {
295 9
        $expression = (string) $task->getExpression();
296
297 9
        if (!\class_exists(CronTranslator::class)) {
298
            return $expression;
299
        }
300
301
        try {
302 9
            return \sprintf('%s (%s)', $expression, CronTranslator::translate($expression));
303 2
        } catch (CronParsingException $e) {
304 2
            return $expression;
305
        }
306
    }
307
}
308