Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
103 / 103
96.34% covered (success)
96.34%
79 / 82
22.92% covered (danger)
22.92%
22 / 96
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Runner
100.00% covered (success)
100.00%
103 / 103
96.34% covered (success)
96.34%
79 / 82
22.92% covered (danger)
22.92%
22 / 96
100.00% covered (success)
100.00%
10 / 10
735.64
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orderCommands
100.00% covered (success)
100.00%
21 / 21
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 36
100.00% covered (success)
100.00%
1 / 1
7
 showHelp
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
7 / 7
20.00% covered (danger)
20.00%
1 / 5
100.00% covered (success)
100.00%
1 / 1
7.61
 showCommands
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
14 / 14
0.00% covered (danger)
0.00%
0 / 24
100.00% covered (success)
100.00%
1 / 1
6
 run
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
17 / 17
90.00% covered (success)
90.00%
9 / 10
100.00% covered (success)
100.00%
1 / 1
9.08
 echoGroup
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echoCommand
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 showAmbiguousMessage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 getCommand
100.00% covered (success)
100.00%
9 / 9
92.31% covered (success)
92.31%
12 / 13
41.67% covered (danger)
41.67%
5 / 12
100.00% covered (success)
100.00%
1 / 1
13.15
 runCommand
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Cli;
6
7use Throwable;
8use ValueError;
9
10/**
11 * @api
12 */
13final class Runner
14{
15    protected const AMBIGUOUS = 1;
16    protected const NOTFOUND = 2;
17
18    // The commands ordered by group and name
19    private array $toc = [];
20
21    // The commands indexed by name only
22    private array $list = [];
23    private Output $output;
24    private int $longestName = 0;
25
26    public function __construct(
27        Commands $commands,
28        string $output = 'php://output',
29        private bool $debug = false,
30    ) {
31        $this->output = new Output($output);
32        $this->orderCommands($commands);
33    }
34
35    public function orderCommands(Commands $commands): void
36    {
37        $groups = [];
38
39        foreach ($commands->get() as $command) {
40            $name = strtolower($command->name());
41            $prefix = $command->prefix();
42
43            if (!array_key_exists($prefix, $groups)) {
44                $group = $command->group();
45                $group = $group === '' ? 'General' : $group;
46                $groups[$prefix] = [
47                    'title' => $prefix === '' ? 'General' : $group,
48                    'commands' => [],
49                ];
50            }
51
52            $groups[$prefix]['commands'][$name] = $command;
53
54            $this->list[$name][] = $command;
55
56            $len = strlen($prefix . ':' . $command->name());
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
58        }
59
60        ksort($groups);
61
62        foreach ($groups as $name => $group) {
63            $commands = $group['commands'];
64            ksort($commands);
65            $group['commands'] = $commands;
66            $this->toc[$name] = $group;
67        }
68    }
69
70    public function showHelp(): int
71    {
72        $script = $_SERVER['argv'][0] ?? '';
73        $this->output->echo($this->output->color('Usage:', 'brown') . "\n");
74        $this->output->echo("  php {$script} [prefix:]command [arguments]\n\n");
75        $this->output->echo("Prefixes are optional if the command is unambiguous.\n\n");
76        $this->output->echo("Available commands:\n");
77        $this->echoGroup('General');
78        $this->echoCommand('', 'commands', 'Lists all available commands');
79        $this->echoCommand('', 'help', 'Displays this overview');
80
81        foreach ($this->toc as $group) {
82            $this->echoGroup($group['title']);
83
84            foreach ($group['commands'] as $name => $command) {
85                $this->echoCommand($command->prefix(), $name, $command->description());
86            }
87        }
88
89        return 0;
90    }
91
92    /**
93     * Displays a list of all available commands.
94     *
95     * With and without namespace/group. If a command appears in more than
96     * one namespace, e. g. foo:cmd and bar:cmd, only the namespaced ones
97     * will be displayed.
98     */
99    public function showCommands(): int
100    {
101        $list = [];
102
103        foreach ($this->toc as $group) {
104            foreach ($group['commands'] as $command) {
105                $prefix = $command->prefix();
106
107                if ($prefix) {
108                    $key = "{$prefix}:" . $command->name();
109                    $list[$key] = ($list[$key] ?? 0) + 1;
110                }
111
112                $name = $command->name();
113                $list[$name] = ($list[$name] ?? 0) + 1;
114            }
115        }
116
117        ksort($list);
118
119        foreach ($list as $name => $count) {
120            if ($count === 1) {
121                $this->output->echo("{$name}\n");
122            }
123        }
124
125        return 0;
126    }
127
128    public function run(): int|string
129    {
130        try {
131            $argv = $_SERVER['argv'] ?? [];
132
133            $arg = $argv[1] ?? null;
134
135            if ($arg !== null) {
136                $cmd = strtolower($arg);
137                $isHelpCall = false;
138
139                if ($cmd === 'help') {
140                    $isHelpCall = true;
141
142                    $arg = $argv[2] ?? null;
143
144                    if ($arg === null) {
145                        return $this->showHelp();
146                    }
147
148                    $cmd = strtolower($arg);
149                }
150
151                if ($cmd === 'commands') {
152                    return $this->showCommands();
153                }
154
155                try {
156                    return $this->runCommand($this->getCommand($cmd), $isHelpCall);
157                } catch (ValueError $e) {
158                    if ($e->getCode() === self::AMBIGUOUS) {
159                        return $this->showAmbiguousMessage($cmd);
160                    }
161
162                    throw $e;
163                }
164            }
165
166            return $this->showHelp();
167        } catch (Throwable $e) {
168            $this->output->echo("Error while running command '");
169            $this->output->echo($_SERVER['argv'][1] ?? '<no command given>');
170            $this->output->echo("':\n\n" . $e->getMessage() . "\n");
171
172            if ($this->debug) {
173                $this->output->echoln("\nTraceback:", 'yellow');
174                $this->output->echoln($e->getTraceAsString());
175            }
176
177            return 1;
178        }
179    }
180
181    private function echoGroup(string $title): void
182    {
183        $g = $this->output->color($title, 'brown');
184        $this->output->echo("\n{$g}\n");
185    }
186
187    private function echoCommand(string $prefix, string $name, string $desc): void
188    {
189        $prefix = $prefix ? $prefix . ':' : '';
190        $name = $this->output->color($name, 'green');
191
192        // The added magic number takes colorization into
193        // account as it lengthens the string.
194        $prefixedName = str_pad($prefix . $name, $this->longestName + 13);
195        $this->output->echoln("  {$prefixedName}{$desc}");
196    }
197
198    private function showAmbiguousMessage(string $cmd): int
199    {
200        $this->output->echo("Ambiguous command. Please add the group name:\n\n");
201        asort($this->list[$cmd]);
202
203        foreach ($this->list[$cmd] as $command) {
204            $prefix = $this->output->color($command->prefix(), 'brown');
205            $name = strtolower($command->name());
206            $this->output->echoln("  {$prefix}:{$name}");
207        }
208
209        return 1;
210    }
211
212    private function getCommand(string $cmd): Command
213    {
214        if (array_key_exists($cmd, $this->list)) {
215            if (count($this->list[$cmd]) === 1) {
216                return $this->list[$cmd][0];
217            }
218
219            throw new ValueError('Ambiguous command', self::AMBIGUOUS);
220        }
221
222        if (str_contains($cmd, ':')) {
223            [$group, $name] = explode(':', $cmd);
224
225            if (
226                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
227            ) {
228                return $this->toc[$group]['commands'][$name];
229            }
230        }
231
232        throw new ValueError('Command not found', self::NOTFOUND);
233    }
234
235    private function runCommand(Command $command, bool $isHelpCall): int|string
236    {
237        if ($isHelpCall) {
238            $command->output($this->output)->help();
239
240            return 0;
241        }
242
243        return $command->output($this->output)->run();
244    }
245}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Runner->__construct
27        Commands $commands,
28        string $output = 'php://output',
29        private bool $debug = false,
30    ) {
31        $this->output = new Output($output);
32        $this->orderCommands($commands);
33    }
Runner->echoCommand
187    private function echoCommand(string $prefix, string $name, string $desc): void
188    {
189        $prefix = $prefix ? $prefix . ':' : '';
189        $prefix = $prefix ? $prefix . ':' : '';
189        $prefix = $prefix ? $prefix . ':' : '';
189        $prefix = $prefix ? $prefix . ':' : '';
190        $name = $this->output->color($name, 'green');
191
192        // The added magic number takes colorization into
193        // account as it lengthens the string.
194        $prefixedName = str_pad($prefix . $name, $this->longestName + 13);
195        $this->output->echoln("  {$prefixedName}{$desc}");
196    }
Runner->echoGroup
181    private function echoGroup(string $title): void
182    {
183        $g = $this->output->color($title, 'brown');
184        $this->output->echo("\n{$g}\n");
185    }
Runner->getCommand
212    private function getCommand(string $cmd): Command
213    {
214        if (array_key_exists($cmd, $this->list)) {
215            if (count($this->list[$cmd]) === 1) {
216                return $this->list[$cmd][0];
219            throw new ValueError('Ambiguous command', self::AMBIGUOUS);
222        if (str_contains($cmd, ':')) {
222        if (str_contains($cmd, ':')) {
222        if (str_contains($cmd, ':')) {
222        if (str_contains($cmd, ':')) {
223            [$group, $name] = explode(':', $cmd);
224
225            if (
226                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
226                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
226                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
228                return $this->toc[$group]['commands'][$name];
232        throw new ValueError('Command not found', self::NOTFOUND);
233    }
Runner->orderCommands
35    public function orderCommands(Commands $commands): void
36    {
37        $groups = [];
38
39        foreach ($commands->get() as $command) {
39        foreach ($commands->get() as $command) {
40            $name = strtolower($command->name());
41            $prefix = $command->prefix();
42
43            if (!array_key_exists($prefix, $groups)) {
44                $group = $command->group();
45                $group = $group === '' ? 'General' : $group;
45                $group = $group === '' ? 'General' : $group;
45                $group = $group === '' ? 'General' : $group;
45                $group = $group === '' ? 'General' : $group;
46                $groups[$prefix] = [
47                    'title' => $prefix === '' ? 'General' : $group,
47                    'title' => $prefix === '' ? 'General' : $group,
47                    'title' => $prefix === '' ? 'General' : $group,
47                    'title' => $prefix === '' ? 'General' : $group,
48                    'commands' => [],
49                ];
50            }
51
52            $groups[$prefix]['commands'][$name] = $command;
52            $groups[$prefix]['commands'][$name] = $command;
53
54            $this->list[$name][] = $command;
55
56            $len = strlen($prefix . ':' . $command->name());
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
39        foreach ($commands->get() as $command) {
40            $name = strtolower($command->name());
41            $prefix = $command->prefix();
42
43            if (!array_key_exists($prefix, $groups)) {
44                $group = $command->group();
45                $group = $group === '' ? 'General' : $group;
46                $groups[$prefix] = [
47                    'title' => $prefix === '' ? 'General' : $group,
48                    'commands' => [],
49                ];
50            }
51
52            $groups[$prefix]['commands'][$name] = $command;
53
54            $this->list[$name][] = $command;
55
56            $len = strlen($prefix . ':' . $command->name());
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
39        foreach ($commands->get() as $command) {
40            $name = strtolower($command->name());
41            $prefix = $command->prefix();
42
43            if (!array_key_exists($prefix, $groups)) {
44                $group = $command->group();
45                $group = $group === '' ? 'General' : $group;
46                $groups[$prefix] = [
47                    'title' => $prefix === '' ? 'General' : $group,
48                    'commands' => [],
49                ];
50            }
51
52            $groups[$prefix]['commands'][$name] = $command;
53
54            $this->list[$name][] = $command;
55
56            $len = strlen($prefix . ':' . $command->name());
57            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
58        }
59
60        ksort($groups);
61
62        foreach ($groups as $name => $group) {
62        foreach ($groups as $name => $group) {
62        foreach ($groups as $name => $group) {
62        foreach ($groups as $name => $group) {
63            $commands = $group['commands'];
64            ksort($commands);
65            $group['commands'] = $commands;
66            $this->toc[$name] = $group;
67        }
68    }
Runner->run
130        try {
131            $argv = $_SERVER['argv'] ?? [];
132
133            $arg = $argv[1] ?? null;
134
135            if ($arg !== null) {
136                $cmd = strtolower($arg);
137                $isHelpCall = false;
138
139                if ($cmd === 'help') {
140                    $isHelpCall = true;
141
142                    $arg = $argv[2] ?? null;
143
144                    if ($arg === null) {
145                        return $this->showHelp();
148                    $cmd = strtolower($arg);
149                }
150
151                if ($cmd === 'commands') {
151                if ($cmd === 'commands') {
152                    return $this->showCommands();
155                try {
156                    return $this->runCommand($this->getCommand($cmd), $isHelpCall);
157                } catch (ValueError $e) {
158                    if ($e->getCode() === self::AMBIGUOUS) {
159                        return $this->showAmbiguousMessage($cmd);
162                    throw $e;
166            return $this->showHelp();
167        } catch (Throwable $e) {
168            $this->output->echo("Error while running command '");
169            $this->output->echo($_SERVER['argv'][1] ?? '<no command given>');
170            $this->output->echo("':\n\n" . $e->getMessage() . "\n");
171
172            if ($this->debug) {
173                $this->output->echoln("\nTraceback:", 'yellow');
174                $this->output->echoln($e->getTraceAsString());
175            }
176
177            return 1;
177            return 1;
178        }
179    }
Runner->runCommand
235    private function runCommand(Command $command, bool $isHelpCall): int|string
236    {
237        if ($isHelpCall) {
238            $command->output($this->output)->help();
239
240            return 0;
243        return $command->output($this->output)->run();
244    }
Runner->showAmbiguousMessage
198    private function showAmbiguousMessage(string $cmd): int
199    {
200        $this->output->echo("Ambiguous command. Please add the group name:\n\n");
201        asort($this->list[$cmd]);
202
203        foreach ($this->list[$cmd] as $command) {
203        foreach ($this->list[$cmd] as $command) {
203        foreach ($this->list[$cmd] as $command) {
204            $prefix = $this->output->color($command->prefix(), 'brown');
203        foreach ($this->list[$cmd] as $command) {
204            $prefix = $this->output->color($command->prefix(), 'brown');
205            $name = strtolower($command->name());
206            $this->output->echoln("  {$prefix}:{$name}");
207        }
208
209        return 1;
210    }
Runner->showCommands
101        $list = [];
102
103        foreach ($this->toc as $group) {
103        foreach ($this->toc as $group) {
104            foreach ($group['commands'] as $command) {
104            foreach ($group['commands'] as $command) {
105                $prefix = $command->prefix();
106
107                if ($prefix) {
108                    $key = "{$prefix}:" . $command->name();
109                    $list[$key] = ($list[$key] ?? 0) + 1;
110                }
111
112                $name = $command->name();
104            foreach ($group['commands'] as $command) {
105                $prefix = $command->prefix();
106
107                if ($prefix) {
108                    $key = "{$prefix}:" . $command->name();
109                    $list[$key] = ($list[$key] ?? 0) + 1;
110                }
111
112                $name = $command->name();
103        foreach ($this->toc as $group) {
104            foreach ($group['commands'] as $command) {
103        foreach ($this->toc as $group) {
104            foreach ($group['commands'] as $command) {
105                $prefix = $command->prefix();
106
107                if ($prefix) {
108                    $key = "{$prefix}:" . $command->name();
109                    $list[$key] = ($list[$key] ?? 0) + 1;
110                }
111
112                $name = $command->name();
113                $list[$name] = ($list[$name] ?? 0) + 1;
114            }
115        }
116
117        ksort($list);
118
119        foreach ($list as $name => $count) {
119        foreach ($list as $name => $count) {
119        foreach ($list as $name => $count) {
120            if ($count === 1) {
119        foreach ($list as $name => $count) {
120            if ($count === 1) {
121                $this->output->echo("{$name}\n");
119        foreach ($list as $name => $count) {
119        foreach ($list as $name => $count) {
120            if ($count === 1) {
121                $this->output->echo("{$name}\n");
122            }
123        }
124
125        return 0;
126    }
Runner->showHelp
72        $script = $_SERVER['argv'][0] ?? '';
73        $this->output->echo($this->output->color('Usage:', 'brown') . "\n");
74        $this->output->echo("  php {$script} [prefix:]command [arguments]\n\n");
75        $this->output->echo("Prefixes are optional if the command is unambiguous.\n\n");
76        $this->output->echo("Available commands:\n");
77        $this->echoGroup('General');
78        $this->echoCommand('', 'commands', 'Lists all available commands');
79        $this->echoCommand('', 'help', 'Displays this overview');
80
81        foreach ($this->toc as $group) {
81        foreach ($this->toc as $group) {
82            $this->echoGroup($group['title']);
83
84            foreach ($group['commands'] as $name => $command) {
84            foreach ($group['commands'] as $name => $command) {
84            foreach ($group['commands'] as $name => $command) {
81        foreach ($this->toc as $group) {
82            $this->echoGroup($group['title']);
83
84            foreach ($group['commands'] as $name => $command) {
81        foreach ($this->toc as $group) {
82            $this->echoGroup($group['title']);
83
84            foreach ($group['commands'] as $name => $command) {
85                $this->echoCommand($command->prefix(), $name, $command->description());
86            }
87        }
88
89        return 0;
90    }