Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
62 / 62 |
|
97.47% |
77 / 79 |
|
14.36% |
29 / 202 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Help | |
100.00% |
62 / 62 |
|
97.47% |
77 / 79 |
|
14.36% |
29 / 202 |
|
100.00% |
8 / 8 |
804.52 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| showFor | |
100.00% |
12 / 12 |
|
100.00% |
13 / 13 |
|
25.00% |
4 / 16 |
|
100.00% |
1 / 1 |
15.55 | |||
| showSummary | |
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| showArguments | |
100.00% |
6 / 6 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| showOptions | |
100.00% |
19 / 19 |
|
100.00% |
18 / 18 |
|
26.67% |
4 / 15 |
|
100.00% |
1 / 1 |
33.24 | |||
| argumentName | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| showDescription | |
100.00% |
11 / 11 |
|
90.48% |
19 / 21 |
|
4.86% |
7 / 144 |
|
100.00% |
1 / 1 |
49.20 | |||
| text | |
100.00% |
7 / 7 |
|
100.00% |
13 / 13 |
|
38.89% |
7 / 18 |
|
100.00% |
1 / 1 |
18.18 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Console; |
| 6 | |
| 7 | use BackedEnum; |
| 8 | use 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 | */ |
| 16 | final 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 | } |
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.
| 19 | private readonly Io $io, |
| 20 | private readonly string $script, |
| 21 | ) {} |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |