From daa07dcb2bc5168e9a2b9a0a2456786768d6d2c1 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Fri, 16 Dec 2022 15:29:10 +0800 Subject: [PATCH 1/6] add docker compose support --- docker-compose.yml | 96 ++++++++++++++++++++++++++++++++++ docker/.gitignore | 2 + docker/environment.env.example | 7 +++ docker/walle-q/Dockerfile | 12 +++++ docker/zhamao/Dockerfile | 14 +++++ 5 files changed, 131 insertions(+) create mode 100644 docker-compose.yml create mode 100644 docker/.gitignore create mode 100644 docker/environment.env.example create mode 100644 docker/walle-q/Dockerfile create mode 100644 docker/zhamao/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9722eec0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,96 @@ +version: "3.9" + +services: + bot: + build: + context: docker/zhamao + dockerfile: ./Dockerfile + container_name: zhamao-bot + depends_on: + - database-postgres + - database-redis + networks: + - bot-net + ports: + - "20001-20005:20001-20005/tcp" + volumes: + - ".:/bot" + + walle-q: + build: + context: docker/walle-q + dockerfile: ./Dockerfile + container_name: zhamao-bot-walle-q + depends_on: + - bot + networks: + - bot-net + volumes: + - "./docker/volumes/walle-q-data:/bot" + + database-postgres: + container_name: zhamao-bot-db-postgres + env_file: + - docker/environment.env + image: postgres:14.4-alpine + networks: + - bot-net + ports: + - "5432:5432/tcp" + restart: always + volumes: + - "./docker/volumes/postgres-data:/var/lib/postgresql/data" + - "./docker/volumes/postgres-init:/docker-entrypoint-initdb.d" + + database-redis: + command: + - redis-server + - --requirepass + - "$${REDIS_PASSWORD}" + container_name: zhamao-bot-db-redis + env_file: + - docker/environment.env + image: redis:7.0-alpine + networks: + - bot-net + ports: + - "6379:6379/tcp" + restart: always + volumes: + - "./docker/volumes/redis-data:/data" + + db-admin-adminer: + container_name: zhamao-bot-dbadmin-adminer + depends_on: + database-postgres: + condition: service_started + database-redis: + condition: service_started + env_file: + - docker/environment.env + image: adminer:latest + networks: + - bot-net + ports: + - "15432:8080/tcp" + profiles: + - with-adminer + + db-admin-redis-insight: + container_name: zhamao-bot-dbadmin-redis-insight + depends_on: + database-postgres: + condition: service_started + database-redis: + condition: service_started + image: redislabs/redisinsight:latest + networks: + - bot-net + ports: + - "16379:8001/tcp" + profiles: + - with-adminer + +networks: + bot-net: + name: bot-net diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 00000000..0ffbf30a --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +/volumes +environment.env diff --git a/docker/environment.env.example b/docker/environment.env.example new file mode 100644 index 00000000..c683bfee --- /dev/null +++ b/docker/environment.env.example @@ -0,0 +1,7 @@ +REDIS_PASSWORD={{$REDIS_PASSWORD}} + +POSTGRES_USER={{$DB_ROOT_USER}} +POSTGRES_PASSWORD={{$DB_ROOT_PASSWORD}} +POSTGRES_APPLICATION_USER={{$DB_USER}} +POSTGRES_APPLICATION_USER_PASSWORD={{$DB_PASSWORD}} +POSTGRES_APPLICATION_DATABASE={{$DB_NAME}} diff --git a/docker/walle-q/Dockerfile b/docker/walle-q/Dockerfile new file mode 100644 index 00000000..1a69aab8 --- /dev/null +++ b/docker/walle-q/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:latest + +MAINTAINER sunxyw + +RUN apk add --no-cache curl && \ + mkdir -p /bot && \ + curl -L -o /bot/walle-q https://github.com/onebot-walle/walle-q/releases/latest/download/walle-q-i686-linux-musl && \ + chmod +x /bot/walle-q + +ENV TZ=Asia/Shanghai + +ENTRYPOINT [ "/bot/walle-q" ] diff --git a/docker/zhamao/Dockerfile b/docker/zhamao/Dockerfile new file mode 100644 index 00000000..33fdefef --- /dev/null +++ b/docker/zhamao/Dockerfile @@ -0,0 +1,14 @@ +FROM phpswoole/swoole:5.0-php8.1 + +MAINTAINER sunxyw + +WORKDIR "/bot" + +ENV TZ=Asia/Shanghai + +EXPOSE 20001-20005 + +RUN docker-php-ext-install pcntl + +ENTRYPOINT [ "./zhamao" ] +CMD [ "server" ] From ebb724415d4ad4e13d7857a322645906f1b58382 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Fri, 16 Dec 2022 17:58:52 +0800 Subject: [PATCH 2/6] split InitException to SingletonViolationException --- src/ZM/ConsoleApplication.php | 7 ++----- src/ZM/Exception/InitException.php | 5 ----- .../Exception/SingletonViolationException.php | 17 +++++++++++++++++ src/ZM/Framework.php | 5 ++--- src/ZM/InstantApplication.php | 10 +++------- 5 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 src/ZM/Exception/SingletonViolationException.php diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php index c670fee2..7a0c5120 100644 --- a/src/ZM/ConsoleApplication.php +++ b/src/ZM/ConsoleApplication.php @@ -19,7 +19,7 @@ use ZM\Command\Server\ServerReloadCommand; use ZM\Command\Server\ServerStartCommand; use ZM\Command\Server\ServerStatusCommand; use ZM\Command\Server\ServerStopCommand; -use ZM\Exception\InitException; +use ZM\Exception\SingletonViolationException; /** * 命令行启动的入口文件,用于初始化环境变量,并启动命令行应用 @@ -30,13 +30,10 @@ final class ConsoleApplication extends Application { private static $obj; - /** - * @throws InitException - */ public function __construct(string $name = 'zhamao-framework') { if (self::$obj !== null) { - throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Application is not allowed!'); + throw new SingletonViolationException(self::class); } // 初始化命令 diff --git a/src/ZM/Exception/InitException.php b/src/ZM/Exception/InitException.php index 538bf743..49ba9dce 100644 --- a/src/ZM/Exception/InitException.php +++ b/src/ZM/Exception/InitException.php @@ -6,9 +6,4 @@ namespace ZM\Exception; class InitException extends ZMException { - public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null) - { - // TODO: change this to a better error message - parent::__construct($message, '', $code, $previous); - } } diff --git a/src/ZM/Exception/SingletonViolationException.php b/src/ZM/Exception/SingletonViolationException.php new file mode 100644 index 00000000..3e5ed60c --- /dev/null +++ b/src/ZM/Exception/SingletonViolationException.php @@ -0,0 +1,17 @@ + $argv 传入的参数(见 ServerStartCommand) - * @throws InitException * @throws \Exception */ public function __construct(array $argv = []) { // 单例化整个Framework类 if (self::$instance !== null) { - throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Framework in one instance is not allowed!'); + throw new SingletonViolationException(self::class); } self::$instance = $this; diff --git a/src/ZM/InstantApplication.php b/src/ZM/InstantApplication.php index 95f79556..c6380078 100644 --- a/src/ZM/InstantApplication.php +++ b/src/ZM/InstantApplication.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace ZM; use ZM\Command\Server\ServerStartCommand; -use ZM\Exception\InitException; +use ZM\Exception\SingletonViolationException; use ZM\Plugin\InstantPlugin; class InstantApplication extends InstantPlugin @@ -16,14 +16,10 @@ class InstantApplication extends InstantPlugin /** @var array 存储要传入的args */ private array $args = []; - /** - * @param null|mixed $dir - * @throws InitException - */ - public function __construct($dir = null) + public function __construct(mixed $dir = null) { if (self::$obj !== null) { - throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Application is not allowed!'); + throw new SingletonViolationException(self::class); } self::$obj = $this; // 用于标记已经初始化完成 parent::__construct($dir ?? (__DIR__ . '/../..')); From 4bf328ef67aad5896ae7b5cb5511ba95152a51d9 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Fri, 16 Dec 2022 17:59:32 +0800 Subject: [PATCH 3/6] refactor InitCommand --- composer.json | 4 +- src/ZM/Command/Command.php | 73 +++++++++++ src/ZM/Command/InitCommand.php | 219 +++++++++++++++++++++------------ 3 files changed, 214 insertions(+), 82 deletions(-) create mode 100644 src/ZM/Command/Command.php 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; } } From a430cf59c407c9a46e06c70006eee717ea71e4cd Mon Sep 17 00:00:00 2001 From: sunxyw Date: Fri, 16 Dec 2022 21:58:19 +0800 Subject: [PATCH 4/6] add docker init to InitCommand --- docker/walle-q/Dockerfile | 12 ++-- src/Globals/global_defines_app.php | 5 +- src/ZM/Command/InitCommand.php | 106 +++++++++++++++++++++++------ 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/docker/walle-q/Dockerfile b/docker/walle-q/Dockerfile index 1a69aab8..2d784d2f 100644 --- a/docker/walle-q/Dockerfile +++ b/docker/walle-q/Dockerfile @@ -2,11 +2,13 @@ FROM alpine:latest MAINTAINER sunxyw -RUN apk add --no-cache curl && \ - mkdir -p /bot && \ - curl -L -o /bot/walle-q https://github.com/onebot-walle/walle-q/releases/latest/download/walle-q-i686-linux-musl && \ - chmod +x /bot/walle-q +RUN apk add --no-cache curl +RUN mkdir -p /bot && \ + curl -fsSL https://github.com/onebot-walle/walle-q/releases/latest/download/walle-q-i686-linux-musl.tar.gz -O && \ + tar -zxvf walle-q-i686-linux-musl.tar.gz -C /bot && \ + rm -rf walle-q-i686-linux-musl.tar.gz && \ + chmod +x /bot/walle-q-i686-linux-musl ENV TZ=Asia/Shanghai -ENTRYPOINT [ "/bot/walle-q" ] +ENTRYPOINT [ "/bot/walle-q-i686-linux-musl" ] diff --git a/src/Globals/global_defines_app.php b/src/Globals/global_defines_app.php index e2af9fa4..ed798513 100644 --- a/src/Globals/global_defines_app.php +++ b/src/Globals/global_defines_app.php @@ -31,6 +31,9 @@ const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法 const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由 const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许 +const LOAD_MODE_VENDOR = 0; // 从 vendor 加载 +const LOAD_MODE_SRC = 1; // 从 src 加载 + /* 定义工作目录 */ define('WORKING_DIR', getcwd()); @@ -48,7 +51,7 @@ if (DIRECTORY_SEPARATOR === '\\') { } /* 定义启动模式,这里指的是框架本身的源码目录是通过 composer 加入 vendor 加载的还是直接放到 src 目录加载的,前者为 1,后者为 0 */ -define('LOAD_MODE', is_dir(zm_dir(SOURCE_ROOT_DIR . '/src/ZM')) ? 0 : 1); +define('LOAD_MODE', is_dir(zm_dir(SOURCE_ROOT_DIR . '/src/ZM')) ? LOAD_MODE_VENDOR : LOAD_MODE_SRC); /* 定义框架本身所处的根目录,此处如果 LOAD_MODE 为 1 的话,框架自身的根目录在 vendor/zhamao/framework 子目录下 */ if (Phar::running() !== '') { diff --git a/src/ZM/Command/InitCommand.php b/src/ZM/Command/InitCommand.php index 7ca65cdc..e4ac59ab 100644 --- a/src/ZM/Command/InitCommand.php +++ b/src/ZM/Command/InitCommand.php @@ -6,6 +6,7 @@ namespace ZM\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; use ZM\Exception\InitException; class InitCommand extends Command @@ -19,9 +20,10 @@ class InitCommand extends Command protected function configure(): void { - $this->setDescription('Initialize framework starter | 初始化框架运行的基础文件'); + $this->setDescription('初始化框架运行的基础文件'); $this->setDefinition([ - new InputOption('force', 'F', null, '强制重制,覆盖现有文件'), + new InputOption('force', 'f', InputOption::VALUE_NONE, '覆盖现有文件'), + new InputOption('docker', null, InputOption::VALUE_NONE, '启用 Docker 支持'), ]); $this->setHelp('提取框架的基础文件到当前目录,以便于快速开始开发。'); } @@ -32,25 +34,10 @@ class InitCommand extends Command $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 { - $section->writeln('跳过(已存在)'); - } - } + $this->extractFiles($this->getExtractFiles(), $section); }); - if (LOAD_MODE === 1) { + if (LOAD_MODE === LOAD_MODE_SRC) { $this->section('应用自动加载配置', function (ConsoleSectionOutput $section) { $autoload = [ 'psr-4' => [ @@ -62,6 +49,8 @@ class InitCommand extends Command ], ]; + $section->write('更新 composer.json ... '); + if (!file_exists($this->base_path . '/composer.json')) { throw new InitException('未找到 composer.json 文件', '请检查当前目录是否为项目根目录', 41); } @@ -84,13 +73,39 @@ class InitCommand extends Command throw new InitException('写入 composer.json 文件失败', '', 0, $e); } - $section->writeln('执行 composer dump-autoload ...'); + $section->writeln('完成'); + + $section->write('执行 composer dump-autoload ... '); exec('composer dump-autoload'); $section->writeln('完成'); }); } + if ($this->input->getOption('docker')) { + $this->section('应用 Docker 支持', function (ConsoleSectionOutput $section) { + $files = $this->getFilesFromPatterns([ + '/docker/*/Dockerfile', + '/docker/environment.env.example', + '/docker-compose.yml', + ]); + $this->extractFiles($files, $section); + + // 生成 env 文件 + if ($this->shouldExtractFile('/docker/environment.env')) { + $section->write('生成环境变量文件 ... '); + $env = file_get_contents($this->base_path . '/docker/environment.env.example'); + foreach ($this->getEnvVariables() as $key => $value) { + $env = $this->injectEnv($env, $key, $value); + } + file_put_contents($this->base_path . '/docker/environment.env', $env); + $section->writeln('完成'); + } else { + $section->writeln('生成环境变量文件 ... 跳过(已存在)'); + } + }); + } + // 将命令行入口标记为可执行 chmod($this->base_path . '/zhamao', 0755); return 0; @@ -105,10 +120,15 @@ class InitCommand extends Command '/src/Globals/*.php', ]; + return $this->getFilesFromPatterns($patterns); + } + + private function getFilesFromPatterns(array $patterns): array + { $files = []; foreach ($patterns as $pattern) { // TODO: 优化代码,避免在循环中使用 array_merge 以减少资源消耗 - $files = array_merge($files, glob($this->getVendorPath($pattern))); + $files = array_merge($files, glob($this->getVendorPath($pattern), GLOB_BRACE)); } return array_map(function ($file) { return str_replace($this->getVendorPath(''), '', $file); @@ -173,4 +193,48 @@ class InitCommand extends Command } return $this->base_path . '/vendor/' . $package_name . $file; } + + private function extractFiles(array $files, OutputInterface $output): void + { + foreach ($files as $file) { + $output->write("提取 {$file} ... "); + if ($this->shouldExtractFile($file)) { + try { + $this->extractFile($file); + $output->write('完成'); + } catch (InitException $e) { + $output->write('失败'); + throw $e; + } finally { + $output->writeln(''); + } + } else { + $output->writeln('跳过(已存在)'); + } + } + } + + private function injectEnv(string $env, string $key, string $value): string + { + $pattern = "/^{$key}=.+$/m"; + if (preg_match($pattern, $env)) { + return preg_replace($pattern, "{$key}={$value}", $env); + } + + return $env . PHP_EOL . "{$key}={$value}"; + } + + private function getEnvVariables(): array + { + return [ + 'REDIS_PASSWORD' => bin2hex(random_bytes(8)), + + 'POSTGRES_USER' => 'root', + 'POSTGRES_PASSWORD' => bin2hex(random_bytes(8)), + + 'POSTGRES_APPLICATION_DATABASE' => 'zhamao', + 'POSTGRES_APPLICATION_USER' => 'zhamao', + 'POSTGRES_APPLICATION_USER_PASSWORD' => bin2hex(random_bytes(8)), + ]; + } } From 65bb6a2473f64d45a5753bac17e521d4ef65bec1 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sat, 17 Dec 2022 16:27:49 +0800 Subject: [PATCH 5/6] improve Command phpdoc --- src/ZM/Command/Command.php | 65 +++++++++++++++++++++++++++++++++- src/ZM/Command/InitCommand.php | 2 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/ZM/Command/Command.php b/src/ZM/Command/Command.php index 17a1eabb..9ded2f91 100644 --- a/src/ZM/Command/Command.php +++ b/src/ZM/Command/Command.php @@ -6,15 +6,28 @@ namespace ZM\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use ZM\Exception\ZMException; abstract class Command extends \Symfony\Component\Console\Command\Command { + /** + * 输入 + */ protected InputInterface $input; + /** + * 输出 + * + * 一般来说同样会是 ConsoleOutputInterface + */ protected OutputInterface $output; + /** + * {@inheritdoc} + * @internal 不建议覆写此方法,建议使用 {@see handle()} 方法 + */ protected function execute(InputInterface $input, OutputInterface $output): int { $this->input = $input; @@ -22,38 +35,88 @@ abstract class Command extends \Symfony\Component\Console\Command\Command return $this->handle(); } + /** + * 命令的主体 + * + * @return int 命令执行结果 {@see self::SUCCESS} 或 {@see self::FAILURE} 或 {@see self::INVALID} + */ abstract protected function handle(): int; + /** + * 输出一段文本,默认样式 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + * @see OutputInterface::write() + */ protected function write(string $message, bool $newline = true): void { $this->output->write($message, $newline); } + /** + * 输出文本,一般用于提示信息 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + */ protected function info(string $message, bool $newline = true): void { $this->write("{$message}", $newline); } + /** + * 输出文本,一般用于错误信息 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + */ protected function error(string $message, bool $newline = true): void { $this->write("{$message}", $newline); } + /** + * 输出文本,一般用于警告或附注信息 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + */ protected function comment(string $message, bool $newline = true): void { $this->write("{$message}", $newline); } + /** + * 输出文本,一般用于提问信息 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + */ protected function question(string $message, bool $newline = true): void { $this->write("{$message}", $newline); } + /** + * 输出文本,一般用于详细信息 + * + * @param string $message 要输出的文本 + * @param bool $newline 是否在文本后换行 + */ protected function detail(string $message, bool $newline = true): void { $this->write("{$message}", $newline); } + /** + * 输出一个区块,区块内内容可以覆写 + * + * 此功能需要 $output 为 {@see ConsoleOutputInterface} 类型 + * + * @param string $message 作为标题的文本 + * @param callable $callback 回调函数,接收一个参数,类型为 {@see ConsoleSectionOutput} + */ protected function section(string $message, callable $callback): void { $output = $this->output; @@ -67,7 +130,7 @@ abstract class Command extends \Symfony\Component\Console\Command\Command $callback($section); } catch (ZMException $e) { $this->error($e->getMessage()); - exit(1); + exit(self::FAILURE); } } } diff --git a/src/ZM/Command/InitCommand.php b/src/ZM/Command/InitCommand.php index e4ac59ab..e841eea4 100644 --- a/src/ZM/Command/InitCommand.php +++ b/src/ZM/Command/InitCommand.php @@ -108,7 +108,7 @@ class InitCommand extends Command // 将命令行入口标记为可执行 chmod($this->base_path . '/zhamao', 0755); - return 0; + return self::SUCCESS; } private function getExtractFiles(): array From 385f217b96fc29510467d5b147b437e90fc31966 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sat, 17 Dec 2022 16:29:28 +0800 Subject: [PATCH 6/6] remove symfony/polyfill-php80 --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 776d67bd..02a7c2a1 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ "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": {