*/ private array $commands = []; public function register(Command $command): void { $this->commands[$command->name()] = $command; } /** * Auto-discover and register all Command subclasses in a directory. * * Scans PHP files recursively, extracts the FQCN from namespace + class name, * and registers any concrete Command subclass found. */ public function discoverCommands(string $directory): void { if (!is_dir($directory)) { return; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($iterator as $file) { if ($file->getExtension() !== 'php') { continue; } $content = file_get_contents($file->getPathname()); if ($content === false) { continue; } // Extract namespace and class name from file $namespace = ''; if (preg_match('/^namespace\s+(.+?);/m', $content, $nsMatch)) { $namespace = $nsMatch[1]; } $className = ''; if (preg_match('/^(?:final\s+|abstract\s+)?class\s+(\w+)/m', $content, $classMatch)) { $className = $classMatch[1]; } if ($className === '' || $namespace === '') { continue; } $fqcn = $namespace . '\\' . $className; if (!class_exists($fqcn)) { continue; } $reflection = new \ReflectionClass($fqcn); if ($reflection->isAbstract() || !$reflection->isSubclassOf(Command::class)) { continue; } $this->register($reflection->newInstance()); } } /** * @param list $argv Raw CLI arguments (including script name at [0]) */ public function run(array $argv): int { $commandName = $argv[1] ?? ''; if ($commandName === '' || $commandName === 'list' || $commandName === '--help') { $this->printCommandList(); return 0; } if (!isset($this->commands[$commandName])) { fwrite(STDERR, "Unknown command: {$commandName}\n\n"); $this->printCommandList(); return 1; } [$args, $options] = self::parseArgv(array_slice($argv, 2)); // Per-command --help if ($options['help'] ?? false) { $command = $this->commands[$commandName]; $usage = $command->usage(); if ($usage !== '') { fwrite(STDOUT, $usage . "\n"); } else { fwrite(STDOUT, sprintf("%s — %s\n", $command->name(), $command->description())); } return 0; } return $this->commands[$commandName]->execute($args, $options); } private function printCommandList(): void { fwrite(STDOUT, "Usage: php bin/console [arguments] [options]\n\n"); fwrite(STDOUT, "Available commands:\n"); // Group by namespace prefix $grouped = []; foreach ($this->commands as $name => $command) { $parts = explode(':', $name, 2); $group = count($parts) === 2 ? $parts[0] : ''; $grouped[$group][$name] = $command->description(); } ksort($grouped); foreach ($grouped as $group => $commands) { if ($group !== '') { fwrite(STDOUT, " {$group}\n"); } ksort($commands); foreach ($commands as $name => $description) { fwrite(STDOUT, sprintf(" %-30s %s\n", $name, $description)); } } } /** * @param list $tokens * @return array{list, array} */ private static function parseArgv(array $tokens): array { $args = []; $options = []; foreach ($tokens as $token) { if (str_starts_with($token, '--')) { $option = substr($token, 2); if (str_contains($option, '=')) { [$key, $value] = explode('=', $option, 2); $options[$key] = $value; } else { $options[$option] = true; } } else { $args[] = $token; } } return [$args, $options]; } }