Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
CommandRegistry
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
10 / 10
33
100.00% covered (success)
100.00%
1 / 1
 add
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addAll
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromRoutes
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 addFromRoutes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 loadRoutes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 buildCommandRouteEntry
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 detectNamespace
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Console;
16
17use Pop\Router\Match\Cli;
18
19/**
20 * Console command registry class
21 *
22 * @category   Pop
23 * @package    Pop\Console
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29class CommandRegistry
30{
31
32    /**
33     * Commands
34     * @var array
35     */
36    protected array $commands = [];
37
38    /**
39     * Add a command
40     *
41     * @param  Command $command
42     * @return static
43     */
44    public function add(Command $command): static
45    {
46        $this->commands[$command->getName()] = $command;
47        return $this;
48    }
49
50    /**
51     * Add commands
52     *
53     * @param  array $commands
54     * @return static
55     */
56    public function addAll(array $commands): static
57    {
58        foreach ($commands as $command) {
59            $this->add($command);
60        }
61        return $this;
62    }
63
64    /**
65     * Get all commands
66     *
67     * @return array
68     */
69    public function all(): array
70    {
71        return $this->commands;
72    }
73
74    /**
75     * Get a command
76     *
77     * @param  string $name
78     * @return Command|null
79     */
80    public function get(string $name): Command|null
81    {
82        return $this->commands[$name] ?? null;
83    }
84
85    /**
86     * Check if the registry has a command
87     *
88     * @param  string $name
89     * @return bool
90     */
91    public function has(string $name): bool
92    {
93        return isset($this->commands[$name]);
94    }
95
96    /**
97     * Build commands from routes
98     *
99     * @param  Cli     $routeMatch
100     * @param  ?string $scriptName
101     * @return array
102     */
103    public function fromRoutes(Cli $routeMatch, ?string $scriptName = null): array
104    {
105        $routeMatch->match();
106
107        $commandRoutes = $routeMatch->getRoutes();
108        $commands      = $routeMatch->getCommands();
109        $commandsToAdd = [];
110
111        foreach ($commands as $name => $command) {
112            $commandName = implode(' ', $command);
113            $params      = trim(substr((string)$name, strlen((string)$commandName)));
114            $params      = (!empty($params)) ? $params : null;
115            $help        = null;
116
117            // Get help text directly from the route config
118            if (isset($commandRoutes[$name]) && isset($commandRoutes[$name]['help'])) {
119                $help = $commandRoutes[$name]['help'];
120            // Else, get help from the command object help
121            } else if (is_subclass_of($commandRoutes[$name]['controller'], 'Pop\Console\Command\CommandInterface', true)) {
122                $commandClass = $commandRoutes[$name]['controller'];
123                $help         = (new $commandClass())->getHelp();
124            }
125
126            if ($scriptName !== null) {
127                $commandName = $scriptName . ' ' . $commandName;
128            }
129
130            $commandToAdd = new Command(name: $commandName, params: $params, help: $help);
131            if ($scriptName !== null) {
132                $commandToAdd->setScriptName($scriptName);
133            }
134
135            $commandsToAdd[] = $commandToAdd;
136        }
137
138        return $commandsToAdd;
139    }
140
141    /**
142     * Add commands from routes
143     *
144     * @param  Cli     $routeMatch
145     * @param  ?string $scriptName
146     * @return static
147     */
148    public function addFromRoutes(Cli $routeMatch, ?string $scriptName = null): static
149    {
150        $commands = $this->fromRoutes($routeMatch, $scriptName);
151
152        if (!empty($commands)) {
153            $this->addAll($commands);
154        }
155
156        return $this;
157    }
158
159    /**
160     * Load application commands into the route config array
161     *
162     * @param  array  $routes
163     * @param  string $location
164     * @param  bool   $prepend
165     * @return array
166     */
167    public static function loadRoutes(array $routes, string $location, bool $prepend = true): array
168    {
169        if (!file_exists($location)) {
170            return $routes;
171        }
172
173        $commands = array_values(array_filter(scandir($location), function ($value) {
174            return (($value != '.') && ($value != '..') && ($value != '.empty'));
175        }));
176
177        $namespace     = null;
178        $commandRoutes = [];
179        $total         = count($commands);
180
181        foreach ($commands as $i => $command) {
182            $entry = self::buildCommandRouteEntry($command, $location, $namespace, $i == ($total - 1));
183            if ($entry !== null) {
184                [$key, $route]        = $entry;
185                $commandRoutes[$key] = $route;
186            }
187        }
188
189        if (!empty($commandRoutes)) {
190            $routes = ($prepend) ? array_merge($commandRoutes, $routes) : array_merge($routes, $commandRoutes);
191        }
192
193        return $routes;
194    }
195
196    /**
197     * Build a single route entry from a discovered command file
198     *
199     * @param  string  $command
200     * @param  string  $location
201     * @param  ?string $namespace
202     * @param  bool    $isLast
203     * @return ?array
204     */
205    protected static function buildCommandRouteEntry(string $command, string $location, ?string &$namespace, bool $isLast): ?array
206    {
207        if (!str_ends_with($command, '.php')) {
208            return null;
209        }
210
211        if ($namespace === null) {
212            $namespace = self::detectNamespace($location . DIRECTORY_SEPARATOR . $command);
213        }
214
215        $commandClass = $namespace . '\\' . substr($command, 0, -4);
216        if (!class_exists($commandClass)) {
217            return null;
218        }
219
220        $commandObject = new $commandClass();
221        $commandRoute  = ['controller' => $commandClass];
222
223        if ($commandObject->hasHelp()) {
224            $commandRoute['help'] = $commandObject->getHelp() . ($isLast ? PHP_EOL : '');
225        }
226
227        return [(string)$commandObject, $commandRoute];
228    }
229
230    /**
231     * Detect the namespace declared in a PHP file
232     *
233     * @param  string $file
234     * @return ?string
235     */
236    protected static function detectNamespace(string $file): ?string
237    {
238        if (!file_exists($file)) {
239            return null;
240        }
241
242        $matches = [];
243        preg_match('/^\s*namespace\s+([^;]+);/m', file_get_contents($file), $matches);
244
245        return isset($matches[1]) ? trim($matches[1]) : null;
246    }
247
248}