25 KiB
Ywxapp 插件应用开发指南
适用框架:Ywxapp(基于 ThinkPHP 8 的多应用框架) 本文以
mqttbroker插件为实例,说明从脚手架到打包上线的完整流程。
1. 概述
Ywxapp 的「插件」本质上就是一个 ThinkPHP 多应用(Multi-App):
- 插件源码目录:
addon/<name>/ - 插件命名空间:
addon\<name>\... - 访问 URL 前缀:
/<name>/...(例如/mqttbroker/backend/index)
核心支撑类:
| 类 / 文件 | 职责 |
|---|---|
ywxapp\addon |
插件基类,定义 install() / uninstall() 抽象钩子 |
ywxapp\service\AddonService |
安装 / 卸载 / 打包 / 开发者模式 |
ywxapp\service\AppService |
框架 boot 时扫描 addon/,加载已启用插件的事件、中间件、服务 |
config/addon.php |
插件机制全局配置(路径、优先级、白名单等) |
一个插件 = 一个自带路由、控制器、模型、视图、配置、命令、静态资源的独立应用。
2. 插件目录结构
以 mqttbroker 为例:
addon/mqttbroker/
├── Addon.php # 安装/卸载钩子(必须)
├── info.php # 插件元信息(必须)
├── menu.json # 后台/会员/前台菜单定义
├── install.sql # 建表/种子 SQL(安装时自动导入)
├── common.php # 应用公共文件(随应用自动加载,可选)
├── config.php # 设置表单字段定义(可选,仅当插wophp 文件,
# 统一在 info.php 的 events / middleware / services 数组里声明(详见 3.1)。
# - config.php 非必须:无可配置项的插件直接不建即可。
├── controller/ # 控制器:admin/、api/ ...
├── model/ # 模型
├── service/ # 业务服务类
├── protocol/ # 自定义协议(如 MQTT)
├── view/ # 视图模板(backend/admin/*.html)
├── route/
│ └── app.php # 插件路由
└── static/ # 静态资源(打包时自动迁出到 public/static/addon/mqttbroker/)
3. 核心文件详解
3.1 info.php(元信息,必须)
return [
'name' => 'mqttbroker', // 小写字母,唯一标识,须与目录名一致
'title' => 'MQTT代理',
'intro' => '...',
'author' => '',
'website' => '',
'version' => '2.1.0', // 语义化版本 x.y.z
'state' => 1, // 0=禁用 1=启用(由框架读取控制加载)
'url' => '/mqttbroker/admin',
'license' => '', // 付费插件可声明 license=>true,配合 addon_license_check
'config' => [], // boot 时已合并进容器(见 3.5)
'events' => [], // 随应用加载:app()->loadEvent(...)
'middleware' => [], // 随应用加载:app()->middleware->import(...)
'services' => [], // 随应用加载:app()->bind(...)
];
- 框架
AppService::loadAddonRelevant()在 boot 期扫描addon/,仅对state=1的插件加载其events/middleware/services。 - 安装流程会写入
install_time,并管理state。
事件 / 中间件 / 服务在哪里声明? 统一在
info.php对应数组里填写(见下方示例)。虽然 ThinkPHP 的 MultiApp 在分发到该插件应用时也会自动加载event.php/middleware.php/provider.php,但插件体系的推荐入口是info.php:它在 boot 期由AppService::loadAddonRelevant()读取,对所有启用插件生效,且不依赖当前请求是否命中该插件(时机早、行为一致)。因此插件无需创建event.php/middleware.php文件。
events:事件监听映射,如['MqttClientConnect' => \addon\mqttbroker\listener\Connect::class](等价于Event::listen)。middleware:要注册进应用中间件栈的类,如[\addon\mqttbroker\middleware\MqttAuth::class]。services:服务容器绑定(TP 服务类),如['mqttBroker' => \addon\mqttbroker\service\Broker::class]。没有要注册的就保留空数组
[](你的mqttbroker当前即如此)。这些数组仅在state=1(启用)时才会被加载。
3.2 Addon.php(钩子,必须实现 install/uninstall)
<?php
declare (strict_types = 1);
namespace addon\mqttbroker;
use think\facade\Db;
use ywxapp\AddonBase;
class Addon extends addon
{
// 安装钩子:表由 install.sql 自动创建,通常无需额外处理
public function install()
{
return true;
}
// 卸载钩子:清理本插件数据表与菜单残留(DROP/ALTER 必须放这里!)
public function uninstall()
{
Db::execute("DROP TABLE IF EXISTS `wxapp_mqttbroker_connection`");
// ... 其余表
Db::execute("DELETE FROM wxapp_admin_power WHERE addon='mqttbroker'");
Db::execute("DELETE FROM wxapp_user_rule WHERE name LIKE 'mqttbroker:%'");
return true;
}
}
⚠️ 重要事实:
enable/disable在ywxapp\addon中不是抽象方法;框架AddonService::enable()/disable()仅切换info.php的state值,不会回调插件的enable()/disable()钩子。因此不要把"启用/禁用时需要执行的逻辑"写在钩子里,菜单显隐完全由state决定。
3.3 install.sql(建表,自动导入)
安装时由 AddonService::importsql() 自动执行,规则如下:
- 白名单限制:仅允许
CREATE TABLE与INSERT语句;SET NAMES/SET FOREIGN_KEY_CHECKS等会被跳过(仅记日志)。 - 表命名:
wxapp_<插件名>_*,与核心表前缀保持一致。 - 前缀替换:SQL 中的
__PREFIX__会被替换为数据库实际前缀(若直接写死wxapp_也可,但需与数据库配置一致)。 - 建议全部用
CREATE TABLE IF NOT EXISTS,便于重跑。 - 破坏性语句(DROP / ALTER / DELETE)必须放进
Addon::uninstall(),绝不能写进 install.sql(白名单会拒绝执行)。
示例:
CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_connection` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(191) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `idx_client_id` (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MQTT客户端连接表';
3.4 menu.json(菜单,自动注入)
{
"backend": [
{
"name": "mqttbroker",
"title": "MQTT代理",
"icon": "fa fa-sitemap",
"type": 1,
"sort": 60,
"status": 1,
"child": [
{ "name": "mqttbroker/index", "title": "概览", "icon": "fa fa-dashboard", "type": 2, "sort": 1, "route": "/mqttbroker/backend/index" },
{ "name": "mqttbroker/stats", "title": "实时监控", "type": 2, "sort": 8, "route": "/mqttbroker/backend/stats" }
]
}
],
"member": [],
"frontend": []
}
字段说明:
- 顶层三类:
backend(后台)、member(会员中心)、frontend(前台)。 - 每项:
name(权限标识,首个菜单须与插件标识同名)、title、icon、type(1=菜单 2=按钮)、sort、route(实际访问 URL)、child(子项)。 - 框架会把
name处理为<addon>:<type>:<name>,写入wxapp_admin_power(后台)或wxapp_user_rule(会员/前台)。 - 安装时
createMenu()注入;卸载/重装时先清理旧菜单再重建(保证幂等)。
3.5 config.php(后台设置表单,可选)
重要:
config.php不是必须文件,框架也不会自动读取/渲染它。 它只是一份「设置表单字段定义」,是否生效完全取决于插件自己有没有实现一个后台设置页去include它。
- 若插件没有可配置项(如
wxchat),就不要创建config.php(早期脚手架生成的['status'=>true]属于无效占位,已清理)。- 若插件需要后台可配置(如
mqttbroker),则用「字段描述符数组」格式编写,并在插件内自建设置页读取它(见下方"如何真正用起来")。
return [
[
'name' => 'port',
'title' => '监听端口',
'type' => 'number',
'value' => '1883',
'tip' => 'Broker 监听的 TCP 端口',
],
[
'name' => 'ssl_enabled',
'title' => '启用TLS',
'type' => 'select',
'options' => ['0' => '关闭', '1' => '开启'],
'value' => '0',
],
// ...
];
字段格式:每项为一个描述符 ['name'=>标识, 'title'=>显示名, 'type'=>控件类型, 'value'=>默认值, 'tip'=>提示, 'options'=>下拉项]。type 常用 string / number / select / textarea。
如何真正用起来(以 mqttbroker 为例):config.php 是「设置表单字段定义」,框架统一把配置以独立项写入数据库表 wxapp_addon_config(addon+name 唯一,每行一个配置项),并配合缓存(键 addon_config_<name>,默认 1 小时)。读取/保存统一走 AddonService::config():
- 后台通用配置页 + 列表快捷入口:后台「插件管理」列表页(
app/backend/view/backend/addon/index.html)每行带「配置」按钮(仅对含config.php的插件显示),直达通用页(app/backend/controller/addon.php::setting)。通用页自动按config.php渲染表单、AJAX 保存写库并清缓存。无需每个插件自建页面,没有config.php的插件提示「无可配置项」。 - 插件也可自建设置页(如 mqttbroker 的
controller/backend/MqttBroker.php::setting/saveSetting),只需调用:$def = include ADDON_PATH . 'mqttbroker' . DIRECTORY_SEPARATOR . 'config.php'; $saved = \ywxapp\service\AddonService::config('mqttbroker'); // 读数据库配置(带缓存) // 保存:\ywxapp\service\AddonService::config('mqttbroker', input('post.')); - 运行期读取:业务代码用
AddonService::config($name)、基类getConfig()或助手get_addon_config($name)取值。建议业务默认值在代码里用array_merge($defaults, $saved)叠加,保证未配置时使用默认值(config.php的value仅作表单初始显示)。
三条容易混淆的配置入口,务必区分:
| 入口 | 存储位置 | 加载时机 | 用途 |
|---|---|---|---|
config.php + 数据库 |
wxapp_addon_config(独立项,每行一个配置) |
后台通用配置页或插件自建设置页调用 AddonService::config() |
管理员可视化配置(端口、开关等),带缓存 |
info.php 的 config/events/middleware/services |
info.php |
boot 期由 AppService 加载 |
事件/中间件/服务注册 |
config/ 目录下的 *.php |
各配置文件 | MultiApp 分发到该插件时加载 | 标准 TP 应用级配置 |
另:框架基类
ywxapp\addon::getConfig()也能读config.php(同样合并.addonrc),但仅当你的类继承自它时可用;mqttbroker的做法是直接include+AddonService::config(),二者等效,按需选一即可。
3.6 route/app.php(路由)
<?php
use think\facade\Route;
// 后台
Route::group('admin', function () {
Route::get('index', 'backend/MqttBroker/index');
Route::post('publish', 'backend/MqttBroker/doPublish');
Route::get('stats', 'backend/MqttBroker/stats');
});
// 对外 API
Route::group('api', function () {
Route::get('status', 'api/Mqtt/status');
Route::post('publish', 'api/Mqtt/publish');
});
- 框架以多应用方式加载插件:
addon/<name>即应用<name>,URL 前缀为/<name>/。 - 控制器引用格式:
layer/Controller/action→ 对应文件controller/<layer>/<Controller>.php(如backend/MqttBroker/index→controller/backend/MqttBroker.php::index())。 - 同目录下的
common.php会随应用自动加载。 - ⚠️ 插件的事件监听、中间件、服务容器绑定推荐通过
info.php的events/middleware/services数组声明(boot 期由AppService::loadAddonRelevant()读取,见 3.1),而不要依赖event.php/middleware.php文件——后者仅由 MultiApp 在请求命中该插件应用时才分发加载,时机较晚且与插件统一加载机制不一致。所以插件无需创建这两个文件;要注册就写进info.php,不需要则留空数组[]。
3.7 控制器 / 模型 / 视图 / 服务
- 命名空间:
- 后台控制器:
addon\<name>\controller\admin,继承ywxapp\controller\AddonBackend - API 控制器:
addon\<name>\controller\api,继承ywxapp\controller\ApiController - 模型:
addon\<name>\model\Xxx - 服务:
addon\<name>\service\Xxx
- 后台控制器:
- 鉴权开关(开发期可放开):
protected $noNeedLogin = ['*']; protected $noNeedVerify = ['*']; - 视图:后台模板放
view/backend/admin/*.html,用$this->fetch('admin/index')渲染(路径前缀由框架视图配置决定)。
3.8 command/(命令行 / 常驻进程)
// config/console.php 注册
"mqttbroker:start" => \addon\mqttbroker\command\MqttBroker::class,
- 命令行放在
command/,在config/console.php注册。 - 长驻进程(如 Broker)基于 Workerman:命令内
Worker::runAll()。 - Windows 与 Linux 行为差异:单 PHP 进程无法多
Worker实例,需用DIRECTORY_SEPARATOR === '\\'做平台降级(如 WS 监听仅 Linux 实网生效)。
4. 插件生命周期
框架启动 (AppService::boot)
└─ loadAddonRelevant(): 扫描 addon/,对 state=1 的插件加载 events/middleware/services
└─ 加载 ywxapp/helper.php(提供 addon_url / hook / get_addon_* 等全局函数)
安装 (addon:manage -a develop 或 后台离线/在线安装)
└─ executeInstall(): 写 info(state=0) → install 钩子 → 注入菜单 → importsql
(离线/在线安装默认 state=0,需手动启用;addon:manage -a develop 直接置 state=1)
启用 / 禁用 (后台 toggle)
└─ enable()/disable() 钩子(可重写,返回 false 阻断)→ 再修改 info.php 的 state
默认实现会联动 Menu::enable/disable 同步菜单可见性(见 Addon.php 生成模板)
升级 (后台升级)
└─ upgrade($currentVersion) 钩子(可重写,用于数据/配置迁移)→ 清理配置缓存
卸载 (后台卸载)
└─ uninstall() 钩子(DROP 表 / 清菜单)→ 删除插件目录
生命周期钩子统一由
AddonService::callAddonHook($method)调用插件主类addon\<name>\Addon的同名方法(install/uninstall/enable/disable/upgrade)。 基类ywxapp\addon已为enable/disable/upgrade提供默认空实现,只有install/uninstall仍是抽象方法,必须实现。
4.1 钩子体系(可插拔扩展点)
直接复用 ThinkPHP 自带的事件机制,无需框架额外封装:
- 业务侧在关键节点用 ThinkPHP 内置全局助手
event('事件名', $params)触发(即Event::trigger的全局封装,无需额外封装)。 - 插件侧在
info.php['events']['listen']中声明监听器即可挂接,框架 boot 期loadEvent会自动注册,无需改框架源码。
// addon/<name>/info.php
'events' => [
'listen' => [
'user_login_after' => [\addon\xxx\listener\UserLogin::class],
],
],
// addon/<name>/listener/UserLogin.php
namespace addon\xxx\listener;
class UserLogin
{
public function handle($user)
{
// $user 为登录成功的会员模型实例
}
}
业务侧在关键节点预埋触发点(完整清单):
会员 / 认证(ywxapp/library/User.php)
| 事件名 | 触发时机 | 参数 | 代码位置 |
|---|---|---|---|
user_register_after |
会员注册成功 | 会员模型实例 $user |
User::register() 事务提交前 |
user_login_after |
会员/API 登录成功 | 会员模型实例 $user |
User::login() 末尾 |
user_logout_after |
会员登出成功 | 会员模型实例 $user |
User::logout() 末尾 |
user_changepwd_after |
会员修改密码成功 | 会员模型实例 $user |
User::changepwd() 事务提交前 |
监听器
handle($user)收到的$user均为ywxapp\model\User实例。
内容 / 文章(app/api/controller/v1/Article.php)
| 事件名 | 触发时机 | 参数 | 代码位置 |
|---|---|---|---|
article_create_after |
文章创建成功 | 文章模型实例 $article |
Article::create() 成功后 |
article_update_after |
文章更新成功 | 文章模型实例 $article |
Article::update() 保存后 |
article_delete_after |
文章删除成功 | 文章模型实例 $article |
Article::delete() 删除后 |
MQTT 消息(addon/mqttbroker/service/Broker.php)
| 事件名 | 触发时机 | 参数(数组) | 代码位置 |
|---|---|---|---|
mqtt_message_published |
客户端发布消息被 Broker 接受并分发 | ['topic'=>string, 'payload'=>string, 'qos'=>int, 'retain'=>bool, 'clientId'=>string] |
Broker::handlePublish()(qos0/1)与 Broker::handlePubRel()(qos2) |
说明:
$SYS/开头的内部指标主题不会触发该事件(避免每秒指标造成噪声); qos2 消息在 PUBREL 确认后才真正分发,故事件在handlePubRel中触发,保证每一条消息仅触发一次。
全局异常(ywxapp/handler/ExceptionHandle.php)
| 事件名 | 触发时机 | 参数 | 代码位置 |
|---|---|---|---|
app_exception |
应用抛出异常 | ['exception'=>\Throwable, 'ignoreReport'=>bool] |
ExceptionHandle::report() |
app_exception_report |
异常进入上报流程 | 同上 | ExceptionHandle::report() |
插件市场 / 订单(app/api/controller/v1/Addon.php)
| 事件名 | 触发时机 | 参数(数组) | 代码位置 |
|---|---|---|---|
order_paid |
插件购买订单支付完成(已签发授权 + 写入开发者分红账本) | ['order'=>订单模型, 'aid'=>int 插件id, 'uid'=>int 用户id] |
Addon::completeOrder() 末尾 |
插件可监听此钩子实现"发货 / 开通权益 / 发送通知"等购买后联动。
插件生命周期(ywxapp/service/AddonService.php)
| 事件名 | 触发时机 | 参数(数组) | 代码位置 |
|---|---|---|---|
addon_install_after |
插件安装成功(已写 info 状态、注入菜单、导入 SQL) | ['name'=>string 插件标识, 'info'=>array 插件信息] |
AddonService::executeInstall() 提交后 |
addon_uninstall_after |
插件卸载完成(目录与数据表已清理) | ['name'=>string 插件标识] |
AddonService::uninstall() 末尾 |
addon_enable_after |
插件被启用 | ['name'=>string 插件标识] |
AddonService::enable() 末尾 |
addon_disable_after |
插件被禁用 | ['name'=>string 插件标识] |
AddonService::disable() 末尾 |
注意:与插件主类自身的
install()/uninstall()/enable()/disable()生命周期方法(由callAddonHook调用)不同, 上述*_after是全局事件——任意插件均可监听以对"其它插件"的状态变化做出联动。
新增业务钩子点只需在对应位置调用
event('your_event', $data)即可, 插件侧用同名事件名在info.php['events']['listen']中监听,无需任何注册代码。
4.2 辅助函数层(ywxapp/helper.php)
由 AppService::register() 在框架启动时自动加载,全局可用:
| 函数 | 作用 |
|---|---|
addon_url($name, $url='', $vars=[]) |
生成插件访问地址 /<name>/<url>(修复了原先未定义的致命 bug) |
get_addon_instance($name) |
获取插件主类实例 |
get_addon_info($name, $force=false) |
读取插件元信息(优先缓存) |
get_addon_config($name, $force=false) |
读取插件配置(优先缓存) |
get_appmarket_addon_list($onlyEnabled=false) |
扫描插件目录,返回元信息列表 |
事件触发统一使用 ThinkPHP 自带的
event('事件名', $params)助手,无需框架额外封装。
5. 开发者模式(本地边改边测)⭐
痛点
当你的插件源码直接放在 addon/<name>/ 本地改代码时,框架的「压缩包安装」流程要求目标目录不存在才允许安装(validateAddonInfo 会抛 "Addon already exists")。结果是:目录在、代码能跑,但 install.sql(建表)和 menu.json(菜单)从未执行过,卡在中间态,无法测试。
解决方案:addon:manage -a develop 命令
php think addon:manage mqttbroker -a develop
它在原地执行标准安装步骤,不需要先打包 zip:
- 置
info.php的state = 1(直接启用) - 调用
install钩子 - 清理旧菜单 → 重新注入
menu.json - 执行
importsql()(建表)
幂等、可反复执行:改了 install.sql 或 menu.json 后,再跑一次即可生效(CREATE TABLE IF NOT EXISTS + 菜单先清后建,不会报错)。
开发环境若需跳过远程授权校验:设置
app_debug=1且ywxapp.unknownsources=1(AddonService::valid()会直接放行)。
6. 打包与发布上线
本地打包
通过后台「插件管理 → 打包」按钮(调用 AddonService::package()),生成:
addon/temp/mqttbroker-2.1.0.zip
打包内容 = 整个插件目录 + public/static/addon/<name>/(若存在)。注意:打包时 static/ 会被迁出到 public/static/addon/<name>/ 并删除插件目录内的 static 副本。
上传到服务器
- 服务器后台「插件管理 → 离线安装」,上传该 zip。
- 服务器侧
local()流程:校验 → 解压到addon/<name>/→install钩子 → 注入菜单 →importsql()。 - 安装后默认
state=0(禁用),在后台启用插件即可。
注意事项
- 离线安装要求服务器上
addon/<name>不存在;重装需先卸载。 install.sql白名单限制同上(仅CREATE TABLE/INSERT)。- 静态资源访问路径:
/static/addon/<name>/...。
7. 完整开发流程(实例)
# 1. 脚手架(或手写目录结构)
php think addon:make mqttbroker -a build
# 2. 编写 info.php / Addon.php / install.sql / menu.json / config.php / route/app.php / 控制器 / 模型
# 3. 开发者模式:建表 + 注入菜单 + 启用(只需一次,之后改 SQL/菜单再跑)
php think addon:manage mqttbroker -a develop
# 4. 启动服务 / 调试
php think mqttbroker:start
# 5. 迭代:改业务代码即时生效;
# 改了 install.sql 或 menu.json → 再跑一次 addon:manage <name> -a develop
# 6. 打包
# 后台「插件管理 → 打包」→ 下载 mqttbroker-2.1.0.zip
# 7. 部署:服务器后台离线安装 → 启用
8. 常用命令速查
| 命令 | 作用 |
|---|---|
php think addon:manage <name> -a develop |
开发者模式安装(建表+菜单+启用,免打包,可重跑) |
php think addon:make <name> -a build |
生成插件脚手架(统一命令 addon:make,-a 指定动作) |
php think addon:make <name> -a clear |
清理插件缓存/临时文件 |
php think addon:make <name> -a controller --name=<类> |
生成控制器(另有 --api/--plain) |
php think addon:make <name> -a model --name=<类> |
生成模型 |
php think addon:make <name> -a <action> --name=<类> |
其余生成器:middleware/validate/event/listener/service/subscribe |
| (后台按钮)打包 | 生成 <name>-<version>.zip |
php think mqttbroker:start |
示例插件:启动 Broker(具体命令名随插件自定义) |
9. 常见坑
- install.sql 只允许 CREATE TABLE / INSERT;DROP / ALTER / DELETE 必须放
Addon::uninstall()。 - enable/disable 不触发插件钩子,仅切换
info.php的state。 - 表前缀:install.sql 用
__PREFIX__或固定wxapp_,需与数据库配置一致。 - 路由控制器引用格式:
layer/Controller/action对应controller/<layer>/<Controller>.php。 - 卸载会 DROP 表且删除目录,生产环境操作前务必备份。
- 本地改了 install.sql 没生效:忘了重跑
addon:manage <name> -a develop(它幂等,放心跑)。 - Windows 下多 Worker / WebSocket 仅 Linux 实网可验:用
DIRECTORY_SEPARATOR === '\\'做平台降级。 - 事件/中间件/服务不要写进
event.php/middleware.php文件:插件体系通过info.php的events/middleware/services数组声明(boot 期由AppService加载),插件目录里无需创建这两个文件;命令行则放进command/并在config/console.php注册(不能省略)。 config.php是可选的,框架不会自动读它:只有插件自建后台设置页去include它才生效(见 3.5)。没有可配置项就别建(['status'=>true]这类占位无意义,会误导)。默认值别忘了与运行期代码里的默认值保持一致。