mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-23 16:45:35 +08:00
update plugin install and load strategy
This commit is contained in:
@@ -25,7 +25,7 @@ class PluginManager
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入插件父目录,扫描插件目录下的所有插件并注册添加
|
||||
* 传入插件父目录,扫描插件目录下的所有插件并注册添加(开发插件)
|
||||
*
|
||||
* @param string $dir 插件目录
|
||||
* @return int 返回添加插件的数量
|
||||
@@ -54,21 +54,31 @@ class PluginManager
|
||||
}
|
||||
|
||||
// 先看有没有 zmplugin.json,没有则不是正常的插件,发个 notice 然后跳过
|
||||
$meta_file = $item . '/zmplugin.json';
|
||||
$meta_file = $item . '/composer.json';
|
||||
if (!is_file($meta_file)) {
|
||||
logger()->notice('插件目录 {dir} 没有插件元信息(zmplugin.json),跳过扫描。', ['dir' => $item]);
|
||||
logger()->notice('插件目录 {dir} 没有插件元信息(composer.json),跳过扫描。', ['dir' => $item]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检验元信息是否合法,不合法发个 notice 然后跳过
|
||||
$json_meta = json_decode(file_get_contents($meta_file), true);
|
||||
if (!is_array($json_meta)) {
|
||||
logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
|
||||
logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
|
||||
continue;
|
||||
}
|
||||
if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
|
||||
logger()->notice('插件目录 {dir} 的插件元信息未提供版本和名称,不是有效的插件,跳过扫描。', ['dir' => $item]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构造一个元信息对象
|
||||
$meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_SOURCE, $item);
|
||||
$meta = new PluginMeta(
|
||||
name: $json_meta['name'],
|
||||
version: $json_meta['extra']['zm-plugin-version'],
|
||||
description: $json_meta['description'] ?? '',
|
||||
plugin_type: ZM_PLUGIN_TYPE_SOURCE,
|
||||
root_dir: $item
|
||||
);
|
||||
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
|
||||
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
|
||||
continue;
|
||||
@@ -93,18 +103,29 @@ class PluginManager
|
||||
// 加载这个 Phar 文件
|
||||
$phar = require $phar_path;
|
||||
// 读取元信息
|
||||
$plugin_file_path = zm_dir('phar://' . $phar_path . '/zmplugin.json');
|
||||
$plugin_file_path = zm_dir('phar://' . $phar_path . '/composer.json');
|
||||
if (!file_exists($plugin_file_path)) {
|
||||
throw new PluginException('插件元信息 zmplugin.json 文件不存在');
|
||||
throw new PluginException('插件元信息 composer.json 文件不存在');
|
||||
}
|
||||
// 解析元信息的 JSON
|
||||
$meta_json = json_decode(file_get_contents($plugin_file_path), true);
|
||||
$json_meta = json_decode(file_get_contents($plugin_file_path), true);
|
||||
// 失败抛出异常
|
||||
if (!is_array($meta_json)) {
|
||||
if (!is_array($json_meta)) {
|
||||
throw new PluginException('插件信息文件解析失败');
|
||||
}
|
||||
// 解析 name 和版本失败
|
||||
if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
|
||||
throw new PluginException('插件文件 ' . $phar_path . ' 的插件元信息未提供版本和名称,不是有效的插件');
|
||||
}
|
||||
// $phar 这时应该是一个 ZMPlugin 对象,写入元信息
|
||||
$meta = new PluginMeta($meta_json, ZM_PLUGIN_TYPE_PHAR, zm_dir('phar://' . $phar_path));
|
||||
// 构造一个元信息对象
|
||||
$meta = new PluginMeta(
|
||||
name: $json_meta['name'],
|
||||
version: $json_meta['extra']['zm-plugin-version'],
|
||||
description: $json_meta['description'] ?? '',
|
||||
plugin_type: ZM_PLUGIN_TYPE_PHAR,
|
||||
root_dir: zm_dir('phar://' . $phar_path)
|
||||
);
|
||||
// 如果已经返回了一个插件对象,那么直接塞进去实体
|
||||
if ($phar instanceof ZMPlugin) {
|
||||
$meta->bindEntity($phar);
|
||||
@@ -144,7 +165,7 @@ class PluginManager
|
||||
$cnt = 0;
|
||||
foreach ($json['packages'] as $item) {
|
||||
$root_dir = $vendor_dir . '/' . $item['name'];
|
||||
$meta_file = zm_dir($root_dir . '/zmplugin.json');
|
||||
$meta_file = zm_dir($root_dir . '/composer.json');
|
||||
if (!file_exists($meta_file)) {
|
||||
continue;
|
||||
}
|
||||
@@ -152,12 +173,22 @@ class PluginManager
|
||||
// 检验元信息是否合法,不合法发个 notice 然后跳过
|
||||
$json_meta = json_decode(file_get_contents($meta_file), true);
|
||||
if (!is_array($json_meta)) {
|
||||
logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
|
||||
logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
|
||||
continue;
|
||||
}
|
||||
// 解析 name 和版本失败
|
||||
if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构造一个元信息对象
|
||||
$meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_COMPOSER, zm_dir($root_dir));
|
||||
$meta = new PluginMeta(
|
||||
name: $json_meta['name'],
|
||||
version: $json_meta['extra']['zm-plugin-version'],
|
||||
description: $json_meta['description'] ?? '',
|
||||
plugin_type: ZM_PLUGIN_TYPE_COMPOSER,
|
||||
root_dir: $root_dir
|
||||
);
|
||||
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
|
||||
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
|
||||
continue;
|
||||
@@ -223,6 +254,7 @@ class PluginManager
|
||||
if ($meta->getPluginType() !== ZM_PLUGIN_TYPE_NATIVE) {
|
||||
logger()->info('正在启用插件 ' . $name);
|
||||
}
|
||||
/* 插件从 zmplugin.json 改为 composer 了,所以不需要自己判断依赖
|
||||
// 先判断依赖关系,如果声明了依赖,但依赖不合规则报错崩溃
|
||||
foreach ($meta->getDependencies() as $dep_name => $dep_version) {
|
||||
// 缺少依赖的插件,不行
|
||||
@@ -233,7 +265,7 @@ class PluginManager
|
||||
if (VersionComparator::compareVersionRange(self::$plugins[$dep_name]->getVersion(), $dep_version) === false) {
|
||||
throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是这个插件的版本不符合要求');
|
||||
}
|
||||
}
|
||||
}*/
|
||||
// 如果插件为单文件形式,且设置了 pluginLoad 事件,那就调用
|
||||
$meta->getEntity()?->emitPluginLoad($parser);
|
||||
if (($entity = $meta->getEntity()) instanceof ZMPlugin) {
|
||||
|
||||
@@ -20,41 +20,22 @@ class PluginMeta implements \JsonSerializable
|
||||
/** @var string 插件描述 */
|
||||
private string $description;
|
||||
|
||||
/** @var array 插件的依赖列表 */
|
||||
private array $dependencies;
|
||||
|
||||
/** @var null|string 插件的根目录 */
|
||||
private ?string $root_dir;
|
||||
|
||||
/** @var int 插件类型 */
|
||||
private int $plugin_type;
|
||||
|
||||
/** @var array 元信息原文 */
|
||||
private array $metas;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
private ?ZMPlugin $entity = null;
|
||||
|
||||
/**
|
||||
* @param array $meta 元信息数组格式
|
||||
* @param int $plugin_type 插件类型
|
||||
* @param null|string $root_dir 插件根目录
|
||||
*/
|
||||
public function __construct(array $meta, int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null)
|
||||
public function __construct(string $name, string $version = '1.0-dev', string $description = '', int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null)
|
||||
{
|
||||
// 设置名称
|
||||
$this->name = $meta['name'] ?? '<anonymous>';
|
||||
// 设置版本
|
||||
$this->version = $meta['version'] ?? '1.0-dev';
|
||||
// 设置描述
|
||||
$this->description = $meta['description'] ?? '';
|
||||
// 设置依赖
|
||||
$this->dependencies = $meta['dependencies'] ?? [];
|
||||
$this->metas = $meta;
|
||||
// 设置插件根目录
|
||||
$this->name = $name;
|
||||
$this->version = $version;
|
||||
$this->description = $description;
|
||||
$this->plugin_type = $plugin_type;
|
||||
// 设置插件根目录
|
||||
$this->root_dir = $root_dir;
|
||||
}
|
||||
|
||||
@@ -148,11 +129,6 @@ class PluginMeta implements \JsonSerializable
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return $this->dependencies;
|
||||
}
|
||||
|
||||
public function getRootDir(): string
|
||||
{
|
||||
return $this->root_dir;
|
||||
@@ -174,15 +150,9 @@ class PluginMeta implements \JsonSerializable
|
||||
'name' => $this->name,
|
||||
'version' => $this->version,
|
||||
'description' => $this->description,
|
||||
'dependencies' => $this->dependencies,
|
||||
];
|
||||
}
|
||||
|
||||
public function getMetas(): array
|
||||
{
|
||||
return $this->metas;
|
||||
}
|
||||
|
||||
public function getEntity(): ?ZMPlugin
|
||||
{
|
||||
return $this->entity;
|
||||
|
||||
15
src/ZM/Plugin/Strategy/ComposerStrategy.php
Normal file
15
src/ZM/Plugin/Strategy/ComposerStrategy.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin\Strategy;
|
||||
|
||||
class ComposerStrategy extends PluginInstallStrategy
|
||||
{
|
||||
public function install(array $option = []): bool
|
||||
{
|
||||
// TODO: Composer 类型的插件还没有实现怎么安装,但很简单。这次 Commit 我偏要鸽!
|
||||
$this->error = 'Not implemented';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
149
src/ZM/Plugin/Strategy/GitStrategy.php
Normal file
149
src/ZM/Plugin/Strategy/GitStrategy.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin\Strategy;
|
||||
|
||||
use ZM\Utils\ZMRequest;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
class GitStrategy extends PluginInstallStrategy
|
||||
{
|
||||
private string $git_api_link = 'https://api.github.com/repos/{owner}/{repo}/contents/composer.json';
|
||||
|
||||
private string $token = '';
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function install(array $option = []): bool
|
||||
{
|
||||
// 应用 git_api_link
|
||||
if (isset($option['git-api-link'])) {
|
||||
$this->git_api_link = $option['git-api-link'];
|
||||
}
|
||||
if (isset($option['github-token'])) {
|
||||
$this->token = $option['github-token'];
|
||||
}
|
||||
|
||||
$git_url = parse_url($this->input);
|
||||
|
||||
// GitHub 做特殊处理,直接调用 API 检查
|
||||
$is_github = false;
|
||||
$plugin_name = null;
|
||||
if ($git_url['host'] === 'github.com' && ($option['github-skip-check'] ?? false) !== true) {
|
||||
if (!$this->checkGitAPI($git_url, $plugin_name)) {
|
||||
return false;
|
||||
}
|
||||
$is_github = true;
|
||||
}
|
||||
|
||||
// 使用 Composer 管理插件,将仓库链接绑定到 composer.json
|
||||
$this->logger->info('正在使用 Composer 下载并安装插件');
|
||||
$composer = ZMUtil::getComposerMetadata($this->root_composer_path);
|
||||
$origin_composer = $composer;
|
||||
$already_has_repo = false;
|
||||
// 不破坏原有队列,加入 GitHub 的 repo
|
||||
if (!isset($composer['repositories'])) {
|
||||
$composer['repositories'] = [];
|
||||
}
|
||||
if (is_assoc_array($composer['repositories'])) {
|
||||
$composer['repositories'] = [$composer['repositories']];
|
||||
}
|
||||
foreach ($composer['repositories'] as $v) {
|
||||
if (($v['url'] ?? '') === $this->input) {
|
||||
$already_has_repo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$already_has_repo) {
|
||||
$composer['repositories'][] = [
|
||||
'type' => $is_github ? 'github' : 'git',
|
||||
'url' => $this->input,
|
||||
'.belongs' => $plugin_name,
|
||||
];
|
||||
}
|
||||
// 写入 composer.json
|
||||
if (ZMUtil::putComposerMetadata($this->root_composer_path, $composer) === false) {
|
||||
$this->error = '写入 composer.json 失败';
|
||||
return false;
|
||||
}
|
||||
$env = getenv('COMPOSER_EXECUTABLE');
|
||||
if ($env === false) {
|
||||
$env = 'composer';
|
||||
}
|
||||
if ($plugin_name === null) {
|
||||
$this->error = '没有从 Git 获取到插件的元信息,目前无法从 GitHub 以外的 Git 仓库下载插件,后续会更新!';
|
||||
ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
|
||||
return false;
|
||||
}
|
||||
if ($plugin_name === '') {
|
||||
$this->error = '获取插件名称失败!';
|
||||
ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
|
||||
return false;
|
||||
}
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGINT, function () use ($origin_composer) {
|
||||
echo "强行中断,恢复 Composer 中\n";
|
||||
ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
|
||||
});
|
||||
}
|
||||
passthru("{$env} require {$plugin_name}", $code);
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGINT, SIG_IGN);
|
||||
}
|
||||
if ($code !== 0) {
|
||||
$this->error = '使用 composer 引入 Git 插件出现了一些错误,请看上方错误';
|
||||
ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于调用 GitHub 的 API 用作不下载就检查插件是否合规
|
||||
*
|
||||
* @param array $git_url 解析后的链接
|
||||
*/
|
||||
private function checkGitAPI(array $git_url, ?string &$plugin_name = null): bool
|
||||
{
|
||||
$this->logger->info('正在检查 GitHub 插件是否为框架插件');
|
||||
[, $owner, $repo] = explode('/', $git_url['path']);
|
||||
if (str_ends_with($repo, '.git')) {
|
||||
$repo = substr($repo, 0, -4);
|
||||
}
|
||||
|
||||
// 调用 HTTP 客户端获取 API 信息
|
||||
$header = ['User-Agent' => 'zhamao-framework'];
|
||||
if ($this->token !== '') {
|
||||
$header['Authorization'] = 'token ' . $this->token;
|
||||
}
|
||||
$api = ZMRequest::get(
|
||||
str_replace(['{owner}', '{repo}'], [$owner, $repo], $this->git_api_link),
|
||||
$header,
|
||||
only_body: false
|
||||
);
|
||||
if ($api->getStatusCode() !== 200) {
|
||||
$this->error = "GitHub API 请求失败[{$api->getStatusCode()}]";
|
||||
if ($api->getStatusCode() === 403) {
|
||||
$this->error .= '可能是 API 滥用导致的,建议生成一个 GitHub Token。';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查插件的 composer.json 是否合规
|
||||
$content = json_decode($api->getBody()->getContents(), true);
|
||||
if (isset($content['message'])) {
|
||||
$this->error = '该 GitHub 仓库中不存在 composer.json 文件!';
|
||||
return false;
|
||||
}
|
||||
$contents = implode('', array_map(fn ($x) => base64_decode($x), explode("\n", $content['content'])));
|
||||
$json = json_decode($contents, true);
|
||||
if (!$this->checkComposerIntegrity($json)) {
|
||||
return false;
|
||||
}
|
||||
$plugin_name = $json['name'];
|
||||
$this->installed_name = $plugin_name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
67
src/ZM/Plugin/Strategy/PluginInstallStrategy.php
Normal file
67
src/ZM/Plugin/Strategy/PluginInstallStrategy.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin\Strategy;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Plugin\PluginManager;
|
||||
|
||||
abstract class PluginInstallStrategy
|
||||
{
|
||||
protected string $error = '';
|
||||
|
||||
protected string $installed_name = '';
|
||||
|
||||
public function __construct(
|
||||
protected string $input,
|
||||
protected string $plugin_dir,
|
||||
protected string $root_composer_path = '',
|
||||
protected ?LoggerInterface $logger = null,
|
||||
) {
|
||||
if ($this->root_composer_path === '') {
|
||||
$this->root_composer_path = zm_dir(WORKING_DIR);
|
||||
}
|
||||
if ($this->logger === null) {
|
||||
$this->logger = ob_logger();
|
||||
}
|
||||
}
|
||||
|
||||
abstract public function install(array $option = []): bool;
|
||||
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function getInstalledName(): string
|
||||
{
|
||||
return $this->installed_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于检查 Composer 文件的信息是否完整
|
||||
*/
|
||||
protected function checkComposerIntegrity(mixed $composer): bool
|
||||
{
|
||||
// 必须是 array
|
||||
if (!is_array($composer)) {
|
||||
$this->error = 'composer.json 元信息获取出错';
|
||||
return false;
|
||||
}
|
||||
if (!isset($composer['extra']['zm-plugin-version'])) {
|
||||
$this->error = 'composer.json 内没有标明该炸毛插件的版本,或该仓库不是炸毛插件';
|
||||
return false;
|
||||
}
|
||||
if (!isset($composer['name'])) {
|
||||
$this->error = 'composer.json 插件元信息内没有名字!';
|
||||
return false;
|
||||
}
|
||||
$plugin_name = $composer['name'];
|
||||
if (PluginManager::isPluginExists($plugin_name)) {
|
||||
$this->error = "插件 {$plugin_name} 已存在,无法再次安装";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user