插件开发指南
插件是什么
插件(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 家族调用;至少在两个不同主题下测过(展示型钩子不要假设主题一定埋了侧栏埋点)。