Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
97.47% covered (success)
97.47%
77 / 79
14.36% covered (danger)
14.36%
29 / 202
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Help
100.00% covered (success)
100.00%
62 / 62
97.47% covered (success)
97.47%
77 / 79
14.36% covered (danger)
14.36%
29 / 202
100.00% covered (success)
100.00%
8 / 8
804.52
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
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
 showFor
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
13 / 13
25.00% covered (danger)
25.00%
4 / 16
100.00% covered (success)
100.00%
1 / 1
15.55
 showSummary
100.00% covered (success)
100.00%
5 / 5
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
 showArguments
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 showOptions
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
18 / 18
26.67% covered (danger)
26.67%
4 / 15
100.00% covered (success)
100.00%
1 / 1
33.24
 argumentName
100.00% covered (success)
100.00%
1 / 1
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
 showDescription
100.00% covered (success)
100.00%
11 / 11
90.48% covered (success)
90.48%
19 / 21
4.86% covered (danger)
4.86%
7 / 144
100.00% covered (success)
100.00%
1 / 1
49.20
 text
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
13 / 13
38.89% covered (danger)
38.89%
7 / 18
100.00% covered (success)
100.00%
1 / 1
18.18
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use BackedEnum;
8use ReflectionParameter;
9
10/**
11 * Renders a command's help screen from its declaration, for the runner's
12 * `help <command>` and `--help`.
13 *
14 * @internal
15 */
16final class Help
17{
18    public function __construct(
19        private readonly Io $io,
20        private readonly string $script,
21    ) {}
22
23    /**
24     * Renders help for a command instance or class from its `#[Command]`
25     * attribute and `__invoke()` signature.
26     *
27     * @param class-string|object $command
28     */
29    public function showFor(object|string $command): void
30    {
31        $class = is_object($command) ? $command::class : $command;
32        $meta = Command::of($class);
33        $signature = Signature::of($class, $meta->full());
34        $arguments = $signature->arguments();
35        $options = $signature->options();
36
37        $usage = $meta->full();
38
39        foreach ($arguments as $argument) {
40            $name = $this->argumentName($argument);
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
42        }
43
44        $this->showSummary($meta->description, $usage . ($options === [] ? '' : ' [options]'));
45        $this->showArguments($arguments);
46        $this->showOptions($options);
47    }
48
49    /**
50     * Renders the description, which is markup, and the usage line: the
51     * top of a help screen, and all of it for the runner's built-ins.
52     */
53    public function showSummary(string $description, string $usage): void
54    {
55        if ($description !== '') {
56            $this->io->line('<yellow>Description:</yellow>');
57            $this->io->line('  ' . $description . "\n");
58        }
59
60        $this->io->line('<yellow>Usage:</yellow>');
61        $this->io->line('  php %s %s', $this->script, $usage);
62    }
63
64    /** @param list<Argument> $arguments */
65    private function showArguments(array $arguments): void
66    {
67        if ($arguments === []) {
68            return;
69        }
70
71        $this->io->line("\n<yellow>Arguments:</yellow>");
72
73        foreach ($arguments as $argument) {
74            $this->io->line('    <green>%s</green>', $this->argumentName($argument));
75            $this->showDescription($argument->arg->description, $argument->parameter, $argument->type);
76        }
77    }
78
79    /** @param array<string, Option> $options */
80    private function showOptions(array $options): void
81    {
82        if ($options === []) {
83            return;
84        }
85
86        $this->io->line("\n<yellow>Options:</yellow>");
87
88        foreach ($options as $option) {
89            $suffix = match (true) {
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
92                default => "=<{$option->label()}>",
93            };
94
95            $short = $option->opt->short;
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
98
99            $this->io->line('    <green>%s</green>', $flags);
100            $this->showDescription(
101                $option->opt->description,
102                $option->parameter,
103                $option->type,
104                repeatable: $option->type->name === 'array',
105            );
106        }
107    }
108
109    private function argumentName(Argument $argument): string
110    {
111        return "<{$argument->name}>" . ($argument->variadic() ? '...' : '');
112    }
113
114    /**
115     * Renders the description, which is markup, followed by the choices,
116     * the mark of a repeatable option, and the default.
117     */
118    private function showDescription(
119        string $description,
120        ReflectionParameter $parameter,
121        Type $type,
122        bool $repeatable = false,
123    ): void {
124        $parts = $description === '' ? [] : [$description];
125        $choices = $type->choices();
126
127        if ($choices !== []) {
128            $parts[] = '[choices: ' . $this->io->escape(implode(', ', $choices)) . ']';
129        }
130
131        if ($repeatable) {
132            $parts[] = '[repeatable]';
133        }
134
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
136
137        if ($default !== '') {
138            $parts[] = '[default: ' . $this->io->escape($default) . ']';
139        }
140
141        if ($parts !== []) {
142            $this->io->line($this->io->indent(implode(' ', $parts), 8, 80));
143        }
144    }
145
146    /**
147     * Renders a default value; null, false, and arrays render empty.
148     */
149    private function text(mixed $value): string
150    {
151        if ($value instanceof BackedEnum) {
152            $value = $value->value;
153        }
154
155        return match (true) {
156            $value === true => 'true',
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
158            default => '',
159        };
160    }
161}

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.

