메뉴

InnoCMS 插件系统详解:从 Hook 到完整工程化封装

InnoCMS 2026-06-10 246
InnoCMS 插件系统详解:从 Hook 到完整工程化封装
Hook 是底层抽象,但插件是更大概念:元信息、入口类、路由、视图、迁移、设置、生命周期管理。本文把插件作为工程实体全维度讲清楚。

Hook 是 InnoCMS 插件的底层抽象(参见 《Hook 系统详解》),但「插件」本身是个更大的概念:一个完整的工程化封装,包含元信息、入口类、路由、视图、迁移、设置、生命周期管理。这篇文章把"插件"作为工程实体的全部维度讲清楚,给想自己写插件或评估插件质量的人一张地图。

InnoCMS 插件从扫描到运行的生命周期

插件加载的五个阶段

当请求到达 InnoCMS 时,PluginManager 做的事情:

  1. 扫描:读取 plugins/ 目录下所有子目录,识别潜在插件
  2. 解析 config.json:每个子目录必须有 config.json,含 code/name/version/author。格式不对的目录被跳过
  3. 激活检查:查数据库 plugins 表,看这个 code 是否被启用。未启用的插件不进入下一步
  4. 实例化 Boot:require Boot.php(PSR-4 命名空间 Plugin\<Code>\Boot),new 一个实例
  5. 调用 init():执行 Boot::init(),里面是插件所有 hook 注册、路由加载、Blade 注入逻辑

整个加载过程在每个请求都跑一遍(生产环境开 opcache,PHP 类实例化成本可忽略)。所以插件代码必须是无状态的,不能依赖全局副作用。

插件的标准目录结构

plugins/YourPlugin/
├── config.json              # 必填:插件身份证
├── Boot.php                 # 必填:入口类
├── Controllers/             # 后台/前台控制器
├── Models/                  # 插件专属模型
├── Routes/
│   ├── panel.php            # 后台路由
│   └── front.php            # 前台路由
├── Views/                   # Blade 模板
├── Lang/                    # 翻译
├── Migrations/              # 数据库迁移
├── Static/                  # 静态资源(JS/CSS)
└── Settings/                # 设置字段定义(自动生成设置页)

config.json:插件元信息

{
  "code": "ArticleShare",
  "name": "文章分享按钮",
  "description": "在文章详情页底部添加社交分享按钮",
  "version": "1.2.0",
  "author": "InnoCMS Team",
  "requires": {
    "innocms": "^1.0"
  }
}

注意几个字段:

  • code 必须跟目录名完全一致(推荐 PascalCase)
  • version 用语义化版本号,升级时用得到
  • requires.innocms 声明兼容的 InnoCMS 版本范围,避免在新版本上跑挂

Boot.php:插件的入口

namespace Plugin\ArticleShare;

use Plugin\ArticleShare\Models\ShareLog;

class Boot
{
    public function init(): void
    {
        // 1. 注册后台侧栏菜单
        listen_hook_filter('component.sidebar.plugin.routes', function ($data) {
            $data[] = [
                'route' => 'article_share.index',
                'title' => '分享统计',
                'icon'  => 'bi-share',
            ];
            return $data;
        });

        // 2. 注入文章页的分享按钮
        listen_blade_insert('article.share', function () {
            return view('ArticleShare::buttons');
        });

        // 3. 过滤文章数据,附加 share_url
        listen_hook_filter('article.show.data', function ($data) {
            $data['share_url'] = urlencode($data['article']->url);
            return $data;
        });

        // 4. 监听分享点击事件
        listen_hook_action('article.share.clicked', function ($articleId) {
            ShareLog::create([
                'article_id' => $articleId,
                'ip'         => request()->ip(),
            ]);
        });
    }
}

四种 hook 各司其职:

  • Filter component.sidebar.plugin.routes —— 给后台加菜单项
  • Blade insert article.share —— 在主题模板的 hook 点显示按钮
  • Filter article.show.data —— 给渲染数据附加 share_url
  • Action article.share.clicked —— 记录分享点击的副作用

路由:插件自己的 URL

Routes/panel.php 自动挂载到 /panel/plugins/<code>/ 前缀下:

// plugins/ArticleShare/Routes/panel.php
use Illuminate\Support\Facades\Route;
use Plugin\ArticleShare\Controllers\ShareController;

Route::get('/', [ShareController::class, 'index'])
    ->name('article_share.index');

Route::delete('/{log}', [ShareController::class, 'destroy'])
    ->name('article_share.destroy');

