'', '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'] ?? (''); // 设置版本(如果有) 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'] = ''; // 到这里,说明没有 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; } }