diff --git a/src/ZM/Command/Plugin/PluginCommand.php b/src/ZM/Command/Plugin/PluginCommand.php index ed14771a..185cb190 100644 --- a/src/ZM/Command/Plugin/PluginCommand.php +++ b/src/ZM/Command/Plugin/PluginCommand.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace ZM\Command\Plugin; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; use ZM\Bootstrap; use ZM\Command\Command; +use ZM\Plugin\PluginManager; abstract class PluginCommand extends Command { @@ -18,6 +22,111 @@ abstract class PluginCommand extends Command Bootstrap\LoadPlugins::class, ]; + /** + * 插件名称合规验证器 + */ + public function validatePluginName(string $answer): string + { + 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('插件名称只能包含字母、数字、下划线、短横线'); + } + $exp = explode('/', $answer); + if (count($exp) !== 2) { + throw new \RuntimeException('插件名称必须为"组织或所有者/插件名称"的格式,且只允许有一个斜杠分割两者'); + } + if ($exp[0] === 'zhamao') { + throw new \RuntimeException('插件所有者或组织名不可以为"zhamao",请换个名字'); + } + if ($exp[0] === '' || $exp[1] === '') { + throw new \RuntimeException('插件所有者或组织名、插件名称均不可为空'); + } + if (PluginManager::isPluginExists($answer)) { + throw new \RuntimeException('名称为 ' . $answer . ' 的插件已存在,请换个名字'); + } + return $answer; + } + + /** + * 命名空间合规验证器 + */ + public function validateNamespace(string $answer): string + { + 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; + } + + /** + * 添加 Question 来询问缺失的参数 + * + * @param string $name 参数名称 + * @param string $question 问题 + * @param callable $validator 验证器 + */ + protected function questionWithArgument(string $name, string $question, callable $validator): void + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question('' . $question . ''); + $question->setValidator($validator); + $this->input->setArgument($name, $helper->ask($this->input, $this->output, $question)); + } + + /** + * 添加 Question 来询问缺失的参数 + * + * @param string $name 参数名称 + * @param string $question 问题 + * @param callable $validator 验证器 + */ + protected function questionWithOption(string $name, string $question, callable $validator): void + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new Question('' . $question . ''); + $question->setValidator($validator); + $this->input->setOption($name, $helper->ask($this->input, $this->output, $question)); + } + + /** + * 添加选择题来询问缺失的参数 + * + * @param string $name 可选参数名称 + * @param string $question 问题 + * @param array $selection 选项(K-V 类型) + */ + protected function choiceWithOption(string $name, string $question, array $selection): void + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion('' . $question . '', $selection); + $this->input->setOption($name, $helper->ask($this->input, $this->output, $question)); + } + + protected function getTypeDisplayName(int $type): string + { + return match ($type) { + ZM_PLUGIN_TYPE_NATIVE => '内部', + ZM_PLUGIN_TYPE_PHAR => 'Phar', + ZM_PLUGIN_TYPE_SOURCE => '源码', + ZM_PLUGIN_TYPE_COMPOSER => 'Composer 外部加载' + }; + } + protected function execute(InputInterface $input, OutputInterface $output): int { return parent::execute($input, $output); diff --git a/src/ZM/Command/Plugin/PluginListCommand.php b/src/ZM/Command/Plugin/PluginListCommand.php new file mode 100644 index 00000000..9ecdfd90 --- /dev/null +++ b/src/ZM/Command/Plugin/PluginListCommand.php @@ -0,0 +1,26 @@ +output); + $table->setHeaders(['名称', '版本', '类型']); + foreach ($all as $k => $v) { + $table->addRow([$k, $v->getVersion(), $this->getTypeDisplayName($v->getPluginType())]); + } + $table->setStyle('box'); + $table->render(); + return static::SUCCESS; + } +} diff --git a/src/ZM/Command/Plugin/PluginMakeCommand.php b/src/ZM/Command/Plugin/PluginMakeCommand.php index d70f9253..f357bfba 100644 --- a/src/ZM/Command/Plugin/PluginMakeCommand.php +++ b/src/ZM/Command/Plugin/PluginMakeCommand.php @@ -5,11 +5,8 @@ declare(strict_types=1); namespace ZM\Command\Plugin; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; use ZM\Store\FileSystem; use ZM\Utils\CodeGenerator\PluginGenerator; @@ -43,65 +40,24 @@ class PluginMakeCommand extends PluginCommand $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)); + $this->questionWithArgument('name', '请输入插件名称(插件名称格式为"所有者/插件名",例如"foobar/demo-plugin")', [$this, 'validatePluginName']); } // 询问插件类型 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)); + $this->choiceWithOption('type', '请输入要生成的插件结构类型', [ + 'file' => 'file 类型为单文件,方便写简单功能', + 'psr4' => 'psr4 类型为目录,按照 psr-4 结构生成,同时将生成 composer.json 用来支持自动加载(推荐)', + ]); } 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)); + $this->questionWithOption('namespace', '请输入插件命名空间:', [$this, 'validateNamespace']); } }