mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-18 13:14:52 +08:00
218 lines
8.6 KiB
PHP
218 lines
8.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ZM\Plugin;
|
||
|
||
use ZM\Annotation\AnnotationMap;
|
||
use ZM\Annotation\AnnotationParser;
|
||
use ZM\Annotation\Framework\BindEvent;
|
||
use ZM\Annotation\OneBot\BotEvent;
|
||
use ZM\Exception\PluginException;
|
||
use ZM\Store\FileSystem;
|
||
|
||
class PluginManager
|
||
{
|
||
private const DEFAULT_META = [
|
||
'name' => '<anonymous>',
|
||
'version' => 'dev',
|
||
'dir' => '',
|
||
'object' => null,
|
||
'entry_file' => null,
|
||
'autoload' => null,
|
||
'dependencies' => [],
|
||
];
|
||
|
||
/** @var array|string[] 缺省的自动加载插件的入口文件 */
|
||
public static array $default_entries = [
|
||
'main.php',
|
||
'entry.php',
|
||
'index.php',
|
||
];
|
||
|
||
/** @var array 插件信息列表 */
|
||
private static array $plugins = [];
|
||
|
||
/**
|
||
* 传入插件父目录,扫描插件目录下的所有插件并注册添加
|
||
*
|
||
* @param string $dir 插件目录
|
||
* @return int 返回添加插件的数量
|
||
* @throws PluginException
|
||
*/
|
||
public static function addPluginsFromDir(string $dir): int
|
||
{
|
||
// 遍历插件目录
|
||
if (!is_dir($dir)) {
|
||
return 0;
|
||
}
|
||
$list = FileSystem::scanDirFiles($dir, false, false, true);
|
||
$cnt = 0;
|
||
foreach ($list as $item) {
|
||
// 必须是目录形式的插件
|
||
if (!is_dir($item)) {
|
||
continue;
|
||
}
|
||
$plugin_meta = self::DEFAULT_META;
|
||
$plugin_meta['dir'] = $item;
|
||
|
||
// 看看有没有插件信息文件
|
||
$info_file = $item . '/zmplugin.json';
|
||
$main_file = '';
|
||
// 如果有的话,就从插件信息文件中找到插件信息
|
||
if (is_file($info_file)) {
|
||
$info = json_decode(file_get_contents($info_file), true);
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
logger()->error('插件信息文件解析失败: ' . json_last_error_msg());
|
||
continue;
|
||
}
|
||
// 设置名称(如果有)
|
||
$plugin_meta['name'] = $info['name'] ?? ('<anonymous:' . pathinfo($item, PATHINFO_BASENAME) . '>');
|
||
// 设置版本(如果有)
|
||
if (isset($info['version'])) {
|
||
$plugin_meta['version'] = $info['version'];
|
||
}
|
||
// 设置了入口文件,则遵循这个入口文件
|
||
if (isset($info['main'])) {
|
||
$main_file = FileSystem::isRelativePath($info['main']) ? ($item . '/' . $info['main']) : $info['main'];
|
||
} else {
|
||
$main_file = self::matchDefaultEntry($item);
|
||
}
|
||
|
||
// 检查有没有 composer.json 和 vendor/autoload.php 自动加载,如果有的话,那就写上去
|
||
$composer_file = $item . '/composer.json';
|
||
if (is_file(zm_dir($composer_file))) {
|
||
// composer.json 存在,那么就加载这个插件
|
||
$composer = json_decode(file_get_contents($composer_file), true);
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
logger()->error('插件 composer.json 文件解析失败: ' . json_last_error_msg());
|
||
continue;
|
||
}
|
||
if (isset($composer['autoload']['psr-4']) && is_assoc_array($composer['autoload']['psr-4'])) {
|
||
$plugin_meta['autoload'] = $composer['autoload']['psr-4'];
|
||
}
|
||
}
|
||
|
||
// 主文件存在,则加载
|
||
if (is_file(zm_dir($main_file))) {
|
||
// 如果入口文件存在,那么就加载这个插件
|
||
$plugin_meta['entry_file'] = $main_file;
|
||
}
|
||
|
||
// composer.json 不存在,那么就忽略这个插件,并报 warning
|
||
if (!is_file(zm_dir($composer_file)) && $plugin_meta['entry_file'] === null) {
|
||
logger()->warning('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
|
||
continue;
|
||
}
|
||
} else {
|
||
$plugin_meta['name'] = '<unnamed:' . pathinfo($item, PATHINFO_BASENAME) . '>';
|
||
// 到这里,说明没有 zmplugin.json 这个文件,那么我们就直接匹配
|
||
$main_file = self::matchDefaultEntry($item);
|
||
if (is_file(zm_dir($main_file))) {
|
||
// 如果入口文件存在,那么就加载这个插件
|
||
$plugin_meta['entry_file'] = $main_file;
|
||
} else {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 到这里,说明插件信息收集齐了,只需要加载就行了
|
||
self::addPlugin($plugin_meta);
|
||
++$cnt;
|
||
}
|
||
return $cnt;
|
||
}
|
||
|
||
/**
|
||
* 添加插件到全局注册中
|
||
*
|
||
* @throws PluginException
|
||
*/
|
||
public static function addPlugin(array $meta = []): void
|
||
{
|
||
if (!isset($meta['name'])) {
|
||
throw new PluginException('Plugin must have a name!');
|
||
}
|
||
logger()->debug('Adding plugin: ' . $meta['name']);
|
||
|
||
self::$plugins[$meta['name']] = $meta;
|
||
|
||
// 存在直接声明的对象,那么直接初始化
|
||
if (isset($meta['object']) && $meta['object'] instanceof ZMPlugin) {
|
||
return;
|
||
}
|
||
|
||
// 存在入口文件(单文件),从单文件加载
|
||
if (isset($meta['entry_file']) && is_file(zm_dir($meta['entry_file']))) {
|
||
$zmplugin = self::$plugins[$meta['name']]['object'] = require $meta['entry_file'];
|
||
if (!$zmplugin instanceof ZMPlugin) {
|
||
unset(self::$plugins[$meta['name']]);
|
||
throw new PluginException('插件 ' . $meta['name'] . ' 的入口文件 ' . $meta['entry_file'] . ' 必须返回一个 ZMPlugin 对象');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 存在自动加载,检测 vendor/autoload.php 是否存在,如果存在,那么就加载
|
||
if (isset($meta['autoload'], $meta['dir']) && $meta['dir'] !== '' && is_file($meta['dir'] . '/vendor/autoload.php')) {
|
||
require_once $meta['dir'] . '/vendor/autoload.php';
|
||
return;
|
||
}
|
||
// 如果都不存在,那是不可能的事情,抛出一个谁都没见过的异常
|
||
unset(self::$plugins[$meta['name']]);
|
||
throw new PluginException('插件 ' . $meta['name'] . ' 无法加载,因为没有入口文件,也没有自动加载文件和内建 Composer');
|
||
}
|
||
|
||
/**
|
||
* 启用所有插件
|
||
*
|
||
* @param AnnotationParser $parser 传入注解解析器,用于将插件中的事件注解解析出来
|
||
*/
|
||
public static function enablePlugins(AnnotationParser $parser): void
|
||
{
|
||
foreach (self::$plugins as $name => $plugin) {
|
||
if (!isset($plugin['internal'])) {
|
||
logger()->info('Enabling plugin: ' . $name);
|
||
}
|
||
if (isset($plugin['object']) && $plugin['object'] instanceof ZMPlugin) {
|
||
$obj = $plugin['object'];
|
||
// 将 Event 加入事件监听
|
||
foreach ($obj->getEvents() as $event) {
|
||
$bind = new BindEvent($event[0], $event[2]);
|
||
$bind->on($event[1]);
|
||
AnnotationMap::$_list[BindEvent::class][] = $bind;
|
||
}
|
||
// 将 Routes 加入事件监听
|
||
foreach ($obj->getRoutes() as $route) {
|
||
$parser->parseSpecial($route);
|
||
}
|
||
// 将 BotEvents 加入事件监听
|
||
foreach ($obj->getBotEvents() as $event) {
|
||
AnnotationMap::$_list[BotEvent::class][] = $event;
|
||
}
|
||
// 将 BotCommand 加入事件监听
|
||
foreach ($obj->getBotCommands() as $cmd) {
|
||
$parser->parseSpecial($cmd);
|
||
}
|
||
} elseif (isset($plugin['autoload'], $plugin['dir'])) {
|
||
foreach ($plugin['autoload'] as $k => $v) {
|
||
$parser->addRegisterPath($plugin['dir'] . '/' . $v . '/', trim($k, '\\'));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static function matchDefaultEntry(string $dir): string
|
||
{
|
||
$main = '';
|
||
// 没有设置入口文件,则遍历默认入口文件列表
|
||
foreach (self::$default_entries as $entry) {
|
||
$main_file = $dir . '/' . $entry;
|
||
if (is_file(zm_dir($main_file))) {
|
||
$main = $main_file;
|
||
break;
|
||
}
|
||
}
|
||
return $main;
|
||
}
|
||
}
|