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']);
}
}