Skip to content
CatchAdmin 插件市场也正式上线啦!!! GO ! 还有 CatchAdmin 正在参加 Gitee 2025 最受欢迎的开源软件投票活动 ⭐请给我投一票吧!

插件核心机制

本章深入介绍 CatchAdmin 插件系统的核心组件和工作机制。

系统架构概览

CatchAdmin 插件系统由以下核心组件构成:

┌─────────────────────────────────────────────────────────────────┐
│                         Composer                                 │
│                    (包管理器核心)                                 │
├──────────────────────────┬──────────────────────────────────────┤
│   PluginInstallerPlugin  │          PluginHook                  │
│   (安装路径管理)          │          (生命周期钩子)               │
├──────────────────────────┴──────────────────────────────────────┤
│                    InstalledPluginManager                        │
│                    (插件记录管理)                                 │
├─────────────────────────────────────────────────────────────────┤
│                      Plugin                                      │
│                    (插件信息读取)                                 │
├─────────────────────────────────────────────────────────────────┤
│                    PluginServiceProvider                         │
│                    (路由加载 & 命令注册)                          │
└─────────────────────────────────────────────────────────────────┘

组件职责

组件位置职责
PluginInstallerPluginpackages/plugin-installer/注册自定义安装器
PluginInstallerpackages/plugin-installer/确定插件安装路径
PluginHookpackages/plugin-hook/监听 Composer 事件,执行钩子
InstalledPluginManagerpackages/plugin/管理 plugins.json 文件
Pluginpackages/plugin/提供插件信息查询接口
PluginServiceProviderpackages/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_DUMPautoload 生成后afterInstall
PRE_PACKAGE_UNINSTALL卸载包之前beforeUninstall
POST_PACKAGE_UNINSTALL卸载包之后(记录待处理)
POST_AUTOLOAD_DUMPautoload 生成后afterUninstall
PRE_PACKAGE_UPDATE更新包之前beforeUpdate
POST_PACKAGE_UPDATE更新包之后(记录待处理)
POST_AUTOLOAD_DUMPautoload 生成后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 删除记录     │
└───────────────────────────────────┘


     卸载完成