Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
21 / 21
93.33% covered (success)
93.33%
28 / 30
48.39% covered (danger)
48.39%
15 / 31
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Command
100.00% covered (success)
100.00%
21 / 21
93.33% covered (success)
93.33%
28 / 30
48.39% covered (danger)
48.39%
15 / 31
100.00% covered (success)
100.00%
4 / 4
36.24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
85.71% covered (warning)
85.71%
12 / 14
27.27% covered (danger)
27.27%
6 / 22
100.00% covered (success)
100.00%
1 / 1
14.62
 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 may carry a group prefix separated by a single colon, e.g.
15 * `db:migrate`; further colons are rejected.
16 * The prefix namespaces the command on the command line and groups it in the
17 * help overview under the capitalized prefix; `group` overrides that
18 * displayed title.
19 *
20 * @api
21 */
22#[Attribute(Attribute::TARGET_CLASS)]
23final class Command
24{
25    public readonly string $name;
26    public readonly string $prefix;
27
28    public function __construct(
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
38            /** @var array{0: string, 1: string} $parts */
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
45                throw new ValueError("Invalid command name '{$name}'");
46            }
47        }
48
49        if ($bare === '') {
50            throw new ValueError("Invalid command name '{$name}'");
51        }
52
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
56
57    public function full(): string
58    {
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
60    }
61
62    public function title(): string
63    {
64        if ($this->group !== '') {
65            return $this->group;
66        }
67
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
69    }
70
71    /**
72     * Reads the attribute off a command instance or class.
73     *
74     * @param class-string|object $command
75     */
76    public static function of(object|string $command): self
77    {
78        $class = is_object($command) ? $command::class : $command;
79        $attributes = new ReflectionClass($class)->getAttributes(self::class);
80
81        if ($attributes === []) {
82            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
83        }
84
85        return $attributes[0]->newInstance();
86    }
87}

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
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
45                throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
39            $parts = explode(':', $lower, limit: 2);
40            [$prefix, $bare] = $parts;
41
42            // A single colon only: a second one would collide with the
43            // prefixed lookup of another command.
44            if ($prefix === '' || str_contains($bare, ':')) {
 
44            if ($prefix === '' || str_contains($bare, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
49        if ($bare === '') {
 
50            throw new ValueError("Invalid command name '{$name}'");
29        string $name,
30        public readonly string $description = '',
31        public readonly string $group = '',
32    ) {
33        $lower = strtolower($name);
34        $prefix = '';
35        $bare = $lower;
36
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
37        if (str_contains($lower, ':')) {
 
49        if ($bare === '') {
 
53        $this->prefix = $prefix;
54        $this->name = $bare;
55    }
Command->full
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
60    }
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
 
59        return $this->prefix === '' ? $this->name : "{$this->prefix}:{$this->name}";
60    }
Command->of
76    public static function of(object|string $command): self
77    {
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
79        $attributes = new ReflectionClass($class)->getAttributes(self::class);
80
81        if ($attributes === []) {
 
82            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
76    public static function of(object|string $command): self
77    {
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
79        $attributes = new ReflectionClass($class)->getAttributes(self::class);
80
81        if ($attributes === []) {
 
85        return $attributes[0]->newInstance();
86    }
76    public static function of(object|string $command): self
77    {
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
79        $attributes = new ReflectionClass($class)->getAttributes(self::class);
80
81        if ($attributes === []) {
 
82            throw new ValueError("Command class '{$class}' has no #[Command] attribute");
76    public static function of(object|string $command): self
77    {
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
 
78        $class = is_object($command) ? $command::class : $command;
79        $attributes = new ReflectionClass($class)->getAttributes(self::class);
80
81        if ($attributes === []) {
 
85        return $attributes[0]->newInstance();
86    }
Command->title
64        if ($this->group !== '') {
 
65            return $this->group;
64        if ($this->group !== '') {
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
69    }
64        if ($this->group !== '') {
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
 
68        return $this->prefix === '' ? 'General' : ucfirst($this->prefix);
69    }