Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
60 / 63
80.00% covered (success)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandRegistry
95.24% covered (success)
95.24%
60 / 63
80.00% covered (success)
80.00%
8 / 10
32
0.00% covered (danger)
0.00%
0 / 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%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 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
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
6.17
 detectNamespace
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
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 <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
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 <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
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            $commandsToAdd[] = new Command(name: $commandName, params: $params, help: $help);
131        }
132
133        return $commandsToAdd;
134    }
135
136    /**
137     * Add commands from routes
138     *
139     * @param  Cli     $routeMatch
140     * @param  ?string $scriptName
141     * @return static
142     */
143    public function addFromRoutes(Cli $routeMatch, ?string $scriptName = null): static
144    {
145        $commands = $this->fromRoutes($routeMatch, $scriptName);
146
147        if (!empty($commands)) {
148            $this->addAll($commands);
149        }
150
151        return $this;
152    }
153
154    /**
155     * Load application commands into the route config array
156     *
157     * @param  array  $routes
158     * @param  string $location
159     * @param  bool   $prepend
160     * @return array
161     */
162    public static function loadRoutes(array $routes, string $location, bool $prepend = true): array
163    {
164        if (!file_exists($location)) {
165            return $routes;
166        }
167
168        $commands = array_values(array_filter(scandir($location), function ($value) {
169            return (($value != '.') && ($value != '..') && ($value != '.empty'));
170        }));
171
172        $namespace     = null;
173        $commandRoutes = [];
174        $total         = count($commands);
175
176        foreach ($commands as $i => $command) {
177            $entry = self::buildCommandRouteEntry($command, $location, $namespace, $i == ($total - 1));
178            if ($entry !== null) {
179                [$key, $route]        = $entry;
180                $commandRoutes[$key] = $route;
181            }
182        }
183
184        if (!empty($commandRoutes)) {
185            $routes = ($prepend) ? array_merge($commandRoutes, $routes) : array_merge($routes, $commandRoutes);
186        }
187
188        return $routes;
189    }
190
191    /**
192     * Build a single route entry from a discovered command file
193     *
194     * @param  string  $command
195     * @param  string  $location
196     * @param  ?string $namespace
197     * @param  bool    $isLast
198     * @return ?array
199     */
200    protected static function buildCommandRouteEntry(string $command, string $location, ?string &$namespace, bool $isLast): ?array
201    {
202        if (!str_ends_with($command, '.php')) {
203            return null;
204        }
205
206        if ($namespace === null) {
207            $namespace = self::detectNamespace($location . DIRECTORY_SEPARATOR . $command);
208        }
209
210        $commandClass = $namespace . '\\' . substr($command, 0, -4);
211        if (!class_exists($commandClass)) {
212            return null;
213        }
214
215        $commandObject = new $commandClass();
216        $commandRoute  = ['controller' => $commandClass];
217
218        if ($commandObject->hasHelp()) {
219            $commandRoute['help'] = $commandObject->getHelp() . ($isLast ? PHP_EOL : '');
220        }
221
222        return [(string)$commandObject, $commandRoute];
223    }
224
225    /**
226     * Detect the namespace declared in a PHP file
227     *
228     * @param  string $file
229     * @return ?string
230     */
231    protected static function detectNamespace(string $file): ?string
232    {
233        if (!file_exists($file)) {
234            return null;
235        }
236
237        $matches = [];
238        preg_match('/^\s*namespace\s+([^;]+);/m', file_get_contents($file), $matches);
239
240        return isset($matches[1]) ? trim($matches[1]) : null;
241    }
242
243}