访问 /panel/plugins/ArticleShare/ 就是 ShareController::index。front.php 同理挂载到 /<locale>/plugins/<code>/

视图命名空间

插件的 Blade 视图用 PluginCode::view-name 命名空间引用:

// 在某个 closure 里
return view('ArticleShare::buttons', [
    'url'   => $shareUrl,
    'title' => $articleTitle,
]);

对应文件 plugins/ArticleShare/Views/buttons.blade.php。命名空间 = config.json 的 code。

设置页:自动生成表单

InnoCMS 插件管理器和典型插件生态

插件需要可配置项(API Key、显示位置、自定义文案)时,不用自己写设置页 —— 把字段声明放进 Settings/fields.php,系统会自动生成完整的设置表单 + 数据持久化:

// plugins/ArticleShare/Settings/fields.php
return [
    [
        'name'    => 'enabled_platforms',
        'label'   => '启用的平台',
        'type'    => 'checkbox',
        'options' => [
            'wechat'    => '微信',
            'weibo'     => '微博',
            'twitter'   => 'Twitter',
            'facebook'  => 'Facebook',
        ],
        'default' => ['wechat', 'weibo'],
    ],
    [
        'name'    => 'button_style',
        'label'   => '按钮样式',
        'type'    => 'select',
        'options' => ['square' => '方形', 'round' => '圆形'],
        'default' => 'round',
    ],
];

代码里用 plugin_setting('ArticleShare', 'enabled_platforms') 读值。后台「插件管理 → ArticleShare → 设置」自动展示这个表单。

迁移:插件需要自己的表时

// plugins/ArticleShare/Migrations/2024_06_19_000001_create_share_logs_table.php
return new class extends Migration {
    public function up(): void {
        Schema::create('plugin_article_share_logs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('article_id')->constrained()->cascadeOnDelete();
            $table->string('platform', 20);
            $table->string('ip', 45)->nullable();
            $table->timestamps();
        });
    }
    public function down(): void {
        Schema::dropIfExists('plugin_article_share_logs');
    }
};

启用插件时自动 migrate,卸载时自动 migrate:rollback。表名约定 plugin_<code_snake>_<entity>,避免跟内核表冲突。

生命周期:启用、禁用、卸载

插件有三种状态:

  • 已安装未启用:目录在 plugins/,但 pluginsactive=0Boot::init() 不执行
  • 启用active=1Boot::init() 执行,迁移已跑
  • 已卸载:目录被删除,plugins 表记录被清,migrate:rollback 跑过,所有数据表清理

插件可以监听自己的生命周期事件做清理(删缓存、清日志、通知外部服务):

listen_hook_action('plugin.installed.ArticleShare', function () {
    Cache::forget("article_share:*");
});

listen_hook_action('plugin.uninstalled.ArticleShare', function () {
    // 卸载前清理外部资源
});

一个完整的插件案例:PartnerLink

看一个真实生产插件 plugins/PartnerLink(友情链接管理)的代码量分布:

  • config.json —— 8 行
  • Boot.php —— 60 行(4 个 hook 注册)
  • Controllers/PartnerLinkController.php —— 150 行(CRUD)
  • Models/PartnerLink.php —— 25 行
  • Migrations/ —— 1 个文件,30 行
  • Views/ —— 5 个 Blade 模板,共 280 行
  • Routes/panel.php —— 8 行
  • Lang/zh-cn/partner_link.php —— 12 行

总计约 600 行代码就完成了一个完整的友情链接管理插件 —— 后台 CRUD、前台展示、多语言、自动迁移、卸载清理。这就是 InnoCMS 插件系统的工程化收益。

插件开发的避坑清单

  • 命名空间必须正确Plugin\<Code>\...,跟目录名 / config.json code 一致。不一致 PSR-4 加载不到
  • Boot::init() 必须可重入:每次请求都会执行,不要在里面做"只该执行一次"的副作用
  • 不要直接读 other 插件目录:用 PluginManager API,避免依赖加载顺序
  • 迁移文件名必须含时间戳:跟 Laravel 主迁移命名一致,否则 migration order 错乱
  • 视图命名空间用 codeArticleShare::buttons,不是 articlesshare::buttons(大小写敏感)
  • 设置字段 default 必须给:插件首次启用还没保存设置时,plugin_setting 会返回 default 值

掌握这套结构,写一个完整的 InnoCMS 插件通常 1-3 天 —— 取决于业务复杂度。后续文章会拆常见插件模式(支付集成、外部 API 同步、SEO 自动化)的具体实现。