Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
96.88% covered (success)
96.88%
31 / 32
66.67% covered (warning)
66.67%
14 / 21
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Commands
100.00% covered (success)
100.00%
29 / 29
96.88% covered (success)
96.88%
31 / 32
66.67% covered (warning)
66.67%
14 / 21
100.00% covered (success)
100.00%
5 / 5
25.48
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
 add
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
12 / 12
71.43% covered (warning)
71.43%
5 / 7
100.00% covered (success)
100.00%
1 / 1
6.84
 entries
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
 addArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
12 / 12
62.50% covered (warning)
62.50%
5 / 8
100.00% covered (success)
100.00%
1 / 1
7.90
 validClass
100.00% covered (success)
100.00%
3 / 3
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use Closure;
8use ValueError;
9
10/**
11 * Collects command registrations.
12 *
13 * Accepts command instances, class-strings of zero-argument constructible
14 * commands, and lazy factories keyed by class-string:
15 *
16 *     $commands = new Commands([
17 *         new Greet($translator),
18 *         Simple::class,
19 *         Expensive::class => fn() => new Expensive($db),
20 *     ]);
21 *
22 * Commands carry their metadata in a #[Command] attribute. For a
23 * lightweight one-off, register an anonymous class — attributes work
24 * inline:
25 *
26 *     $commands->add(new #[Command('cache:clear', 'Clears the cache')] class {
27 *         public function __invoke(Io $io): int { ... }
28 *     });
29 *
30 * @api
31 */
32final class Commands
33{
34    /** @var list<Entry> */
35    private array $entries = [];
36
37    public function __construct(array|object|string $commands = [])
38    {
39        $this->add($commands);
40    }
41
42    public function add(array|object|string $commands): void
43    {
44        if ($commands instanceof Closure) {
45            throw new ValueError(
46                'Closure commands are not supported; use an anonymous class with a #[Command] attribute',
47            );
48        }
49
50        if ($commands instanceof Commands) {
51            foreach ($commands->entries() as $entry) {
52                $this->entries[] = $entry;
53            }
54
55            return;
56        }
57
58        if (is_array($commands)) {
59            $this->addArray($commands);
60
61            return;
62        }
63
64        if (is_string($commands)) {
65            $this->entries[] = Entry::fromClass($this->validClass($commands));
66
67            return;
68        }
69
70        $this->entries[] = Entry::fromInstance($commands);
71    }
72
73    /** @return list<Entry> */
74    public function entries(): array
75    {
76        return $this->entries;
77    }
78
79    private function addArray(array $commands): void
80    {
81        foreach ($commands as $key => $item) {
82            if (is_string($key)) {
83                if (!$item instanceof Closure) {
84                    throw new ValueError("Factory for command class '{$key}' must be a closure");
85                }
86
87                $this->entries[] = Entry::fromFactory($this->validClass($key), $item);
88
89                continue;
90            }
91
92            if (!is_object($item) && !is_string($item)) {
93                throw new ValueError('Invalid command registration');
94            }
95
96            $this->add($item);
97        }
98    }
99
100    /** @return class-string */
101    private function validClass(string $class): string
102    {
103        if (!class_exists($class)) {
104            throw new ValueError("Unknown command class '{$class}'");
105        }
106
107        return $class;
108    }
109}