add plugin loader support

This commit is contained in:
crazywhalecc 2022-12-19 01:45:27 +08:00
parent 52a195aca2
commit cd2bb1b570
18 changed files with 373 additions and 96 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
/tmp/ /tmp/
/temp/ /temp/
/site/ /site/
/plugins/
# 框架审计文件 # 框架审计文件
audit.log audit.log

View File

@ -71,7 +71,8 @@ $config['plugin'] = [
/* 内部默认启用的插件 */ /* 内部默认启用的插件 */
$config['native_plugin'] = [ $config['native_plugin'] = [
'onebot12' => true, 'onebot12' => true, // OneBot v12 协议支持
'onebot12-ban-other-ws' => true, // OneBot v12 协议支持,禁止其他 WebSocket 连接
]; ];
/* 静态文件读取器 */ /* 静态文件读取器 */

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
$plugin = new ZMPlugin(__DIR__); $plugin = new \ZM\ZMApplication(__DIR__);
/* /*
* 发送 "测试 123",回复 "你好123" * 发送 "测试 123",回复 "你好123"
@ -17,8 +17,4 @@ $route1 = Route::make('/index233')->on(fn () => '<h1>Hello world</h1>');
$plugin->addBotCommand($cmd1); $plugin->addBotCommand($cmd1);
$plugin->addHttpRoute($route1); $plugin->addHttpRoute($route1);
return [ $plugin->run();
'plugin-name' => 'pasd',
'version' => '1.0.0',
'plugin' => $plugin,
];

View File

@ -24,10 +24,10 @@ $app->enablePlugins([
'd', 'd',
]); ]);
// BotCommand 事件构造 // BotCommand 事件构造
$cmd = \ZM\Annotation\OneBot\BotCommand::make('test')->withMethod(function () { $cmd = BotCommand::make('test')->on(function () {
ctx()->reply('test ok'); ctx()->reply('test ok');
}); });
$event = \ZM\Annotation\OneBot\BotEvent::make('message')->withMethod(function () { $event = BotEvent::make(type: 'message')->on(function () {
}); });
$app->addBotEvent($event); $app->addBotEvent($event);
$app->addBotCommand($cmd); $app->addBotCommand($cmd);

View File

@ -13,3 +13,9 @@ class_alias(\ZM\Annotation\OneBot\BotEvent::class, 'BotEvent');
class_alias(\ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument'); class_alias(\ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument');
class_alias(\ZM\Annotation\Closed::class, 'Closed'); class_alias(\ZM\Annotation\Closed::class, 'Closed');
class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin'); class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
// 下面是 OneBot 相关类的全局别称
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketCloseEvent::class, 'WebSocketCloseEvent');
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketMessageEvent::class, 'WebSocketMessageEvent');
class_alias(\OneBot\Driver\Event\Http\HttpRequestEvent::class, 'HttpRequestEvent');

View File

@ -4,19 +4,12 @@ declare(strict_types=1);
namespace Module\Example; namespace Module\Example;
use ZM\Annotation\Framework\Setup;
use ZM\Annotation\Http\Route; use ZM\Annotation\Http\Route;
use ZM\Annotation\Middleware\Middleware; use ZM\Annotation\Middleware\Middleware;
use ZM\Middleware\TimerMiddleware; use ZM\Middleware\TimerMiddleware;
class Hello123 class Hello123
{ {
#[Setup]
public function onRequest()
{
echo "OK\n";
}
#[Route('/route', request_method: ['GET'])] #[Route('/route', request_method: ['GET'])]
#[Middleware(TimerMiddleware::class)] #[Middleware(TimerMiddleware::class)]
public function route() public function route()

View File

@ -6,11 +6,9 @@ namespace ZM\Annotation;
abstract class AnnotationBase implements \IteratorAggregate abstract class AnnotationBase implements \IteratorAggregate
{ {
public string $method = ''; /** @var array|\Closure|string 方法名或闭包 */
public \Closure|string|array $method = '';
/**
* @var \Closure|string
*/
public $class = ''; public $class = '';
public array $group = []; public array $group = [];

View File

@ -127,14 +127,18 @@ class AnnotationHandler
// 由于3.0有额外的插件模式支持,所以注解就不再提供独立的闭包函数调用支持了 // 由于3.0有额外的插件模式支持,所以注解就不再提供独立的闭包函数调用支持了
// 提取要调用的目标类和方法名称 // 提取要调用的目标类和方法名称
$class = $v->class; $class = $v->class;
$target_class = new $class();
$target_method = $v->method; $target_method = $v->method;
if ($class !== '') {
$target_class = new $class();
$callback = [$target_class, $target_method];
} else {
$callback = $target_method;
}
// 先执行规则失败就返回false // 先执行规则失败就返回false
if ($rule_callback !== null && !$rule_callback($v)) { if ($rule_callback !== null && !$rule_callback($v)) {
$this->status = self::STATUS_RULE_FAILED; $this->status = self::STATUS_RULE_FAILED;
return false; return false;
} }
$callback = [$target_class, $target_method];
try { try {
$this->return_val = middleware()->process($callback, ...$args); $this->return_val = middleware()->process($callback, ...$args);
} catch (InterruptException $e) { } catch (InterruptException $e) {

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ZM\Annotation; namespace ZM\Annotation;
use ZM\Annotation\Interfaces\Level;
/** /**
* 注解全局存取位置 * 注解全局存取位置
*/ */
@ -36,4 +38,20 @@ class AnnotationMap
self::$_list = array_merge(self::$_list, $parser->generateAnnotationList()); self::$_list = array_merge(self::$_list, $parser->generateAnnotationList());
self::$_map = $parser->getAnnotationMap(); self::$_map = $parser->getAnnotationMap();
} }
/**
* 排序所有的注解
*/
public static function sortAnnotationList(): void
{
foreach (self::$_list as $class => $annotations) {
if (is_a($class, Level::class, true)) {
usort(self::$_list[$class], function ($a, $b) {
$left = $a->getLevel(); /** @phpstan-ignore-line */
$right = $b->getLevel(); /* @phpstan-ignore-line */
return $left > $right ? -1 : ($left == $right ? 0 : 1);
});
}
}
}
} }

