Passed
Push — master ( 81662d...79e5c8 )
by Kevin
02:02
created

ScheduleListCommand::getTaskIssues()   B

Complexity

Conditions 7
Paths 36

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

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

241
                $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...
242
            }
243
        }
244 4
    }
245
246 4
    private function renderFrequency(Task $task): string
247
    {
248 4
        if (!\class_exists(CronTranslator::class)) {
249
            return $task->getExpression();
250
        }
251
252 4
        return CronTranslator::translate($task->getExpression())." ({$task->getExpression()})";
253
    }
254
}
255