mtcms 应用市场 文档中心 应用市场 →

插件开发指南

从目录结构到钩子机制,开发一个能过审的插件更新于 2026-10-31 23:45:24

插件是什么

插件(addon)给核心「加功能」,通过挂钩子实现;只允许与 8 个官方钩子点交互,禁止修改任何核心文件或其他插件、主题的文件 —— 覆盖核心文件的插件会被审核直接拒绝。

插件可自建数据表(前缀固定为 mt_插件名_),未启用或未授权时钩子不加载,对站点零损耗;卸载后表、权限节点、配置、目录全部清理,不留残留。

平台不提供任何跳过授权校验的开发豁免开关,调试请使用市场端的试用授权,路径与真实客户完全一致。

目录结构与入口类

目录结构:addons/插件名/ 下放 manifest.json(必需)、Plugin.php(必需)、controller/(可选,插件自己的后台接口)、static/(可选,静态资源)、install.sql(可选,安装时逐条执行的建表语句)。目录名必须等于 manifest.name,不能改。

Plugin.php 入口类的命名空间必须是 addons\插件名,类名固定为 Plugin,无参构造。install() 与 uninstall() 回调可选(用 method_exists 判断),但类本身必须存在。

两个易踩的坑:manifest.hooks 里声明的钩子方法必须真实存在且签名兼容,否则功能静默失效(异常只记日志不中断主流程);install() 执行时插件自己的配置行还没创建,不要在里面读配置,一律用代码内默认值兜底。

// addons/mt_hello_bar/manifest.json
{
    "name": "mt_hello_bar",
    "title": "站点公告条",
    "version": "1.0.0",
    "cms_version": ">=8.0",
    "author": "你的名字或公司",
    "description": "在页面底部输出一条公告横幅。",
    "hooks": { "page_footer": "onPageFooter" },
    "tables": [],
    "pricing": { "type": "free" }
}

// addons/mt_hello_bar/Plugin.php
<?php
declare(strict_types=1);

namespace addons\mt_hello_bar;   // 必须与目录名一致

class Plugin
{
    // 无参构造,不要在构造函数里做重活
    public function __construct() {}

    /** 展示型钩子:返回 HTML 字符串,多插件输出自动拼接 */
    public function onPageFooter(array $params = []): string
    {
        return '<div data-mt-addon="mt_hello_bar" '
             . 'style="position:fixed;bottom:0;left:0;right:0;background:#111;'
             . 'color:#fff;text-align:center;padding:8px;font-size:13px;z-index:9999;">'
             . '本站已开通在线留言,欢迎反馈。'
             . '</div>';
    }
}

manifest 字段

必填:name(小写字母开头,只允许字母、数字、下划线,安装端不接受短横线,统一用下划线如 mt_seo_boost)、title(中文显示名)、version(语义化版本)、cms_version(适配的核心版本,缺了直接拒绝安装)。

建议填写:description(市场详情页取第一行作副标题)、author 与 author_url、homepage。

选填但有规则:hooks 声明 {钩子名: 方法名},钩子名必须在官方清单内,出现清单外的钩子安装时直接拒绝;tables 登记插件自建的表,卸载时管理器按它兜底删表,必须登记;menu 注册后台菜单节点;auth_rules 声明权限点数组,安装时自动写入权限节点表。

钩子点清单(仅 8 个)

内容类:content_before_save(文章入库前)、content_after_save(入库后)、content_before_delete(软删除前),均为通知型,参数含表名、动作与数据,无返回值。

展示类:page_header(页面 body 开头)、page_footer(body 结束前)、content_detail_sidebar(详情页侧栏,需主题埋点),返回 HTML 字符串,多个插件的输出自动拼接。

后台类:admin_menu(返回 {title, path, perm} 数组,自动并入后台「扩展管理」分组,无需改核心前端)、admin_content_form(内容编辑页扩展,预留)。

监听器抛异常只记日志、不中断主流程 —— 插件不允许拖垮核心,所以插件内部所有会失败的操作都要 try/catch 并自行留痕,否则出了问题静默无感。

数据表与卸载

表名前缀固定为 mt_插件名_,表结构写在 install.sql,安装时逐条执行,请使用 IF NOT EXISTS 保证可重复执行。

卸载链路:Plugin::uninstall() → 按 manifest.tables 兜底 DROP → 清权限节点 → 删配置行 → 删插件目录,任何一步都不允许留残留。

插件的后台接口在 controller/ 下实现,路由自行注册并挂 JWT 中间件与 manifest.auth_rules 里声明的权限点。

安装期安全扫描(命中即拒)

包内所有 .php 文件会被静态扫描,命中任意一条直接拒绝安装:eval()、create_function()、assert() 等动态执行;exec()、system()、shell_exec()、passthru()、proc_open()、popen() 等命令执行;base64_decode(gzinflate(...)) 这类混淆后门特征;向 app/、config/、route/、public/ 等核心目录写文件。

需要执行外部程序(如调用命令行工具)的功能,一律改成 HTTP 调用,或让站长在插件配置里填可执行路径、由用户触发。

打包时不要包含 .DS_Store、node_modules、.git、源工程文件;manifest.json 必须在 zip 根目录(进入目录全选内容压缩,而不是压缩目录本身),zip 多套一层目录是最常见的安装报错原因。

本地调试与自查

本地安装走后台「扩展管理 → 插件管理 → 上传插件包安装」,同一版本重复安装会被拒,更高版本走升级路径并保留站长配置。

本地导入的插件会带「未验证来源」标记(没有市场签名);包名已在市场登记为付费商品的,本地导入会被直接拒绝,只有市场没有的包名(站长自建插件)才允许本地上传。

提交前自查:目录名等于 manifest.name;hooks 里每个方法名真实存在;自建表已登记且 install.sql 可重复执行;卸载能清干净;无 eval / exec 家族调用;至少在两个不同主题下测过(展示型钩子不要假设主题一定埋了侧栏埋点)。