主题
插件核心机制
本章深入介绍 CatchAdmin 插件系统的核心组件和工作机制。
系统架构概览
CatchAdmin 插件系统由以下核心组件构成:
┌─────────────────────────────────────────────────────────────────┐
│ Composer │
│ (包管理器核心) │
├──────────────────────────┬──────────────────────────────────────┤
│ PluginInstallerPlugin │ PluginHook │
│ (安装路径管理) │ (生命周期钩子) │
├──────────────────────────┴──────────────────────────────────────┤
│ InstalledPluginManager │
│ (插件记录管理) │
├─────────────────────────────────────────────────────────────────┤
│ Plugin │
│ (插件信息读取) │
├─────────────────────────────────────────────────────────────────┤
│ PluginServiceProvider │
│ (路由加载 & 命令注册) │
└─────────────────────────────────────────────────────────────────┘组件职责
| 组件 | 位置 | 职责 |
|---|---|---|
PluginInstallerPlugin | packages/plugin-installer/ | 注册自定义安装器 |
PluginInstaller | packages/plugin-installer/ | 确定插件安装路径 |
PluginHook | packages/plugin-hook/ | 监听 Composer 事件,执行钩子 |
InstalledPluginManager | packages/plugin/ | 管理 plugins.json 文件 |
Plugin | packages/plugin/ | 提供插件信息查询接口 |
PluginServiceProvider | packages/plugin/ | 加载路由,注册命令 |
PluginInstaller
作用
PluginInstaller 是一个 Composer 自定义安装器,负责将 catchadmin-plugin 类型的包安装到指定目录。
核心逻辑
PluginInstallerPlugin.php - 注册安装器:
php
class PluginInstallerPlugin implements PluginInterface
{
public function activate(Composer $composer, IOInterface $io): void
{
$installer = new PluginInstaller($io, $composer);
$composer->getInstallationManager()->addInstaller($installer);
}
}PluginInstaller.php - 确定安装路径:
php
class PluginInstaller extends LibraryInstaller
{
public function supports(string $packageType): bool
{
return isset($this->typePathMap[$packageType]);
}
public function getInstallPath(PackageInterface $package): string
{
$basePath = $this->typePathMap[$package->getType()];
$name = $package->getPrettyName(); // vendor/package
return $basePath . '/' . $name;
}
}PluginHook - 生命周期钩子
作用
PluginHook 监听 Composer 事件,在适当时机执行插件定义的钩子方法,并更新插件记录。
订阅的 Composer 事件
php
public static function getSubscribedEvents(): array
{
return [
PackageEvents::PRE_PACKAGE_INSTALL => 'onPrePackageInstall',
PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
PackageEvents::PRE_PACKAGE_UNINSTALL => 'onPrePackageUninstall',
PackageEvents::POST_PACKAGE_UNINSTALL => 'onPostPackageUninstall',
PackageEvents::PRE_PACKAGE_UPDATE => 'onPrePackageUpdate',
PackageEvents::POST_PACKAGE_UPDATE => 'onPostPackageUpdate',
ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump',
];
}事件与钩子对应关系
| Composer 事件 | 触发时机 | 执行的钩子 |
|---|---|---|
PRE_PACKAGE_INSTALL | 安装包之前 | beforeInstall |
POST_PACKAGE_INSTALL | 安装包之后 | (记录待处理) |
POST_AUTOLOAD_DUMP | autoload 生成后 | afterInstall |
PRE_PACKAGE_UNINSTALL | 卸载包之前 | beforeUninstall |
POST_PACKAGE_UNINSTALL | 卸载包之后 | (记录待处理) |
POST_AUTOLOAD_DUMP | autoload 生成后 | afterUninstall |
PRE_PACKAGE_UPDATE | 更新包之前 | beforeUpdate |
POST_PACKAGE_UPDATE | 更新包之后 | (记录待处理) |
POST_AUTOLOAD_DUMP | autoload 生成后 | afterUpdate |
钩子方法签名
钩子方法是实例方法(非静态方法):
php
// hook.php
class Hook
{
// 安装钩子
public function beforeInstall(array $context): void;
public function afterInstall(array $context): void;
// 更新钩子
public function beforeUpdate(array $context): void;
public function afterUpdate(array $context): void;
// 卸载钩子
public function beforeUninstall(array $context): void;
public function afterUninstall(array $context): void;
}$context 参数内容
php
[
'name' => 'vendor/package', // 包名
'version' => '1.0.0', // 版本号
'path' => 'packages/vendor/package', // 安装路径
'namespace' => 'Vendor\Package', // 根命名空间
]钩子类加载机制
插件钩子采用约定方式加载:
- 文件名:
hook.php(小写) - 类名:
Hook(首字母大写) - 位置:插件根目录
php
// packages/vendor/package/hook.php
<?php
class Hook
{
public function afterInstall(array $context): void
{
// 安装后的逻辑
}
public function afterUninstall(array $context): void
{
// 卸载后的逻辑
}
}PluginHook 会在执行钩子时自动加载:
php
protected function invokeHookMethod(string $packagePath, string $method, array $context): void
{
$hookFile = $packagePath . '/hook.php';
if (! file_exists($hookFile)) {
return;
}
if (! class_exists('Hook', false)) {
require_once $hookFile;
}
if (class_exists('Hook', false)) {
$instance = new \Hook();
if (method_exists($instance, $method)) {
$instance->$method($context);
}
}
}📌 约定说明
- 钩子类不需要命名空间,因为它不通过 PSR-4 加载
- 如需使用插件的其他类,在
after*钩子中调用(此时 autoload 已生成)
InstalledPluginManager - 插件记录管理
作用
InstalledPluginManager 负责管理 storage/packages/plugins.json 文件,提供已安装插件信息的增删改查接口。
核心方法
php
class InstalledPluginManager
{
protected string $storagePath;
public function __construct(?string $storagePath = null)
{
if ($storagePath) {
$this->storagePath = $storagePath;
} elseif (function_exists('config')) {
$this->storagePath = config('plugin.installed_file');
} else {
// Composer 环境下使用默认路径
$this->storagePath = getcwd() . '/storage/packages/plugins.json';
}
}
// 获取所有已安装插件
public function getAll(): array;
// 获取单个插件信息
public function get(string $name): ?array;
// 添加插件记录
public function add(array $data): bool;
// 更新插件记录
public function update(string $name, array $data): bool;
// 删除插件记录
public function remove(string $name): bool;
}环境兼容性
InstalledPluginManager 同时支持 Laravel 环境和 Composer 环境:
php
// Laravel 环境:使用 config() 函数
if (function_exists('config')) {
$this->storagePath = config('plugin.installed_file');
}
// Composer 环境:使用 getcwd()
else {
$this->storagePath = getcwd() . '/storage/packages/plugins.json';
}PluginServiceProvider - 路由与命令加载
作用
PluginServiceProvider 是 Laravel 服务提供者,负责:
- 注册插件配置
- 加载插件路由
- 注册 Artisan 命令
路由加载逻辑
php
public function boot(): void
{
// 如果路由已缓存,跳过动态加载
if ($this->app->routesAreCached()) {
return;
}
// 加载所有插件路由
foreach (Plugin::allRoutes() as $routeFile) {
if (file_exists($routeFile)) {
require $routeFile;
}
}
}路由发现机制
Plugin::allRoutes() 方法从已安装插件中收集所有路由文件:
php
public static function allRoutes(): array
{
$routes = [];
foreach (self::all() as $plugin) {
$routesDir = base_path($plugin['path'] . '/routes');
if (is_dir($routesDir)) {
$files = glob($routesDir . '/*.php');
$routes = array_merge($routes, $files);
}
}
return $routes;
}注册的 Artisan 命令
php
protected $commands = [
PluginOptimizeCommand::class, // plugin:optimize
PluginClearCommand::class, // plugin:clear
PluginPackCommand::class, // catch:plugin-pack
];安装/卸载完整流程图
安装流程
composer require vendor/plugin
│
▼
┌───────────────────────────────────┐
│ 1. Composer 解析依赖 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 2. PluginInstaller.supports() │
│ 检查是否为 catchadmin-plugin │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 3. PluginInstaller.getInstallPath()│
│ 确定安装路径 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 4. PRE_PACKAGE_INSTALL 事件 │
│ → PluginHook.onPrePackageInstall│
│ → 执行 beforeInstall 钩子 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 5. Composer 下载/链接包 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 6. POST_PACKAGE_INSTALL 事件 │
│ → 记录待处理的安装信息 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 7. Composer 生成 autoload │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 8. POST_AUTOLOAD_DUMP 事件 │
│ → InstalledPluginManager.add() │
│ → 写入 plugins.json │
│ → 执行 afterInstall 钩子 │
└───────────────────────────────────┘
│
▼
安装完成卸载流程
composer remove vendor/plugin
│
▼
┌───────────────────────────────────┐
│ 1. Composer 解析依赖变更 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 2. PRE_PACKAGE_UNINSTALL 事件 │
│ → 执行 beforeUninstall 钩子 │
│ → 记录待处理的卸载信息 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 3. Composer 删除包 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 4. POST_PACKAGE_UNINSTALL 事件 │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 5. Composer 重新生成 autoload │
└───────────────────────────────────┘
│
▼
┌───────────────────────────────────┐
│ 6. POST_AUTOLOAD_DUMP 事件 │
│ → 执行 afterUninstall 钩子 │
│ → InstalledPluginManager.remove()│
│ → 从 plugins.json 删除记录 │
└───────────────────────────────────┘
│
▼
卸载完成
