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;
}
}