mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 15:02:49 +08:00
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
390 lines
11 KiB
PHP
390 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Fendx\CLI;
|
|
|
|
use Fendx\CLI\Command\CommandInterface;
|
|
use Fendx\CLI\Input\InputInterface;
|
|
use Fendx\CLI\Input\ArgvInput;
|
|
use Fendx\CLI\Output\OutputInterface;
|
|
use Fendx\CLI\Output\ConsoleOutput;
|
|
use Fendx\CLI\Exception\CommandNotFoundException;
|
|
|
|
final class Application
|
|
{
|
|
private string $name;
|
|
private string $version;
|
|
private array $commands = [];
|
|
private bool $autoExit = true;
|
|
private ?CommandInterface $runningCommand = null;
|
|
|
|
public function __construct(string $name = 'FendxCLI', string $version = '1.0.0')
|
|
{
|
|
$this->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 <info>%s</info> version <comment>%s</comment>\n\n",
|
|
$this->name,
|
|
$this->name,
|
|
$this->version
|
|
);
|
|
|
|
$help .= "<info>Usage:</info>\n";
|
|
$help .= " command [options] [arguments]\n\n";
|
|
|
|
$help .= "<info>Options:</info>\n";
|
|
$help .= " <info>-h, --help</info> Display this help message\n";
|
|
$help .= " <info>-v, --version</info> Display application version\n\n";
|
|
|
|
$help .= "<info>Available commands:</info>\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(" <info>%-30s</info> %s\n", $name, $command->getDescription());
|
|
}
|
|
$help .= "\n";
|
|
}
|
|
|
|
// 显示命名空间命令
|
|
foreach ($namespaces as $namespace) {
|
|
$help .= sprintf(" <info>%s</info>:\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(" <info>%-30s</info> %s\n", $namespace . ':' . $shortName, $command->getDescription());
|
|
}
|
|
$help .= "\n";
|
|
}
|
|
|
|
return $help;
|
|
}
|
|
|
|
public function renderException(\Exception $exception, OutputInterface $output): void
|
|
{
|
|
$output->writeln('');
|
|
$output->writeln(sprintf('<error>%s</error>', $exception->getMessage()));
|
|
$output->writeln('');
|
|
|
|
if ($exception instanceof CommandNotFoundException && !empty($exception->getAlternatives())) {
|
|
$output->writeln('<info>Did you mean one of these?</info>');
|
|
$output->writeln('');
|
|
|
|
foreach ($exception->getAlternatives() as $alternative) {
|
|
$output->writeln(sprintf(' <info>%s</info>', $alternative));
|
|
}
|
|
$output->writeln('');
|
|
}
|
|
|
|
// 显示堆栈跟踪(仅在调试模式下)
|
|
if ($this->isDebug()) {
|
|
$output->writeln('<error>Exception trace:</error>');
|
|
$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(
|
|
' <info>%d.</info> %s%s%s() at <info>%s:%s</info>',
|
|
$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('<error>%s</error>', $throwable->getMessage()));
|
|
$output->writeln('');
|
|
}
|
|
}
|
|
}
|