Skip to content

Commit d8a9569

Browse files
authored
Merge pull request #2081 from nWidart/add-module-list-commands
Add ListCommands Command
2 parents 3c7bbdb + 9a5bf07 commit d8a9569

File tree

3 files changed

+441
-0
lines changed

3 files changed

+441
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<?php
2+
3+
namespace Nwidart\Modules\Commands\Actions;
4+
5+
use Illuminate\Support\Facades\File;
6+
use Illuminate\Support\Str;
7+
use Nwidart\Modules\Commands\BaseCommand;
8+
use ReflectionClass;
9+
use Symfony\Component\Console\Command\Command as SymfonyCommand;
10+
11+
class ListCommands extends BaseCommand
12+
{
13+
protected $name = 'module:list-commands';
14+
15+
protected $description = 'List all commands in the specified module(s)';
16+
17+
public function executeAction($name): void
18+
{
19+
$module = $this->getModuleModel($name);
20+
$commands = $this->findCommands($module);
21+
22+
if (empty($commands)) {
23+
$this->components->info("No commands found in module <fg=cyan;options=bold>{$module->getName()}</>");
24+
25+
return;
26+
}
27+
28+
// Group commands by directory
29+
$groupedCommands = [];
30+
foreach ($commands as $command) {
31+
$directory = $this->getDirectoryFromNamespace($command['namespace']);
32+
if (! isset($groupedCommands[$directory])) {
33+
$groupedCommands[$directory] = [];
34+
}
35+
$groupedCommands[$directory][] = $command;
36+
}
37+
38+
// Display commands by group
39+
foreach ($groupedCommands as $directory => $dirCommands) {
40+
$this->components->twoColumnDetail("<fg=yellow>{$directory}</>", '');
41+
42+
foreach ($dirCommands as $command) {
43+
$name = $command['name'] ?: '<fg=red>Unknown</>';
44+
$this->components->success(" <fg=green>{$name}</>");
45+
}
46+
47+
$this->newLine();
48+
}
49+
}
50+
51+
/**
52+
* Find all commands in a module
53+
*
54+
* @return array<string, array<string, string|null>>
55+
*/
56+
protected function findCommands($module): array
57+
{
58+
$commands = [];
59+
$moduleName = $module->getName();
60+
$moduleNamespace = $this->getModuleNamespace($moduleName);
61+
62+
// Check possible command paths
63+
$possiblePaths = [
64+
$module->getExtraPath('Commands'),
65+
$module->getExtraPath('Console/Commands'),
66+
$module->getAppPath().'/Commands',
67+
$module->getAppPath().'/Console',
68+
$module->getAppPath().'/Console/Commands',
69+
];
70+
71+
foreach ($possiblePaths as $path) {
72+
if (! is_dir($path)) {
73+
continue;
74+
}
75+
76+
$files = File::allFiles($path);
77+
78+
foreach ($files as $file) {
79+
// Skip if not a PHP file
80+
if ($file->getExtension() !== 'php') {
81+
continue;
82+
}
83+
84+
// Get the class name from the file path
85+
$relativePath = str_replace($module->getPath().'/', '', $file->getPathname());
86+
$className = $this->getClassNameFromPath($relativePath, $moduleNamespace);
87+
88+
// Try to get command information
89+
$commandInfo = $this->getCommandInfo($className);
90+
91+
if ($commandInfo) {
92+
$commands[] = $commandInfo;
93+
}
94+
}
95+
}
96+
97+
return $commands;
98+
}
99+
100+
/**
101+
* Get module namespace
102+
*/
103+
protected function getModuleNamespace(string $moduleName): string
104+
{
105+
return config('modules.namespace', 'Modules').'\\'.$moduleName;
106+
}
107+
108+
/**
109+
* Convert a file path to a class name with namespace
110+
*/
111+
protected function getClassNameFromPath(string $path, string $moduleNamespace): string
112+
{
113+
// Remove .php extension
114+
$path = str_replace('.php', '', $path);
115+
116+
// Convert directory separators to namespace separators
117+
$path = str_replace('/', '\\', $path);
118+
119+
// If the path starts with app/, remove it and prepend the module namespace
120+
if (Str::startsWith($path, 'app\\')) {
121+
$path = $moduleNamespace.'\\'.Str::after($path, 'app\\');
122+
} else {
123+
$path = $moduleNamespace.'\\'.$path;
124+
}
125+
126+
return $path;
127+
}
128+
129+
/**
130+
* Get command information from class
131+
*/
132+
protected function getCommandInfo(string $className): ?array
133+
{
134+
try {
135+
// Extract the short class name from the fully qualified class name
136+
$shortClassName = $this->getShortClassName($className);
137+
138+
if (! class_exists($className)) {
139+
return [
140+
'class' => $className,
141+
'name' => $shortClassName,
142+
'namespace' => $this->getNamespaceFromClass($className),
143+
];
144+
}
145+
146+
$reflection = new ReflectionClass($className);
147+
148+
// Skip if not a command class
149+
if (! $reflection->isSubclassOf(SymfonyCommand::class) &&
150+
! $reflection->isSubclassOf('Illuminate\Console\Command')) {
151+
return null;
152+
}
153+
154+
// Skip if the class is not instantiable or has required constructor parameters
155+
if (! $reflection->isInstantiable() || $reflection->getConstructor()?->getNumberOfRequiredParameters() > 0) {
156+
return [
157+
'class' => $className,
158+
'name' => $shortClassName,
159+
'namespace' => $reflection->getNamespaceName(),
160+
];
161+
}
162+
163+
// Create a proper instance of the command
164+
$commandInstance = $reflection->newInstance();
165+
166+
// Get name directly from the instance
167+
$name = null;
168+
169+
if (method_exists($commandInstance, 'getName')) {
170+
$name = $commandInstance->getName();
171+
}
172+
173+
return [
174+
'class' => $className,
175+
'name' => $name ?? $shortClassName,
176+
'namespace' => $reflection->getNamespaceName(),
177+
];
178+
} catch (\Throwable $e) {
179+
// If we can't instantiate the class, just return basic info with the class name
180+
return [
181+
'class' => $className,
182+
'name' => $this->getShortClassName($className),
183+
'namespace' => $this->getNamespaceFromClass($className),
184+
];
185+
}
186+
}
187+
188+
/**
189+
* Get short class name from fully qualified class name
190+
*/
191+
protected function getShortClassName(string $className): string
192+
{
193+
$parts = explode('\\', $className);
194+
195+
return end($parts);
196+
}
197+
198+
/**
199+
* Get namespace from class name
200+
*/
201+
protected function getNamespaceFromClass(string $className): string
202+
{
203+
$parts = explode('\\', $className);
204+
array_pop($parts); // Remove the class name
205+
206+
return implode('\\', $parts);
207+
}
208+
209+
/**
210+
* Get directory name from namespace
211+
*/
212+
protected function getDirectoryFromNamespace(string $namespace): string
213+
{
214+
$parts = explode('\\', $namespace);
215+
216+
// Look for Commands or Console\Commands in the namespace
217+
$commandsIndex = array_search('Commands', $parts);
218+
if ($commandsIndex !== false) {
219+
// If we found 'Commands', check if it's preceded by 'Console'
220+
if ($commandsIndex > 0 && $parts[$commandsIndex - 1] === 'Console') {
221+
return 'Console/Commands';
222+
}
223+
224+
return 'Commands';
225+
}
226+
227+
// Default to the last two parts of the namespace
228+
return implode('/', array_slice($parts, -2, 2));
229+
}
230+
231+
public function getInfo(): ?string
232+
{
233+
return 'Listing commands...';
234+
}
235+
}

src/Providers/ConsoleServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static function defaultCommands(): Collection
3131
Commands\Actions\EnableCommand::class,
3232
Commands\Actions\InstallCommand::class,
3333
Commands\Actions\ListCommand::class,
34+
Commands\Actions\ListCommands::class,
3435
Commands\Actions\ModelPruneCommand::class,
3536
Commands\Actions\ModelShowCommand::class,
3637
Commands\Actions\ModuleDeleteCommand::class,

0 commit comments

Comments
 (0)