插件开发
插件放在 app/Plugin/<插件名>/ 下,目录名就是插件的标识,用大驼峰。
目录结构
app/Plugin/Demo/
Config/
Info.php 插件信息(必需)
Config.php 默认配置值
Submit.js 后台配置表单(也可用旧格式 Submit.php)
Hook/
Main.php 钩子订阅
Lifecycle.php 生命周期(可选,也可以并进 Main.php)
Controller/
Api.php 插件自己的路由
View/
index.html 模板只有 Config/Info.php 是必需的,其余按需要加。
Config/Info.php
<?php
declare(strict_types=1);
use App\Consts\Plugin;
return [
Plugin::NAME => '演示插件',
Plugin::AUTHOR => '你的名字',
Plugin::WEB_SITE => 'https://example.com',
Plugin::DESCRIPTION => '一句话说明这个插件干什么',
Plugin::VERSION => '1.0.0'
];Config/Config.php
插件的默认配置值。STATUS 是约定的启用状态字段:
<?php
declare(strict_types=1);
return [
'STATUS' => '0',
'api_key' => '',
'enable_notify' => '1',
];Config/Submit.js — 配置表单
后台「配置」弹窗里长什么样,由这个文件决定。
有两种格式,Submit.js 优先。 两个文件同时存在时,Submit.js 会覆盖 Submit.php。
| 类型 | 新格式(推荐) | 旧格式 |
|---|---|---|
| 通用插件 | Config/Submit.js | Config/Submit.php |
| 支付插件 | Config/Submit.js | Config/Submit.php |
| 主题 | Submit.js(主题根目录,不在 Config/ 下) | Config 接口的 SUBMIT 常量 |
主题的路径和插件不一样,别放进
Config/—— 放错了不会报错,只是永远不生效。
写法
文件内容会被前端 eval,所以整份文件必须是一个表达式。裸数组最简单:
[
{
name: `${util.icon("fa-duotone fa-regular fa-gear")} 基本配置`,
form: [
{
title: "商户号",
name: "mch_id",
type: "input",
placeholder: "支付平台分配的商户号",
required: true
},
{
title: "开启通知",
name: "enable_notify",
type: "switch",
text: "启用"
}
]
},
{
name: "高级",
form: [
{ title: "超时时间", name: "timeout", type: "number", placeholder: "秒" }
]
}
]需要写辅助函数就包一层 IIFE:
(() => {
const T = (s) => (typeof i18n === "function" ? i18n(s) : s);
const cfg = (k, d = "") => (assign && assign[k] != null && assign[k] !== "" ? assign[k] : d);
return [
{
name: T("基本配置"),
form: [
{ title: T("接口密钥"), name: "api_key", type: "input", default: cfg("api_key") }
]
}
];
})()结构:分组 + 字段
和 Submit.php 的扁平字段数组不同,Submit.js 是分组的 —— 每个分组在弹窗里是一个页签,name 是页签标题(可以用 util.icon() 加图标),form 才是字段列表。
字段类型
| type | 控件 |
|---|---|
input | 单行输入框 |
password | 密码框 |
number | 数字框 |
textarea | 多行文本域 |
select | 下拉框 |
radio | 单选 |
checkbox | 多选 |
switch | 开关 |
image | 图片上传 |
explain | 说明文字,不保存 |
html | 直接插入一段 HTML |
custom | 自己渲染,见下 |
作用域里能拿到什么
| 变量 | 用途 |
|---|---|
assign | 当前已保存的配置,用 assign.api_key 取值 |
util | 工具函数,最常用的是 util.icon() |
i18n(s) | 翻译函数,做多语言插件时用 |
layui | layui 实例 |
Submit.js不会自动填默认值。 走Submit.php时,内核会把Config.php里的值注入到每个字段的default;但走.js时内核只是把文件当字符串读出去交给前端,这一步不执行。当前值要自己从assign里取 —— 这就是各个插件都写一个cfg()小函数的原因。
custom:自己渲染
字段类型给不了的东西,用 custom 自己画:
{
title: false,
name: "gateway",
type: "custom",
complete: (form, dom) => {
dom.html(`<a href="/plugin/Demo/panel" class="btn btn-sm btn-primary">打开面板</a>`);
}
}
complete()会被反复调用(切页签、重建表单都会触发)。里面起定时器或轮询的话,记得让上一轮自动作废,否则弹窗关掉后还在后台空转。
还能用 Submit.php 吗
能,内核两种都认,目前仓库里 .php 和 .js 都有不少插件在用。
<?php
declare(strict_types=1);
return [
["title" => "接口密钥", "name" => "api_key", "type" => "input", "placeholder" => "请输入你的 API Key"],
["title" => "说明", "name" => "explain", "type" => "explain", "placeholder" => "这段文字只用来提示,不会保存"],
["title" => "开启通知", "name" => "enable_notify", "type" => "switch", "text" => "启用"],
];Submit.php 是扁平的字段数组,没有分组,另外支持 file、editor、json 三个 .js 里没有的类型。纯字段的简单表单用它完全够;需要分组页签、条件显隐、自定义渲染时再上 .js。
别在配置里传 JSON 字符串。
$_POST全局被清洗过,用表单数组的形式提交。
订阅钩子
<?php
declare(strict_types=1);
namespace App\Plugin\Demo\Hook;
use App\Controller\Base\View\UserPlugin;
use Kernel\Annotation\Hook;
class Main extends UserPlugin
{
#[Hook(point: \App\Consts\Hook::USER_VIEW_FOOTER)]
public function footer(): void
{
echo '<script>console.log("hello from Demo")</script>';
}
}基类按场景选:
| 基类 | 用在 |
|---|---|
App\Controller\Base\View\UserPlugin | 前台 |
App\Controller\Base\View\ManagePlugin | 后台 |
全部可用点位见 Hook 钩子大全。
订阅方请写十六进制字面量(
#[Hook(point: 0x2300)])而不是引用常量。注解参数在插件启用时求值,老版本核心上没有这个常量会抛 Error,把插件卡在「已执行 START、未写入 STATUS」的半启用状态。
生命周期
<?php
declare(strict_types=1);
namespace App\Plugin\Demo\Hook;
use Kernel\Annotation\Plugin;
class Lifecycle
{
#[Plugin(state: Plugin::INSTALL)]
public function install(): void
{
// 安装:建表
}
#[Plugin(state: Plugin::START)]
public function start(): void
{
// 每次启用
}
#[Plugin(state: Plugin::STOP)]
public function stop(): void
{
// 停用
}
#[Plugin(state: Plugin::UNINSTALL)]
public function uninstall(): void
{
// 卸载:清数据
}
#[Plugin(state: Plugin::UPGRADE)]
public function upgrade(): void
{
// 升级
}
#[Plugin(state: Plugin::SAVE_CONFIG)]
public function saveConfig(): void
{
// 后台保存配置之后
}
}
state:这个命名参数不能省。 写成位置参数#[Plugin(Plugin::INSTALL)]会静默不触发 —— 插件显示「启用成功」,但建表根本没跑,之后所有功能都在报表不存在。内核是按$arguments['state']取值的。
插件自己的页面
Controller/Api.php 里的方法会映射成路由:
/plugin/<插件名>/<控制器>/<方法>比如 app/Plugin/Demo/Controller/Api.php 里的 login() 方法,访问地址就是:
/plugin/Demo/api/login渲染模板:
echo $this->render("标题", "index.html", ['key' => 'value']);前台注入 UI 的注意事项
插件往前台注入的 HTML/CSS,会和主题的样式打架。两条经验:
- CSS 一律写成
#你的根ID .你的类的形式提高特异性,别写裸类名 - reset 用
:where()包起来,避免误伤主题自己的元素
另外:scrollIntoView 会把 overflow:hidden 的祖先一起滚动,前台弹窗里慎用。
打包与发布
用开发者中心提交插件时,服务端会整目录自动打包,只会剔掉 Config/Config.php。
所以提交前必须先把大文件挪走 —— 二进制、runtime.log、缓存这些,否则包会大得离谱。
调试
- 报错看网站根目录的
runtime.log - 改了 Hook 文件后,停用再启用插件才会重建钩子注册表
- 改了模板,删掉
runtime/view/compile下的内容