View File

@ -10,7 +10,6 @@ use Koriym\Attributes\DualReader;
use ZM\Annotation\Http\Controller; use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\Route; use ZM\Annotation\Http\Route;
use ZM\Annotation\Interfaces\ErgodicAnnotation; use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Middleware\Middleware; use ZM\Annotation\Middleware\Middleware;
use ZM\Store\FileSystem; use ZM\Store\FileSystem;
use ZM\Utils\HttpUtil; use ZM\Utils\HttpUtil;
@ -67,7 +66,7 @@ class AnnotationParser
* @param string $class_name 注解类名 * @param string $class_name 注解类名
* @param callable $callback 回调函数 * @param callable $callback 回调函数
*/ */
public function addSpecialParser(string $class_name, callable $callback) public function addSpecialParser(string $class_name, callable $callback): void
{ {
$this->special_parsers[$class_name][] = $callback; $this->special_parsers[$class_name][] = $callback;
} }
@ -160,14 +159,11 @@ class AnnotationParser
} }
// 预处理3调用自定义解析器 // 预处理3调用自定义解析器
foreach (($this->special_parsers[get_class($vs)] ?? []) as $parser) { if (($a = $this->parseSpecial($vs)) === true) {
$result = $parser($vs); continue;
if ($result === true) { }
continue 2; if ($a === false) {
} continue 2;
if ($result === false) {
continue 3;
}
} }
} }
@ -191,14 +187,11 @@ class AnnotationParser
} }
// 预处理3.3:调用自定义解析器 // 预处理3.3:调用自定义解析器
foreach (($this->special_parsers[get_class($method_anno)] ?? []) as $parser) { if (($a = $this->parseSpecial($method_anno, $methods_annotations)) === true) {
$result = $parser($method_anno); continue;
if ($result === true) { }
continue 2; if ($a === false) {
} continue 2;
if ($result === false) {
continue 3;
}
} }
// 如果上方没有解析或返回了 true则添加到注解解析列表中 // 如果上方没有解析或返回了 true则添加到注解解析列表中
@ -240,12 +233,20 @@ class AnnotationParser
} }
} }
} }
foreach ($o as $k => $v) {
$this->sortByLevel($o, $k);
}
return $o; return $o;
} }
public function parseSpecial($annotation, $same_method_annotations = null): ?bool
{
foreach (($this->special_parsers[get_class($annotation)] ?? []) as $parser) {
$result = $parser($annotation, $same_method_annotations);
if (is_bool($result)) {
return $result;
}
}
return null;
}
/** /**
* 添加解析的路径 * 添加解析的路径
* *
@ -258,26 +259,6 @@ class AnnotationParser
$this->path_list[] = [$path, $indoor_name]; $this->path_list[] = [$path, $indoor_name];
} }
/**
* 排序注解列表
*
* @param array $events 需要排序的
* @param string $class_name 排序的类名
* @param string $prefix 前缀
* @internal 用于 level 排序
*/
public function sortByLevel(array &$events, string $class_name, string $prefix = '')
{
if (is_a($class_name, Level::class, true)) {
$class_name .= $prefix;
usort($events[$class_name], function ($a, $b) {
$left = $a->getLevel();
$right = $b->getLevel();
return $left > $right ? -1 : ($left == $right ? 0 : 1);
});
}
}
/** /**
* 获取解析器调用的时间(秒) * 获取解析器调用的时间(秒)
*/ */
@ -297,14 +278,16 @@ class AnnotationParser
/** /**
* 添加注解路由 * 添加注解路由
*/ */
private function addRouteAnnotation(Route $vss): void private function addRouteAnnotation(Route $vss, ?array $same_method_annotations = null)
{ {
// 拿到所属方法的类上面有没有控制器的注解 // 拿到所属方法的类上面有没有控制器的注解
$prefix = ''; $prefix = '';
foreach (($this->annotation_tree[$vss->class]['methods_annotations'][$vss->method] ?? []) as $annotation) { if ($same_method_annotations !== null) {
if ($annotation instanceof Controller) { foreach ($same_method_annotations as $annotation) {
$prefix = $annotation->prefix; if ($annotation instanceof Controller) {
break; $prefix = $annotation->prefix;
break;
}
} }
} }
$tail = trim($vss->route, '/'); $tail = trim($vss->route, '/');
@ -314,5 +297,6 @@ class AnnotationParser
$route->setMethods($vss->request_method); $route->setMethods($vss->request_method);
HttpUtil::getRouteCollection()->add(md5($route_name), $route); HttpUtil::getRouteCollection()->add(md5($route_name), $route);
return null;
} }
} }

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ZM\Annotation\OneBot; namespace ZM\Annotation\OneBot;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target; use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase; use ZM\Annotation\AnnotationBase;
@ -114,6 +113,12 @@ class BotCommand extends AnnotationBase implements Level
return $this; return $this;
} }
public function withArgumentObject(CommandArgument $argument): BotCommand
{
$this->arguments[] = $argument;
return $this;
}
public function getLevel(): int public function getLevel(): int
{ {
return $this->level; return $this->level;

View File

@ -43,7 +43,7 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation
* @param string $name 参数名称(可以是中文) * @param string $name 参数名称(可以是中文)
* @param string $description 参数描述(默认为空) * @param string $description 参数描述(默认为空)
* @param bool $required 参数是否必需如果是必需为true默认为false * @param bool $required 参数是否必需如果是必需为true默认为false
* @param string $prompt 当参数为必需时,返回给用户的提示输入的消息(默认为"请输入$name" * @param string $prompt 当参数为必需且缺失时,返回给用户的提示输入的消息(默认为"请输入$name"
* @param string $default 当required为false时未匹配到参数将自动使用default值默认为空 * @param string $default 当required为false时未匹配到参数将自动使用default值默认为空
* @param int $timeout prompt超时时间默认为60秒 * @param int $timeout prompt超时时间默认为60秒
* @throws InvalidArgumentException|ZMKnownException * @throws InvalidArgumentException|ZMKnownException

View File

@ -40,11 +40,23 @@ class WSEventListener
public function onWebSocketMessage(WebSocketMessageEvent $event): void public function onWebSocketMessage(WebSocketMessageEvent $event): void
{ {
// 调用注解
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketMessageEvent::class, true));
$handler->handleAll($event);
} }
/**
* @throws \Throwable
*/
public function onWebSocketClose(WebSocketCloseEvent $event): void public function onWebSocketClose(WebSocketCloseEvent $event): void
{ {
logger()->info('关闭连接: ' . $event->getFd()); logger()->info('关闭连接: ' . $event->getFd());
// 调用注解
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketCloseEvent::class, true));
$handler->handleAll($event);
ConnectionUtil::removeConnection($event->getFd()); ConnectionUtil::removeConnection($event->getFd());
resolve(ContainerServicesProvider::class)->cleanup(); resolve(ContainerServicesProvider::class)->cleanup();
} }

