refactor plugin make command

This commit is contained in:
crazywhalecc 2023-02-20 23:41:24 +08:00 committed by Jerry
parent be2de100aa
commit 055d6d7b3a
3 changed files with 141 additions and 50 deletions

View File

@ -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 . '</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 . '</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>' . $question . '</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);

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ZM\Command\Plugin;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Helper\Table;
use ZM\Plugin\PluginManager;
#[AsCommand(name: 'plugin:list', description: '显示插件列表')]
class PluginListCommand extends PluginCommand
{
protected function handle(): int
{
$all = PluginManager::getPlugins();
$table = new Table($this->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;
}
}

View File

@ -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>请输入插件名称:</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(
'<question>请输入要生成的插件结构类型</question>',
['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>请输入插件命名空间:</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']);
}
}