name = $name; $this->version = $version; } public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { $input = $input ?? new ArgvInput(); $output = $output ?? new ConsoleOutput(); try { $exitCode = $this->doRun($input, $output); } catch (\Exception $e) { $this->renderException($e, $output); $exitCode = 1; } if ($this->autoExit) { exit($exitCode); } return $exitCode; } private function doRun(InputInterface $input, OutputInterface $output): int { // 处理全局选项 if ($input->hasParameterOption(['--help', '-h'])) { $this->showHelp($output); return 0; } if ($input->hasParameterOption(['--version', '-v'])) { $this->showVersion($output); return 0; } // 获取命令名称 $commandName = $input->getFirstArgument(); if ($commandName === null) { $this->showHelp($output); return 0; } // 查找命令 $command = $this->findCommand($commandName); $this->runningCommand = $command; // 设置输入 $input->bind($command->getDefinition()); // 验证输入 $input->validate(); // 执行命令 $exitCode = $command->run($input, $output); return $exitCode; } public function add(CommandInterface $command): self { $command->setApplication($this); $this->commands[$command->getName()] = $command; // 添加别名 foreach ($command->getAliases() as $alias) { $this->commands[$alias] = $command; } return $this; } public function get(string $name): CommandInterface { if (!isset($this->commands[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return $this->commands[$name]; } public function has(string $name): bool { return isset($this->commands[$name]); } public function all(): array { return $this->commands; } public function find(string $name): CommandInterface { if (!$this->has($name)) { // 尝试模糊匹配 $alternatives = $this->findAlternatives($name, array_keys($this->commands)); throw new CommandNotFoundException( sprintf('Command "%s" does not exist.', $name), $alternatives ); } return $this->commands[$name]; } public function findNamespace(string $namespace): string { $allNamespaces = $this->getNamespaces(); foreach ($allNamespaces as $n) { if ($n === $namespace || str_starts_with($namespace, $n . ':')) { return $n; } } throw new CommandNotFoundException( sprintf('There are no commands defined in the "%s" namespace.', $namespace) ); } public function getNamespaces(): array { $namespaces = []; foreach ($this->commands as $name => $command) { if (str_contains($name, ':')) { $namespace = substr($name, 0, strpos($name, ':')); $namespaces[$namespace] = true; } } return array_keys($namespaces); } public function findAlternatives(string $name, array $collection): array { $alternatives = []; $threshold = 1.0; foreach ($collection as $item) { $distance = levenshtein($name, $item); $similarity = 1 - ($distance / max(strlen($name), strlen($item))); if ($similarity >= $threshold) { $alternatives[] = $item; } } return $alternatives; } public function setAutoExit(bool $autoExit): self { $this->autoExit = $autoExit; return $this; } public function getName(): string { return $this->name; } public function getVersion(): string { return $this->version; } public function getRunningCommand(): ?CommandInterface { return $this->runningCommand; } private function showHelp(OutputInterface $output): void { $output->writeln($this->getHelp()); } private function showVersion(OutputInterface $output): void { $output->writeln($this->getVersion()); } public function getHelp(): string { $help = sprintf( "%s %s version %s\n\n", $this->name, $this->name, $this->version ); $help .= "Usage:\n"; $help .= " command [options] [arguments]\n\n"; $help .= "Options:\n"; $help .= " -h, --help Display this help message\n"; $help .= " -v, --version Display application version\n\n"; $help .= "Available commands:\n"; // 按命名空间分组显示命令 $namespaces = $this->getNamespaces(); $commands = $this->all(); // 显示无命名空间的命令 $globalCommands = array_filter($commands, function($name) { return !str_contains($name, ':'); }, ARRAY_FILTER_USE_KEY); if (!empty($globalCommands)) { foreach ($globalCommands as $name => $command) { $help .= sprintf(" %-30s %s\n", $name, $command->getDescription()); } $help .= "\n"; } // 显示命名空间命令 foreach ($namespaces as $namespace) { $help .= sprintf(" %s:\n", $namespace); $namespaceCommands = array_filter($commands, function($name) use ($namespace) { return str_starts_with($name, $namespace . ':'); }, ARRAY_FILTER_USE_KEY); foreach ($namespaceCommands as $name => $command) { $shortName = substr($name, strlen($namespace) + 1); $help .= sprintf(" %-30s %s\n", $namespace . ':' . $shortName, $command->getDescription()); } $help .= "\n"; } return $help; } public function renderException(\Exception $exception, OutputInterface $output): void { $output->writeln(''); $output->writeln(sprintf('%s', $exception->getMessage())); $output->writeln(''); if ($exception instanceof CommandNotFoundException && !empty($exception->getAlternatives())) { $output->writeln('Did you mean one of these?'); $output->writeln(''); foreach ($exception->getAlternatives() as $alternative) { $output->writeln(sprintf(' %s', $alternative)); } $output->writeln(''); } // 显示堆栈跟踪(仅在调试模式下) if ($this->isDebug()) { $output->writeln('Exception trace:'); $output->writeln(''); $trace = $exception->getTrace(); foreach ($trace as $i => $traceItem) { $file = $traceItem['file'] ?? 'unknown'; $line = $traceItem['line'] ?? 'unknown'; $function = $traceItem['function'] ?? 'unknown'; $class = $traceItem['class'] ?? ''; $output->writeln(sprintf( ' %d. %s%s%s() at %s:%s', $i + 1, $class ? $class . '::' : '', $function, $file, $line )); } } } private function isDebug(): bool { return (bool)($_ENV['DEBUG'] ?? false); } public function registerDefaultCommands(): self { // 注册默认命令 $this->add(new Command\HelpCommand()); $this->add(new Command\ListCommand()); $this->add(new Command\VersionCommand()); return $this; } public function doRegisterCommand(string $className): self { if (!class_exists($className)) { throw new \InvalidArgumentException(sprintf('Command class "%s" does not exist.', $className)); } $reflection = new \ReflectionClass($className); if (!$reflection->implementsInterface(CommandInterface::class)) { throw new \InvalidArgumentException(sprintf('Command class "%s" must implement CommandInterface.', $className)); } $command = $reflection->newInstance(); $this->add($command); return $this; } public function loadCommandsFromDirectory(string $directory, string $namespace = ''): self { if (!is_dir($directory)) { return $this; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if ($file->isFile() && $file->getExtension() === 'php') { $className = $this->getClassNameFromFile($file->getPathname(), $namespace); if ($className && class_exists($className)) { try { $this->doRegisterCommand($className); } catch (\InvalidArgumentException $e) { // 忽略无效的命令类 } } } } return $this; } private function getClassNameFromFile(string $filePath, string $namespace): ?string { $content = file_get_contents($filePath); if (!preg_match('/namespace\s+([^;]+);/', $content, $matches)) { return null; } $fileNamespace = $matches[1]; $className = basename($filePath, '.php'); return $fileNamespace . '\\' . $className; } public function setCatchExceptions(bool $catchExceptions): self { // 这个方法用于向后兼容 return $this; } public function renderThrowable(\Throwable $throwable, OutputInterface $output): void { if ($throwable instanceof \Exception) { $this->renderException($throwable, $output); } else { $output->writeln(''); $output->writeln(sprintf('%s', $throwable->getMessage())); $output->writeln(''); } } }