Files
YwxAppThink/docs/插件开发指南.md
T

25 KiB
Raw Blame History

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/disableywxapp\addon不是抽象方法;框架 AddonService::enable()/disable() 仅切换 info.phpstate 值,不会回调插件的 enable()/disable() 钩子。因此不要把"启用/禁用时需要执行的逻辑"写在钩子里,菜单显隐完全由 state 决定。

3.3 install.sql(建表,自动导入)

安装时由 AddonService::importsql() 自动执行,规则如下:

  • 白名单限制:仅允许 CREATE TABLEINSERT 语句;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(权限标识,首个菜单须与插件标识同名)、titleicontype1=菜单 2=按钮)、sortroute(实际访问 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_configaddon+name 唯一,每行一个配置项),并配合缓存(键 addon_config_<name>,默认 1 小时)。读取/保存统一走 AddonService::config()

  1. 后台通用配置页 + 列表快捷入口:后台「插件管理」列表页(app/backend/view/backend/addon/index.html)每行带「配置」按钮(仅对含 config.php 的插件显示),直达通用页(app/backend/controller/addon.php::setting)。通用页自动按 config.php 渲染表单、AJAX 保存写库并清缓存。无需每个插件自建页面,没有 config.php 的插件提示「无可配置项」。
  2. 插件也可自建设置页(如 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.'));
    
  3. 运行期读取:业务代码用 AddonService::config($name)、基类 getConfig() 或助手 get_addon_config($name) 取值。建议业务默认值在代码里用 array_merge($defaults, $saved) 叠加,保证未配置时使用默认值(config.phpvalue 仅作表单初始显示)。

三条容易混淆的配置入口,务必区分

入口 存储位置 加载时机 用途
config.php + 数据库 wxapp_addon_config(独立项,每行一个配置) 后台通用配置页或插件自建设置页调用 AddonService::config() 管理员可视化配置(端口、开关等),带缓存
info.phpconfig/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/indexcontroller/backend/MqttBroker.php::index())。
  • 同目录下的 common.php 会随应用自动加载。
  • ⚠️ 插件的事件监听、中间件、服务容器绑定推荐通过 info.phpevents / 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

  1. info.phpstate = 1(直接启用)
  2. 调用 install 钩子
  3. 清理旧菜单 → 重新注入 menu.json
  4. 执行 importsql()(建表)

幂等、可反复执行:改了 install.sqlmenu.json 后,再跑一次即可生效(CREATE TABLE IF NOT EXISTS + 菜单先清后建,不会报错)。

开发环境若需跳过远程授权校验:设置 app_debug=1ywxapp.unknownsources=1AddonService::valid() 会直接放行)。


6. 打包与发布上线

本地打包

通过后台「插件管理 → 打包」按钮(调用 AddonService::package()),生成:

addon/temp/mqttbroker-2.1.0.zip

打包内容 = 整个插件目录 + public/static/addon/<name>/(若存在)。注意:打包时 static/ 会被迁出到 public/static/addon/<name>/ 并删除插件目录内的 static 副本。

上传到服务器

  1. 服务器后台「插件管理 → 离线安装」,上传该 zip。
  2. 服务器侧 local() 流程:校验 → 解压到 addon/<name>/install 钩子 → 注入菜单 → importsql()
  3. 安装后默认 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. 常见坑

  1. install.sql 只允许 CREATE TABLE / INSERTDROP / ALTER / DELETE 必须放 Addon::uninstall()
  2. enable/disable 不触发插件钩子,仅切换 info.phpstate
  3. 表前缀install.sql 用 __PREFIX__ 或固定 wxapp_,需与数据库配置一致。
  4. 路由控制器引用格式layer/Controller/action 对应 controller/<layer>/<Controller>.php
  5. 卸载会 DROP 表且删除目录,生产环境操作前务必备份。
  6. 本地改了 install.sql 没生效:忘了重跑 addon:manage <name> -a develop(它幂等,放心跑)。
  7. Windows 下多 Worker / WebSocket 仅 Linux 实网可验:用 DIRECTORY_SEPARATOR === '\\' 做平台降级。
  8. 事件/中间件/服务不要写进 event.php / middleware.php 文件:插件体系通过 info.phpevents / middleware / services 数组声明(boot 期由 AppService 加载),插件目录里无需创建这两个文件;命令行则放进 command/ 并在 config/console.php 注册(不能省略)。
  9. config.php 是可选的,框架不会自动读它:只有插件自建后台设置页去 include 它才生效(见 3.5)。没有可配置项就别建(['status'=>true] 这类占位无意义,会误导)。默认值别忘了与运行期代码里的默认值保持一致。