zhamao-framework/src/ZM/Plugin/PluginManager.php

378 lines
16 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace ZM\Plugin;
use Jelix\Version\VersionComparator;
2022-12-19 01:45:27 +08:00
use ZM\Annotation\AnnotationMap;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Framework\BindEvent;
use ZM\Command\Command;
use ZM\Exception\PluginException;
2022-12-19 01:45:27 +08:00
use ZM\Store\FileSystem;
use ZM\Store\PharHelper;
class PluginManager
{
/** @var array<string, PluginMeta> 插件信息列表 */
private static array $plugins = [];
public static function getPlugins(): array
{
return self::$plugins;
}
/**
2022-12-19 01:45:27 +08:00
* 传入插件父目录,扫描插件目录下的所有插件并注册添加
*
* @param string $dir 插件目录
* @return int 返回添加插件的数量
* @throws PluginException
*/
public static function addPluginsFromDir(string $dir): int
{
// 遍历插件目录
2022-12-20 20:10:40 +08:00
if (!is_dir($dir)) {
return 0;
}
2022-12-19 01:45:27 +08:00
$list = FileSystem::scanDirFiles($dir, false, false, true);
$cnt = 0;
foreach ($list as $item) {
// 检查是不是 phar 格式的插件
if (is_file($item) && pathinfo($item, PATHINFO_EXTENSION) === 'phar') {
// 如果是PHP文件尝试添加插件
self::addPluginFromPhar($item);
++$cnt;
continue;
}
2022-12-19 01:45:27 +08:00
// 必须是目录形式的插件
if (!is_dir($item)) {
continue;
}
// 先看有没有 zmplugin.json没有则不是正常的插件发个 notice 然后跳过
$meta_file = $item . '/zmplugin.json';
if (!is_file($meta_file)) {
logger()->notice('插件目录 {dir} 没有插件元信息zmplugin.json跳过扫描。', ['dir' => $item]);
continue;
}
2022-12-19 01:45:27 +08:00
// 检验元信息是否合法,不合法发个 notice 然后跳过
$json_meta = json_decode(file_get_contents($meta_file), true);
if (!is_array($json_meta)) {
logger()->notice('插件目录 {dir} 的插件元信息zmplugin.json不是有效的 JSON跳过扫描。', ['dir' => $item]);
continue;
}
2022-12-19 01:45:27 +08:00
// 构造一个元信息对象
$meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_SOURCE, $item);
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer跳过加载');
continue;
2022-12-19 01:45:27 +08:00
}
// 添加插件到全局列表
self::addPlugin($meta);
2022-12-19 01:45:27 +08:00
++$cnt;
}
return $cnt;
}
/**
* 添加一个 Phar 文件形式的插件
2022-12-19 01:45:27 +08:00
*
* @throws PluginException
*/
public static function addPluginFromPhar(string $phar_path): void
{
$meta = [];
try {
// 加载这个 Phar 文件
$phar = require $phar_path;
// 读取元信息
$plugin_file_path = zm_dir('phar://' . $phar_path . '/zmplugin.json');
if (!file_exists($plugin_file_path)) {
throw new PluginException('插件元信息 zmplugin.json 文件不存在');
}
// 解析元信息的 JSON
$meta_json = json_decode(file_get_contents($plugin_file_path), true);
// 失败抛出异常
if (!is_array($meta_json)) {
throw new PluginException('插件信息文件解析失败');
}
// $phar 这时应该是一个 ZMPlugin 对象,写入元信息
$meta = new PluginMeta($meta_json, ZM_PLUGIN_TYPE_PHAR, zm_dir('phar://' . $phar_path));
// 如果已经返回了一个插件对象,那么直接塞进去实体
if ($phar instanceof ZMPlugin) {
$meta->bindEntity($phar);
}
// 添加到插件列表
self::addPlugin($meta);
} catch (\Throwable $e) {
throw new PluginException('Phar 插件 ' . $phar_path . ' 加载失败: ' . $e->getMessage(), previous: $e);
2022-12-19 01:45:27 +08:00
}
}
2022-12-19 01:45:27 +08:00
/**
* Composer 添加插件
* @throws PluginException
*/
public static function addPluginsFromComposer(): int
{
2023-01-28 11:29:44 +08:00
$try_list = [
SOURCE_ROOT_DIR . '/vendor',
WORKING_DIR . '/vendor',
];
foreach ($try_list as $v) {
if (file_exists($v . '/composer/installed.json')) {
$vendor_dir = $v;
break;
}
}
if (!isset($vendor_dir)) {
logger()->notice('找不到 Composer 的 installed.json 文件,跳过扫描 Composer 插件');
return 0;
}
2023-01-28 11:29:44 +08:00
$json = json_decode(file_get_contents($vendor_dir . '/composer/installed.json'), true);
if (!is_array($json)) {
logger()->notice('Composer 的 installed.json 文件解析失败,跳过扫描 Composer 插件');
return 0;
}
$cnt = 0;
foreach ($json['packages'] as $item) {
2023-01-28 11:29:44 +08:00
$root_dir = $vendor_dir . '/' . $item['name'];
$meta_file = zm_dir($root_dir . '/zmplugin.json');
if (!file_exists($meta_file)) {
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]);
continue;
}
2022-12-19 01:45:27 +08:00
// 构造一个元信息对象
$meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_COMPOSER, zm_dir($root_dir));
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer跳过加载');
continue;
2022-12-19 01:45:27 +08:00
}
// 添加插件到全局列表
self::addPlugin($meta);
++$cnt;
}
return $cnt;
}
2022-12-19 01:45:27 +08:00
/**
* 根据插件元信息对象添加一个插件到框架的全局插件库中
*
* @throws PluginException
*/
public static function addPlugin(PluginMeta $meta): void
{
logger()->debug('Adding plugin: ' . $meta->getName() . '(type:' . $meta->getPluginType() . ')');
// 首先看看有没有 entity如果还没有 entity且 entry_file 有东西,那么就从 entry_file 获取 ZMPlugin 对象
if ($meta->getEntity() === null) {
if (($entry_file = $meta->getEntryFile()) !== null) {
$entity = require $entry_file;
if ($entity instanceof ZMPlugin) {
$meta->bindEntity($entity);
}
}
}
// 如果设置了 ZMPlugin entity并且已设置了 PluginLoad 事件,那就回调
// 接下来看看有没有 autoload有的话 require_once 一下
if (($autoload = $meta->getAutoloadFile()) !== null) {
require_once $autoload;
}
// 如果既没有 entity也没有 autoload那就要抛出异常了
if ($meta->getEntity() === null && $meta->getAutoloadFile() === null) {
throw new PluginException('插件 ' . $meta->getName() . ' 既没有入口文件,也没有自动加载文件,无法加载');
2022-12-19 01:45:27 +08:00
}
// 检查同名插件,如果有同名插件,则抛出异常
if (isset(self::$plugins[$meta->getName()])) {
throw new PluginException('插件 ' . $meta->getName() . ' 已经存在(类型为' . self::$plugins[$meta->getName()]->getPluginType() . '),无法加载同名插件或重复加载!');
}
self::$plugins[$meta->getName()] = $meta;
2022-12-19 01:45:27 +08:00
}
2022-12-20 20:10:40 +08:00
/**
* 启用所有插件
*
* @param AnnotationParser $parser 传入注解解析器,用于将插件中的事件注解解析出来
* @throws PluginException
2022-12-20 20:10:40 +08:00
*/
public static function enablePlugins(AnnotationParser $parser, array $disable_list = []): void
2022-12-19 01:45:27 +08:00
{
foreach (self::$plugins as $name => $meta) {
if (in_array($name, $disable_list)) {
$meta->disablePlugin();
}
if (!$meta->isEnabled()) {
logger()->notice('插件 ' . $name . ' 已被禁用');
continue;
}
// 除了内建插件外,输出 log 告知启动插件
if ($meta->getPluginType() !== ZM_PLUGIN_TYPE_NATIVE) {
2023-02-10 13:12:20 +08:00
logger()->info('正在启用插件 ' . $name);
2022-12-19 01:45:27 +08:00
}
// 先判断依赖关系,如果声明了依赖,但依赖不合规则报错崩溃
foreach ($meta->getDependencies() as $dep_name => $dep_version) {
// 缺少依赖的插件,不行
if (!isset(self::$plugins[$dep_name])) {
throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是没有找到这个插件');
}
// 依赖的插件版本不对,不行
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) {
// 将 BotAction 加入事件监听
foreach ($entity->getBotActions() as $action) {
AnnotationMap::addSingleAnnotation($action);
$parser->parseSpecial($action);
}
// 将 BotCommand 加入事件监听
foreach ($entity->getBotCommands() as $cmd) {
AnnotationMap::addSingleAnnotation($cmd);
$parser->parseSpecial($cmd);
}
2022-12-19 01:45:27 +08:00
// 将 Event 加入事件监听
foreach ($entity->getEvents() as $event) {
2022-12-19 01:45:27 +08:00
$bind = new BindEvent($event[0], $event[2]);
$bind->on($event[1]);
AnnotationMap::addSingleAnnotation($bind);
2022-12-19 01:45:27 +08:00
}
// 将 Routes 加入事件监听
foreach ($entity->getRoutes() as $route) {
2022-12-19 01:45:27 +08:00
$parser->parseSpecial($route);
}
// 将 BotEvents 加入事件监听
foreach ($entity->getBotEvents() as $event) {
AnnotationMap::addSingleAnnotation($event);
2022-12-19 01:45:27 +08:00
}
// 将 Cron 加入注解
foreach ($entity->getCrons() as $cron) {
AnnotationMap::addSingleAnnotation($cron);
$parser->parseSpecial($cron);
2022-12-19 01:45:27 +08:00
}
// 设置 @Init 注解
foreach ($entity->getInits() as $init) {
AnnotationMap::addSingleAnnotation($init);
2022-12-19 01:45:27 +08:00
}
2023-02-10 13:12:20 +08:00
// 设置 TimerTick 注解
foreach ($entity->getTimerTicks() as $tick) {
AnnotationMap::addSingleAnnotation($tick);
$parser->parseSpecial($tick);
}
2022-12-19 01:45:27 +08:00
}
// 如果设置了 Autoload file那么将会把 psr-4 的加载路径丢进 parser
foreach ($meta->getAutoloadPsr4() as $namespace => $path) {
2023-01-28 11:29:44 +08:00
$parser->addPsr4Path($meta->getRootDir() . '/' . $path . '/', trim($namespace, '\\'), [
'plugin:' . $name,
]);
}
2022-12-19 01:45:27 +08:00
}
}
/**
* 打包插件到 Phar
*
* @throws PluginException
*/
public static function packPlugin(string $plugin_name, string $build_dir, ?Command $command_context = null): string
{
// 必须是源码模式才行
if (!isset(self::$plugins[$plugin_name]) || self::$plugins[$plugin_name]->getPluginType() !== ZM_PLUGIN_TYPE_SOURCE) {
throw new PluginException("没有找到名字为 {$plugin_name} 的插件(要打包的插件必须是源码模式)。");
}
try {
// 创建目录
FileSystem::createDir($build_dir);
$plugin = self::$plugins[$plugin_name];
// 插件目录
$dir = $plugin->getRootDir();
// 先判断是不是可写的
PharHelper::ensurePharWritable();
// 拼接 phar 名称,通过插件名和版本号(如果没有版本号则使用 1.0-dev 作为版本号)
$phar_name = $plugin->getName() . '_' . $plugin->getVersion() . '.phar';
$phar_name = zm_dir($build_dir . '/' . $phar_name);
// 判断文件如果存在的话是否是可写的
FileSystem::ensureFileWritable($phar_name);
// 文件存在先删除
if (file_exists($phar_name)) {
$command_context?->info('Phar 文件 ' . $phar_name . ' 已存在,删除中...');
unlink($phar_name);
}
// 先执行一些打包前检查的 bootstrap
// 1. 检查插件是否引用了 require-dev 的内容(检查 --no-dev
if (file_exists($dir . '/composer.json') && file_exists($dir . '/vendor/composer/installed.json')) {
$json = json_decode(file_get_contents($dir . '/vendor/composer/installed.json'), true);
if (!isset($json['dev'])) {
$command_context?->error('插件的 Composer 未正确配置,忽略检查 dev 模式!');
} elseif ($json['dev'] === true) {
throw new PluginException(
"插件的 Composer 配置了 dev 模式,但是打包时没有使用 --no-dev 选项,无法打包\n" .
'请先进入插件目录,执行 composer update --no-dev'
);
}
}
// 创建 Phar 对象
$phar = new \Phar($phar_name, 0);
// 调用插件的打包的用户自定义前置方法
$plugin->getEntity()?->emitPack();
// 扫描插件目录
$dir_list = FileSystem::scanDirFiles($dir, true, true);
if ($command_context instanceof Command) {
$dir_list = $command_context->progress()->iterate($dir_list);
}
$file_added = 0;
$file_ignored = 0;
foreach ($dir_list as $v) {
// 过滤文件
if ($plugin->getEntity()?->emitFilterPack($v) === false) {
++$file_ignored;
continue;
}
// 添加文件
$phar->addFromString($v, php_strip_whitespace(zm_dir($dir . '/' . $v)));
++$file_added;
}
// 找有没有 main没有 main 就不添加 stub
$main = (json_decode(file_get_contents($dir . '/zmplugin.json'), true)['main'] ?? 'main.php');
if (file_exists(zm_dir($dir . '/' . $main)) && $phar->offsetExists($main)) {
$command_context?->info('设置插件默认入口文件 ' . $main);
$phar->setStub($phar->setDefaultStub($main));
} else {
$phar->setStub('<?php __HALT_COMPILER();');
}
// 停止
$phar->stopBuffering();
// 输出结果
$command_context?->info("共添加 {$file_added} 个文件" . ($file_ignored > 0 ? ",忽略 {$file_ignored} 个文件" : ''));
return $phar_name;
} catch (\PharException $e) {
throw new PluginException("插件 {$plugin_name} 打包失败,原因为 Phar 异常:\n" . $e->getMessage(), previous: $e);
2022-12-19 01:45:27 +08:00
}
}
/**
* 检查插件是否被加载
*
* @param string $name 插件名称
*/
public static function isPluginExists(string $name): bool
{
return isset(self::$plugins[$name]);
}
}