Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Command
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
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
 full
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
 title
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 of
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use Attribute;
8use ReflectionClass;
9use ValueError;
10
11/**
12 * Declares a class as a console command.
13 *
14 * The name consists of letters, digits, `.`, `_`, and `-`, starts with a
15 * letter or digit, and is matched case-insensitively. It may carry a group
16 * prefix of the same form, separated by a single colon, e.g. `db:migrate`;
17 * any other name is rejected.
18 * The prefix namespaces the command on the command line. The help overview
19 * lists the command under its `group`, by default the capitalized prefix or,
20 * without one, General; commands with the same group share a section.
21 *
22 * The attribute and its properties are public API; the methods serve the
23 * runner.
24 *
25 * @api
26 */
27#[Attribute(Attribute::TARGET_CLASS)]
28final class Command
29{
30    /**
31     * A single colon only: a second one would collide with the prefixed
32     * lookup of another command.
33     */
34    private const string NAME = '/^(?:([a-z0-9][a-z0-9._-]*):)?([a-z0-9][a-z0-9._-]*)\z/';
35
36    public readonly string $name;
37    public readonly string $prefix;
38
39    public function __construct(
40        string $name,
41        public readonly string $description = '',
42        public readonly string $group = '',
43    ) {
44        if (preg_match(self::NAME, strtolower($name), $parts) !== 1) {
45            throw new ValueError("Invalid command name '{$name}'");
46        }
47
48        $this->prefix = $parts[1];
49        $this->name = $parts[2];
50    }
51
52    /** @internal */
53    public function full(): string
54    {
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
56    }
57
58    /** @internal */
59    public function title(): string
60    {
61        if ($this->group !== '') {
62            return $this->group;
63        }
64
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
66    }
67
68    /**
69     * Reads the attribute off a command instance or class.
70     *
71     * @internal
72     *
73     * @param class-string|object $command
74     */
75    public static function of(object|string $command): self
76    {
77        $class = is_object($command) ? $command::class : $command;
78        $attributes = new ReflectionClass($class)->getAttributes(self::class);
79
80        if ($attributes === []) {
81            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
82        }
83
84        return $attributes[0]->newInstance();
85    }
86}

Paths

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

Command->__construct
40        string $name,
41        public readonly string $description = '',
42        public readonly string $group = '',
43    ) {
44        if (preg_match(self::NAME, strtolower($name), $parts) !== 1) {
 
45            throw new ValueError("Invalid command name '{$name}'");
40        string $name,
41        public readonly string $description = '',
42        public readonly string $group = '',
43    ) {
44        if (preg_match(self::NAME, strtolower($name), $parts) !== 1) {
 
48        $this->prefix = $parts[1];
49        $this->name = $parts[2];
50    }
Command->full
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
56    }
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
55        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
56    }
Command->of
75    public static function of(object|string $command): self
76    {
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
78        $attributes = new ReflectionClass($class)->getAttributes(self::class);
79
80        if ($attributes === []) {
 
81            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
75    public static function of(object|string $command): self
76    {
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
78        $attributes = new ReflectionClass($class)->getAttributes(self::class);
79
80        if ($attributes === []) {
 
84        return $attributes[0]->newInstance();
85    }
75    public static function of(object|string $command): self
76    {
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
78        $attributes = new ReflectionClass($class)->getAttributes(self::class);
79
80        if ($attributes === []) {
 
81            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
75    public static function of(object|string $command): self
76    {
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
 
77        $class = is_object($command) ? $command::class : $command;
78        $attributes = new ReflectionClass($class)->getAttributes(self::class);
79
80        if ($attributes === []) {
 
84        return $attributes[0]->newInstance();
85    }
Command->title
61        if ($this->group !== '') {
 
62            return $this->group;
61        if ($this->group !== '') {
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
66    }
61        if ($this->group !== '') {
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
65        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
66    }