Help->__construct
19        private readonly Io $io,
20        private readonly string $script,
21    ) {}
Help->argumentName
109    private function argumentName(Argument $argument): string
110    {
111        return "<{$argument->name}>" . ($argument->variadic() ? '...' : '');
111        return "<{$argument->name}>" . ($argument->variadic() ? '...' : '');
111        return "<{$argument->name}>" . ($argument->variadic() ? '...' : '');
111        return "<{$argument->name}>" . ($argument->variadic() ? '...' : '');
112    }
Help->showArguments
65    private function showArguments(array $arguments): void
66    {
67        if ($arguments === []) {
68            return;
71        $this->io->line("\n<yellow>Arguments:</yellow>");
72
73        foreach ($arguments as $argument) {
73        foreach ($arguments as $argument) {
73        foreach ($arguments as $argument) {
74            $this->io->line('    <green>%s</green>', $this->argumentName($argument));
73        foreach ($arguments as $argument) {
74            $this->io->line('    <green>%s</green>', $this->argumentName($argument));
75            $this->showDescription($argument->arg->description, $argument->parameter, $argument->type);
76        }
77    }
Help->showDescription
119        string $description,
120        ReflectionParameter $parameter,
121        Type $type,
122        bool $repeatable = false,
123    ): void {
124        $parts = $description === '' ? [] : [$description];
124        $parts = $description === '' ? [] : [$description];
124        $parts = $description === '' ? [] : [$description];
124        $parts = $description === '' ? [] : [$description];
125        $choices = $type->choices();
126
127        if ($choices !== []) {
128            $parts[] = '[choices: ' . $this->io->escape(implode(', ', $choices)) . ']';
128            $parts[] = '[choices: ' . $this->io->escape(implode(', ', $choices)) . ']';
128            $parts[] = '[choices: ' . $this->io->escape(implode(', ', $choices)) . ']';
128            $parts[] = '[choices: ' . $this->io->escape(implode(', ', $choices)) . ']';
129        }
130
131        if ($repeatable) {
131        if ($repeatable) {
132            $parts[] = '[repeatable]';
133        }
134
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
135        $default = $parameter->isDefaultValueAvailable() ? $this->text($parameter->getDefaultValue()) : '';
136
137        if ($default !== '') {
138            $parts[] = '[default: ' . $this->io->escape($default) . ']';
139        }
140
141        if ($parts !== []) {
141        if ($parts !== []) {
142            $this->io->line($this->io->indent(implode(' ', $parts), 8, 80));
142            $this->io->line($this->io->indent(implode(' ', $parts), 8, 80));
142            $this->io->line($this->io->indent(implode(' ', $parts), 8, 80));
142            $this->io->line($this->io->indent(implode(' ', $parts), 8, 80));
143        }
144    }
144    }
Help->showFor
29    public function showFor(object|string $command): void
30    {
31        $class = is_object($command) ? $command::class : $command;
31        $class = is_object($command) ? $command::class : $command;
31        $class = is_object($command) ? $command::class : $command;
31        $class = is_object($command) ? $command::class : $command;
32        $meta = Command::of($class);
33        $signature = Signature::of($class, $meta->full());
34        $arguments = $signature->arguments();
35        $options = $signature->options();
36
37        $usage = $meta->full();
38
39        foreach ($arguments as $argument) {
39        foreach ($arguments as $argument) {
40            $name = $this->argumentName($argument);
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
39        foreach ($arguments as $argument) {
40            $name = $this->argumentName($argument);
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
39        foreach ($arguments as $argument) {
40            $name = $this->argumentName($argument);
41            $usage .= $argument->optional() ? " [{$name}]" : " {$name}";
42        }
43
44        $this->showSummary($meta->description, $usage . ($options === [] ? '' : ' [options]'));
44        $this->showSummary($meta->description, $usage . ($options === [] ? '' : ' [options]'));
44        $this->showSummary($meta->description, $usage . ($options === [] ? '' : ' [options]'));
44        $this->showSummary($meta->description, $usage . ($options === [] ? '' : ' [options]'));
45        $this->showArguments($arguments);
46        $this->showOptions($options);
47    }
Help->showOptions
80    private function showOptions(array $options): void
81    {
82        if ($options === []) {
83            return;
86        $this->io->line("\n<yellow>Options:</yellow>");
87
88        foreach ($options as $option) {
88        foreach ($options as $option) {
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
92                default => "=<{$option->label()}>",
89            $suffix = match (true) {
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
92                default => "=<{$option->label()}>",
93            };
94
95            $short = $option->opt->short;
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
88        foreach ($options as $option) {
89            $suffix = match (true) {
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
92                default => "=<{$option->label()}>",
93            };
94
95            $short = $option->opt->short;
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
88        foreach ($options as $option) {
89            $suffix = match (true) {
90                $option->type->name === 'bool' => '',
91                $option->opt->bare !== null => "[=<{$option->label()}>]",
92                default => "=<{$option->label()}>",
93            };
94
95            $short = $option->opt->short;
96            $long = $option->negation === null ? $option->name : '--[no-]' . substr($option->name, offset: 2);
97            $flags = $short === '' ? $long . $suffix : "{$short}{$suffix}, {$long}{$suffix}";
98
99            $this->io->line('    <green>%s</green>', $flags);
100            $this->showDescription(
101                $option->opt->description,
102                $option->parameter,
103                $option->type,
104                repeatable: $option->type->name === 'array',
105            );
106        }
107    }
Help->showSummary
53    public function showSummary(string $description, string $usage): void
54    {
55        if ($description !== '') {
56            $this->io->line('<yellow>Description:</yellow>');
57            $this->io->line('  ' . $description . "\n");
58        }
59
60        $this->io->line('<yellow>Usage:</yellow>');
60        $this->io->line('<yellow>Usage:</yellow>');
61        $this->io->line('  php %s %s', $this->script, $usage);
62    }
Help->text
149    private function text(mixed $value): string
150    {
151        if ($value instanceof BackedEnum) {
152            $value = $value->value;
153        }
154
155        return match (true) {
155        return match (true) {
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
156            $value === true => 'true',
157            is_string($value) || is_int($value) || is_float($value) => (string) $value,
158            default => '',
158            default => '',
159        };
160    }