From 579269cb5b83552c66a55b8b8b66f26410b5bd4a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 25 Dec 2022 23:11:10 +0800 Subject: [PATCH] add plugin make command --- src/Templates/PluginMain.php.template | 14 ++ src/Templates/main.php.template | 14 ++ src/ZM/Command/Command.php | 5 + src/ZM/Command/Plugin/PluginMakeCommand.php | 123 ++++++++++++++++++ src/ZM/Store/FileSystem.php | 2 +- .../Utils/CodeGenerator/PluginGenerator.php | 84 ++++++++++++ 6 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/Templates/PluginMain.php.template create mode 100644 src/Templates/main.php.template create mode 100644 src/ZM/Command/Plugin/PluginMakeCommand.php create mode 100644 src/ZM/Utils/CodeGenerator/PluginGenerator.php diff --git a/src/Templates/PluginMain.php.template b/src/Templates/PluginMain.php.template new file mode 100644 index 00000000..b845d056 --- /dev/null +++ b/src/Templates/PluginMain.php.template @@ -0,0 +1,14 @@ +reply('这是{name}插件的第一个命令!'); + } +} diff --git a/src/Templates/main.php.template b/src/Templates/main.php.template new file mode 100644 index 00000000..693ca86c --- /dev/null +++ b/src/Templates/main.php.template @@ -0,0 +1,14 @@ +on(fn () => '这是{name}插件的第一个命令!'); + +$plugin->addBotCommand($cmd1); + +return $plugin; diff --git a/src/ZM/Command/Command.php b/src/ZM/Command/Command.php index 97893708..ebb87910 100644 --- a/src/ZM/Command/Command.php +++ b/src/ZM/Command/Command.php @@ -33,6 +33,11 @@ abstract class Command extends \Symfony\Component\Console\Command\Command $this->input = $input; $this->output = $output; if ($this->shouldExecute()) { + if (property_exists($this, 'bootstrappers')) { + foreach ($this->bootstrappers as $bootstrapper) { + (new $bootstrapper())->bootstrap($this->input->getOptions()); + } + } return $this->handle(); } return self::SUCCESS; diff --git a/src/ZM/Command/Plugin/PluginMakeCommand.php b/src/ZM/Command/Plugin/PluginMakeCommand.php new file mode 100644 index 00000000..d55dbfc3 --- /dev/null +++ b/src/ZM/Command/Plugin/PluginMakeCommand.php @@ -0,0 +1,123 @@ +addArgument('name', InputArgument::OPTIONAL, '插件名称', null); + $this->addOption('author', 'a', InputOption::VALUE_OPTIONAL, '作者名称', null); + $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, '插件描述', null); + $this->addOption('plugin-version', null, InputOption::VALUE_OPTIONAL, '插件版本', '1.0.0'); + $this->addOption('type', 'T', InputOption::VALUE_OPTIONAL, '插件类型', null); + + // 下面是 type=psr4 的选项 + $this->addOption('namespace', null, InputOption::VALUE_OPTIONAL, '插件命名空间', null); + + // 下面是辅助用的,和 server:start 一样 + $this->addOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录'); + } + + /** + * {@inheritDoc} + */ + protected function handle(): int + { + $load_dir = config('global.plugin.load_dir'); + if (empty($load_dir)) { + $load_dir = SOURCE_ROOT_DIR . '/plugins'; + } elseif (FileSystem::isRelativePath($load_dir)) { + $load_dir = SOURCE_ROOT_DIR . '/' . $load_dir; + } + $plugin_dir = zm_dir($load_dir); + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + + $other_plugins = is_dir($plugin_dir) ? FileSystem::scanDirFiles($plugin_dir, false, true, true) : []; + + // 询问插件名称 + if ($this->input->getArgument('name') === null) { + $question = new Question('请输入插件名称:'); + $question->setValidator(function ($answer) use ($plugin_dir, $other_plugins) { + if (empty($answer)) { + throw new \RuntimeException('插件名称不能为空'); + } + if (is_numeric(mb_substr($answer, 0, 1))) { + throw new \RuntimeException('插件名称不能以数字开头,且只能包含字母、数字、下划线、短横线'); + } + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $answer)) { + throw new \RuntimeException('插件名称只能包含字母、数字、下划线、短横线'); + } + if (is_dir($plugin_dir . '/' . strtolower($answer))) { + throw new \RuntimeException('插件目录已存在,请换个名字'); + } + foreach ($other_plugins as $dir_name) { + $plugin_name = file_exists($plugin_dir . '/' . $dir_name . '/zmplugin.json') ? (json_decode(file_get_contents($plugin_dir . '/' . $dir_name . '/zmplugin.json'), true)['name'] ?? null) : null; + if ($plugin_name !== null && $plugin_name === $answer) { + throw new \RuntimeException('插件名称已存在,请换个名字'); + } + } + return $answer; + }); + $this->input->setArgument('name', $helper->ask($this->input, $this->output, $question)); + } + + // 询问插件类型 + if ($this->input->getOption('type') === null) { + $question = new ChoiceQuestion( + '请输入要生成的插件结构类型', + ['file' => 'file 类型为单文件,方便写简单功能', 'psr4' => 'psr4 类型为目录,按照 psr-4 结构生成,同时将生成 composer.json 用来支持自动加载'] + ); + $this->input->setOption('type', $helper->ask($this->input, $this->output, $question)); + } + + if ($this->input->getOption('type') === 'psr4') { + // 询问命名空间 + if ($this->input->getOption('namespace') === null) { + $question = new Question('请输入插件命名空间:'); + $question->setValidator(function ($answer) { + if (empty($answer)) { + throw new \RuntimeException('插件命名空间不能为空'); + } + if (is_numeric(mb_substr($answer, 0, 1))) { + throw new \RuntimeException('插件命名空间不能以数字开头,且只能包含字母、数字、反斜线'); + } + // 只能包含字母、数字和反斜线 + if (!preg_match('/^[a-zA-Z0-9\\\\]+$/', $answer)) { + throw new \RuntimeException('插件命名空间只能包含字母、数字、反斜线'); + } + return $answer; + }); + $this->input->setOption('namespace', $helper->ask($this->input, $this->output, $question)); + } + } + + $generator = new PluginGenerator($this->input->getArgument('name'), $plugin_dir); + $generator->generate($this->input->getOptions()); + + $this->info('已生成插件:' . $this->input->getArgument('name')); + $this->info('目录位置:' . zm_dir($plugin_dir . '/' . $this->input->getArgument('name'))); + return self::SUCCESS; + } +} diff --git a/src/ZM/Store/FileSystem.php b/src/ZM/Store/FileSystem.php index 685f18a3..8ce7e3e7 100644 --- a/src/ZM/Store/FileSystem.php +++ b/src/ZM/Store/FileSystem.php @@ -82,7 +82,7 @@ class FileSystem */ public static function createDir(string $path): void { - if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) { + if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) { throw new \RuntimeException(sprintf('无法建立目录:%s', $path)); } } diff --git a/src/ZM/Utils/CodeGenerator/PluginGenerator.php b/src/ZM/Utils/CodeGenerator/PluginGenerator.php new file mode 100644 index 00000000..3396afb3 --- /dev/null +++ b/src/ZM/Utils/CodeGenerator/PluginGenerator.php @@ -0,0 +1,84 @@ +plugin_dir); + + // 创建插件目录 + $plugin_base_dir = $this->plugin_dir . '/' . $this->name; + FileSystem::createDir($plugin_base_dir); + + // 这里开始写入 zmplugin.json + // 创建插件信息文件 + $zmplugin['name'] = $this->name; + // 设置版本 + if ($options['plugin-version'] !== null) { + $zmplugin['version'] = $options['plugin-version']; + } + // 设置作者 + if ($options['author'] !== null) { + $zmplugin['author'] = $options['author']; + } + // 判断单文件还是 psr-4 类型 + if ($options['type'] === 'file') { + // 设置入口文件为 main.php + $zmplugin['main'] = 'main.php'; + } + // 到这里就可以写入文件了 + file_put_contents(zm_dir($plugin_base_dir . '/zmplugin.json'), json_encode($zmplugin, JSON_PRETTY_PRINT)); + + // 接着写入 main.php + if ($options['type'] === 'file') { + $template = file_get_contents(zm_dir(FRAMEWORK_ROOT_DIR . '/src/Templates/main.php.template')); + $replace = ['{name}' => $this->name]; + $main_php = str_replace(array_keys($replace), array_values($replace), $template); + file_put_contents(zm_dir($plugin_base_dir . '/main.php'), $main_php); + } else { + // 如果是 psr4 就复杂一点,但也不麻烦 + // 先创建 src 目录 + FileSystem::createDir($plugin_base_dir . '/src'); + // 再创建 src/PluginMain.php + $template = file_get_contents(zm_dir(FRAMEWORK_ROOT_DIR . '/src/Templates/PluginMain.php.template')); + $replace = [ + '{name}' => $this->name, + '{namespace}' => $options['namespace'], + '{class}' => $this->convertClassName(), + ]; + $main_php = str_replace(array_keys($replace), array_values($replace), $template); + file_put_contents(zm_dir($plugin_base_dir . '/src/PluginMain.php'), $main_php); + // 写入 composer.json + $composer_json = [ + 'autoload' => [ + 'psr-4' => [ + $options['namespace'] . '\\' => 'src/', + ], + ], + ]; + file_put_contents(zm_dir($plugin_base_dir . '/composer.json'), json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + // TODO: 寻找 PHP 运行环境和 Composer 是否在当前目录的情况 + chdir($plugin_base_dir); + passthru('composer dump-autoload'); + chdir(WORKING_DIR); + } + } + + public function convertClassName(): string + { + $name = $this->name; + $string = str_replace(['-', '_'], ' ', $name); + return str_replace(' ', '', ucwords($string)); + } +}