diff --git a/composer.json b/composer.json index dae5f56d..776d67bd 100644 --- a/composer.json +++ b/composer.json @@ -22,14 +22,14 @@ "onebot/libonebot": "dev-develop", "psr/container": "^2.0", "psy/psysh": "^0.11.8", - "symfony/console": "^6.0 || ^5.0 || ^4.0", + "symfony/console": "^6.0", "symfony/polyfill-ctype": "^1.19", "symfony/polyfill-mbstring": "^1.19", "symfony/polyfill-php80": "^1.16", "symfony/routing": "~6.0 || ~5.0 || ~4.0" }, "require-dev": { - "brainmaestro/composer-git-hooks": "^2.8", + "brainmaestro/composer-git-hooks": "^3.0", "friendsofphp/php-cs-fixer": "^3.2 != 3.7.0", "jetbrains/phpstorm-attributes": "^1.0", "phpstan/extension-installer": "^1.1", diff --git a/src/ZM/Command/Command.php b/src/ZM/Command/Command.php new file mode 100644 index 00000000..17a1eabb --- /dev/null +++ b/src/ZM/Command/Command.php @@ -0,0 +1,73 @@ +input = $input; + $this->output = $output; + return $this->handle(); + } + + abstract protected function handle(): int; + + protected function write(string $message, bool $newline = true): void + { + $this->output->write($message, $newline); + } + + protected function info(string $message, bool $newline = true): void + { + $this->write("{$message}", $newline); + } + + protected function error(string $message, bool $newline = true): void + { + $this->write("{$message}", $newline); + } + + protected function comment(string $message, bool $newline = true): void + { + $this->write("{$message}", $newline); + } + + protected function question(string $message, bool $newline = true): void + { + $this->write("{$message}", $newline); + } + + protected function detail(string $message, bool $newline = true): void + { + $this->write("{$message}", $newline); + } + + protected function section(string $message, callable $callback): void + { + $output = $this->output; + if (!$output instanceof ConsoleOutputInterface) { + throw new \LogicException('Section 功能只能在 ConsoleOutputInterface 中使用'); + } + + $this->info($message); + $section = $output->section(); + try { + $callback($section); + } catch (ZMException $e) { + $this->error($e->getMessage()); + exit(1); + } + } +} diff --git a/src/ZM/Command/InitCommand.php b/src/ZM/Command/InitCommand.php index 6ec3d867..7ca65cdc 100644 --- a/src/ZM/Command/InitCommand.php +++ b/src/ZM/Command/InitCommand.php @@ -4,114 +4,173 @@ declare(strict_types=1); namespace ZM\Command; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use ZM\Exception\InitException; class InitCommand extends Command { // the name of the command (the part after "bin/console") protected static $defaultName = 'init'; - private $extract_files = [ - '/zhamao', - '/config/global.php', - '/.gitignore', - '/config/file_header.json', - '/config/console_color.json', - '/config/motd.txt', - '/src/Module/Example/Hello.php', - '/src/Module/Middleware/TimerMiddleware.php', - '/src/Custom/global_function.php', - ]; + private string $base_path; - protected function configure() + private bool $force = false; + + protected function configure(): void { $this->setDescription('Initialize framework starter | 初始化框架运行的基础文件'); $this->setDefinition([ new InputOption('force', 'F', null, '强制重制,覆盖现有文件'), ]); - $this->setHelp("此命令将会解压以下文件到项目的根目录:\n" . implode("\n", $this->getExtractFiles())); - // ... + $this->setHelp('提取框架的基础文件到当前目录,以便于快速开始开发。'); } - protected function execute(InputInterface $input, OutputInterface $output): int + protected function handle(): int { - if (LOAD_MODE === 1) { // 从composer依赖而来的项目模式,最基本的需要初始化的模式 - $output->writeln('Initializing files'); - $base_path = WORKING_DIR; - $args = $input->getOption('force'); - foreach ($this->extract_files as $file) { - if (!file_exists($base_path . $file) || $args) { - $info = pathinfo($file); - @mkdir($base_path . $info['dirname'], 0777, true); - echo 'Copying ' . $file . PHP_EOL; - $package_name = json_decode(file_get_contents(__DIR__ . '/../../../composer.json'), true)['name']; - copy($base_path . '/vendor/' . $package_name . $file, $base_path . $file); + $this->setBasePath(); + $this->force = $this->input->getOption('force'); + + $this->section('提取框架基础文件', function (ConsoleSectionOutput $section) { + foreach ($this->getExtractFiles() as $file) { + $section->write("提取 {$file} ... "); + if ($this->shouldExtractFile($file)) { + try { + $this->extractFile($file); + $section->write('完成'); + } catch (InitException $e) { + $section->write('失败'); + throw $e; + } finally { + $section->writeln(''); + } } else { - echo 'Skipping ' . $file . ' , file exists.' . PHP_EOL; + $section->writeln('跳过(已存在)'); } } - chmod($base_path . '/zhamao', 0755); - $autoload = [ - 'psr-4' => [ - 'Module\\' => 'src/Module', - 'Custom\\' => 'src/Custom', - ], - 'files' => [ - 'src/Custom/global_function.php', - ], - ]; - if (file_exists($base_path . '/composer.json')) { - $composer = json_decode(file_get_contents($base_path . '/composer.json'), true); + }); + + if (LOAD_MODE === 1) { + $this->section('应用自动加载配置', function (ConsoleSectionOutput $section) { + $autoload = [ + 'psr-4' => [ + 'Module\\' => 'src/Module', + 'Custom\\' => 'src/Custom', + ], + 'files' => [ + 'src/Custom/global_function.php', + ], + ]; + + if (!file_exists($this->base_path . '/composer.json')) { + throw new InitException('未找到 composer.json 文件', '请检查当前目录是否为项目根目录', 41); + } + + try { + $composer = json_decode(file_get_contents($this->base_path . '/composer.json'), true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new InitException('解析 composer.json 文件失败', '请检查 composer.json 文件是否存在语法错误', 42, $e); + } + if (!isset($composer['autoload'])) { $composer['autoload'] = $autoload; } else { - foreach ($autoload['psr-4'] as $k => $v) { - if (!isset($composer['autoload']['psr-4'][$k])) { - $composer['autoload']['psr-4'][$k] = $v; - } - } - foreach ($autoload['files'] as $v) { - if (!in_array($v, $composer['autoload']['files'])) { - $composer['autoload']['files'][] = $v; - } - } + $composer['autoload'] = array_merge_recursive($composer['autoload'], $autoload); } - file_put_contents($base_path . '/composer.json', json_encode($composer, 64 | 128 | 256)); - $output->writeln('Executing composer command: `composer dump-autoload`'); + + try { + file_put_contents($this->base_path . '/composer.json', json_encode($composer, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } catch (\JsonException $e) { + throw new InitException('写入 composer.json 文件失败', '', 0, $e); + } + + $section->writeln('执行 composer dump-autoload ...'); exec('composer dump-autoload'); - echo PHP_EOL; - } else { - echo zm_internal_errcode('E00041') . "Error occurred. Please check your updates.\n"; - return 1; - } - $output->writeln('Done!'); - return 0; + + $section->writeln('完成'); + }); } - if (LOAD_MODE === 2) { // 从phar启动的框架包,初始化的模式 - $phar_link = new \Phar(__DIR__); - $current_dir = pathinfo($phar_link->getPath())['dirname']; - chdir($current_dir); - $phar_link = 'phar://' . $phar_link->getPath(); - foreach ($this->extract_files as $file) { - if (!file_exists($current_dir . $file)) { - $info = pathinfo($file); - @mkdir($current_dir . $info['dirname'], 0777, true); - echo 'Copying ' . $file . PHP_EOL; - file_put_contents($current_dir . $file, file_get_contents($phar_link . $file)); - } else { - echo 'Skipping ' . $file . ' , file exists.' . PHP_EOL; - } - } - } - $output->writeln(zm_internal_errcode('E00042') . 'initialization must be started with composer-project mode!'); - return 1; + + // 将命令行入口标记为可执行 + chmod($this->base_path . '/zhamao', 0755); + return 0; } private function getExtractFiles(): array { - return $this->extract_files; + $patterns = [ + '/zhamao', + '/.gitignore', + '/config/*', + '/src/Globals/*.php', + ]; + + $files = []; + foreach ($patterns as $pattern) { + // TODO: 优化代码,避免在循环中使用 array_merge 以减少资源消耗 + $files = array_merge($files, glob($this->getVendorPath($pattern))); + } + return array_map(function ($file) { + return str_replace($this->getVendorPath(''), '', $file); + }, $files); + } + + /** + * 设置基准目录 + */ + private function setBasePath(): void + { + $base_path = WORKING_DIR; + if (file_exists($base_path . '/vendor/autoload.php')) { + $this->base_path = $base_path; + } else { + $phar_link = new \Phar(__DIR__); + $current_dir = pathinfo($phar_link->getPath())['dirname']; + chdir($current_dir); + $phar_link = 'phar://' . $phar_link->getPath(); + if (file_exists($phar_link . '/vendor/autoload.php')) { + $this->base_path = $current_dir; + } else { + throw new InitException('框架启动模式不是 Composer 模式,无法进行初始化', '如果您是从 Github 下载的框架,请参阅文档进行源码模式启动', 42); + } + } + } + + /** + * 提取文件 + * + * @param string $file 文件路径,相对于框架根目录 + * @throws InitException 提取失败时抛出异常 + */ + private function extractFile(string $file): void + { + $info = pathinfo($file); + // 确保目录存在 + if ( + !file_exists($this->base_path . $info['dirname']) + && !mkdir($concurrent_dir = $this->base_path . $info['dirname'], 0777, true) + && !is_dir($concurrent_dir) + ) { + throw new InitException("无法创建目录 {$concurrent_dir}", '请检查目录权限'); + } + + if (copy($this->getVendorPath($file), $this->base_path . $file) === false) { + throw new InitException("无法复制文件 {$file}", '请检查目录权限'); + } + } + + private function shouldExtractFile(string $file): bool + { + return !file_exists($this->base_path . $file) || $this->force; + } + + private function getVendorPath(string $file): string + { + try { + $package_name = json_decode(file_get_contents(__DIR__ . '/../../../composer.json'), true, 512, JSON_THROW_ON_ERROR)['name']; + } catch (\JsonException) { + throw new InitException('无法读取框架包的 composer.json', '请检查框架包完整性,或者重新安装框架包'); + } + return $this->base_path . '/vendor/' . $package_name . $file; } }