View File

@ -18,6 +18,7 @@ use ZM\Plugin\PluginManager;
use ZM\Process\ProcessStateManager; use ZM\Process\ProcessStateManager;
use ZM\Store\Database\DBException; use ZM\Store\Database\DBException;
use ZM\Store\Database\DBPool; use ZM\Store\Database\DBPool;
use ZM\Store\FileSystem;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
class WorkerEventListener class WorkerEventListener
@ -29,7 +30,7 @@ class WorkerEventListener
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function onWorkerStart999() public function onWorkerStart999(): void
{ {
// 自注册一下刷新当前进程的logger进程banner // 自注册一下刷新当前进程的logger进程banner
ob_logger_register(ob_logger()); ob_logger_register(ob_logger());
@ -120,7 +121,7 @@ class WorkerEventListener
{ {
logger()->debug('Loading user sources'); logger()->debug('Loading user sources');
// 首先先加载 source 普通插件,相当于内部模块,不算插件的一种 // 首先先加载 source 模式的代码,相当于内部模块,不算插件的一种
$parser = new AnnotationParser(); $parser = new AnnotationParser();
$composer = ZMUtil::getComposerMetadata(); $composer = ZMUtil::getComposerMetadata();
// 合并 dev 和 非 dev 的 psr-4 加载目录 // 合并 dev 和 非 dev 的 psr-4 加载目录
@ -135,22 +136,49 @@ class WorkerEventListener
} }
} }
// TODO: 然后加载插件目录下的插件 // 首先加载内置插件
PluginManager::addPlugin([ $native_plugins = config('global.native_plugin');
'name' => 'onebot12-adapter', foreach ($native_plugins as $name => $enable) {
'plugin' => new OneBot12Adapter(), if (!$enable) {
]); continue;
}
match ($name) {
'onebot12' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(parser: $parser)]),
'onebot12-ban-other-ws' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(submodule: $name)]),
};
}
// 然后加载插件目录的插件
if (config('global.plugin.enable')) {
$load_dir = config('global.plugin.load_dir');
if (empty($load_dir)) {
$load_dir = SOURCE_ROOT_DIR . '/plugins';
} elseif (FileSystem::isRelativePath($load_dir)) {
$load_dir = SOURCE_ROOT_DIR . '/' . $load_dir;
}
$load_dir = zm_dir($load_dir);
$count = PluginManager::addPluginsFromDir($load_dir);
logger()->info('Loaded ' . $count . ' user plugins');
// 启用并初始化插件
PluginManager::enablePlugins($parser);
}
// 解析所有注册路径的文件,获取注解 // 解析所有注册路径的文件,获取注解
$parser->parseAll(); $parser->parseAll();
// 将Parser解析后的注解注册到全局的 AnnotationMap // 将Parser解析后的注解注册到全局的 AnnotationMap
AnnotationMap::loadAnnotationByParser($parser); AnnotationMap::loadAnnotationByParser($parser);
// 排序所有的
AnnotationMap::sortAnnotationList();
} }
/** /**
* 分发调用 Init 注解
*
* @throws \Throwable * @throws \Throwable
*/ */
private function dispatchInit() private function dispatchInit(): void
{ {
$handler = new AnnotationHandler(Init::class); $handler = new AnnotationHandler(Init::class);
$handler->setRuleCallback(function (Init $anno) { $handler->setRuleCallback(function (Init $anno) {
@ -166,7 +194,7 @@ class WorkerEventListener
* *
* @throws DBException * @throws DBException
*/ */
private function initConnectionPool() private function initConnectionPool(): void
{ {
// 清空 MySQL 的连接池 // 清空 MySQL 的连接池
foreach (DBPool::getAllPools() as $name => $pool) { foreach (DBPool::getAllPools() as $name => $pool) {

View File

@ -11,7 +11,7 @@ class ZMKnownException extends ZMException
{ {
public function __construct($err_code, $message = '', $code = 0, \Throwable $previous = null) public function __construct($err_code, $message = '', $code = 0, \Throwable $previous = null)
{ {
parent::__construct(zm_internal_errcode($err_code) . $message, $code, $previous); parent::__construct(zm_internal_errcode($err_code) . $message, '', $code, $previous);
if ($err_code === 'E99999') { if ($err_code === 'E99999') {
$code = 0; $code = 0;
// 这也太懒了吧 // 这也太懒了吧

View File

@ -5,19 +5,79 @@ declare(strict_types=1);
namespace ZM\Plugin; namespace ZM\Plugin;
use Choir\Http\HttpFactory; use Choir\Http\HttpFactory;
use OneBot\Driver\Event\StopException;
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent; use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\OneBot\BotCommand;
use ZM\Annotation\OneBot\CommandArgument;
use ZM\Utils\ConnectionUtil; use ZM\Utils\ConnectionUtil;
class OneBot12Adapter extends ZMPlugin class OneBot12Adapter extends ZMPlugin
{ {
public function __construct() public function __construct(string $submodule = '', ?AnnotationParser $parser = null)
{ {
parent::__construct(__DIR__); parent::__construct(__DIR__);
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseInput']); switch ($submodule) {
case '':
case 'onebot12':
// 处理所有 OneBot 12 的反向 WS 握手事件
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseInput']);
// 处理和声明所有 BotCommand 下的 CommandArgument
$parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']);
// 不需要给列表写入 CommandArgument
$parser->addSpecialParser(CommandArgument::class, [$this, 'parseCommandArgument']);
break;
case 'onebot12-ban-other-ws':
// 禁止其他类型的 WebSocket 客户端接入
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleUnknownWSReverseInput'], 1);
break;
}
}
/**
* BotCommand 假设含有 CommandArgument 的话,就注册到参数列表中
*
* @param BotCommand $command 命令对象
* @param null|array $same_method_annotations 同一个方法的所有注解
*/
public function parseBotCommand(BotCommand $command, ?array $same_method_annotations = null): ?bool
{
if ($same_method_annotations === null) {
return null;
}
foreach ($same_method_annotations as $v) {
if ($v instanceof CommandArgument) {
$command->withArgumentObject($v);
}
}
return null;
}
/**
* 忽略解析记录 CommandArgument 注解
*/
public function parseCommandArgument(): ?bool
{
return true;
}
/**
* @throws StopException
*/
public function handleUnknownWSReverseInput(WebSocketOpenEvent $event)
{
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
if ($line[0] !== '12') {
logger()->warning('不允许接入除 OneBot 12 以外的 WebSocket Client');
$event->withResponse(HttpFactory::createResponse(403, 'Forbidden'));
$event->stopPropagation();
}
} }
/** /**
* 接入和认证反向 WS 的连接 * 接入和认证反向 WS 的连接
* @throws StopException
*/ */
public function handleWSReverseInput(WebSocketOpenEvent $event): void public function handleWSReverseInput(WebSocketOpenEvent $event): void
{ {
@ -39,7 +99,7 @@ class OneBot12Adapter extends ZMPlugin
if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token鉴权失败 if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token鉴权失败
logger()->warning('OneBot 12 反向 WS 连接鉴权失败,拒绝接入'); logger()->warning('OneBot 12 反向 WS 连接鉴权失败,拒绝接入');
$event->withResponse(HttpFactory::createResponse(401, 'Unauthorized')); $event->withResponse(HttpFactory::createResponse(401, 'Unauthorized'));
return; $event->stopPropagation();
} }
} }
} }

View File

@ -4,37 +4,206 @@ declare(strict_types=1);
namespace ZM\Plugin; 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\Exception\PluginException;
use ZM\Store\FileSystem;
class PluginManager 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 插件信息列表 */ /** @var array 插件信息列表 */
private static array $plugins = []; private static array $plugins = [];
/** /**
* 传入插件父目录,扫描插件目录下的所有插件并注册添加
*
* @param string $dir 插件目录
* @return int 返回添加插件的数量
* @throws PluginException
*/
public static function addPluginsFromDir(string $dir): int
{
// 遍历插件目录
$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 * @throws PluginException
*/ */
public static function addPlugin(array $meta = []): void public static function addPlugin(array $meta = []): void
{ {
// 首先检测 meta 是否存在 plugin 对象 if (!isset($meta['name'])) {
if (isset($meta['plugin'])) { throw new PluginException('Plugin must have a name!');
// 存在的话,说明是单例插件,调用对象内的方法注册事件就行了 }
$meta['type'] = 'instant'; logger()->debug('Adding plugin: ' . $meta['name']);
self::$plugins[$meta['name']] = $meta;
self::$plugins[$meta['name']] = $meta;
// 存在直接声明的对象,那么直接初始化
if (isset($meta['object']) && $meta['object'] instanceof ZMPlugin) {
return; return;
} }
if (isset($meta['dir'])) {
// 不存在的话,说明是多文件插件,是设置了 zmplugin.json 的目录,此目录为自动加载的 // 存在入口文件(单文件),从单文件加载
$meta['type'] = 'dir'; if (isset($meta['entry_file']) && is_file(zm_dir($meta['entry_file']))) {
self::$plugins[$meta['name']] = $meta; $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; return;
} }
// 两者都不存在的话,说明是错误的插件
throw new PluginException('plugin meta must have plugin or dir'); // 存在自动加载,检测 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');
} }
public static function getPlugins(): array public static function enablePlugins(AnnotationParser $parser): void
{ {
return self::$plugins; 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;
} }
} }

View File

@ -6,6 +6,7 @@ namespace ZM;
use ZM\Command\Server\ServerStartCommand; use ZM\Command\Server\ServerStartCommand;
use ZM\Exception\SingletonViolationException; use ZM\Exception\SingletonViolationException;
use ZM\Plugin\PluginManager;
use ZM\Plugin\ZMPlugin; use ZM\Plugin\ZMPlugin;
class ZMApplication extends ZMPlugin class ZMApplication extends ZMPlugin
@ -43,6 +44,7 @@ class ZMApplication extends ZMPlugin
*/ */
public function run() public function run()
{ {
PluginManager::addPlugin(['name' => 'native-app', 'object' => $this]);
(new Framework($this->args))->init()->start(); (new Framework($this->args))->init()->start();
} }
} }