Skip to content

插件开发

插件放在 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
<?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
<?php
declare(strict_types=1);

return [
    'STATUS' => '0',
    'api_key' => '',
    'enable_notify' => '1',
];

Config/Submit.js — 配置表单

后台「配置」弹窗里长什么样,由这个文件决定。

有两种格式,Submit.js 优先。 两个文件同时存在时,Submit.js 会覆盖 Submit.php

类型新格式(推荐)旧格式
通用插件Config/Submit.jsConfig/Submit.php
支付插件Config/Submit.jsConfig/Submit.php
主题Submit.js(主题根目录,不在 Config/ 下)Config 接口的 SUBMIT 常量

主题的路径和插件不一样,别放进 Config/ —— 放错了不会报错,只是永远不生效。

写法

文件内容会被前端 eval,所以整份文件必须是一个表达式。裸数组最简单:

js
[
    {
        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:

js
(() => {
    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)翻译函数,做多语言插件时用
layuilayui 实例

Submit.js 不会自动填默认值。Submit.php 时,内核会把 Config.php 里的值注入到每个字段的 default;但走 .js 时内核只是把文件当字符串读出去交给前端,这一步不执行。当前值要自己从 assign 里取 —— 这就是各个插件都写一个 cfg() 小函数的原因。

custom:自己渲染

字段类型给不了的东西,用 custom 自己画:

js
{
    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
<?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扁平的字段数组,没有分组,另外支持 fileeditorjson 三个 .js 里没有的类型。纯字段的简单表单用它完全够;需要分组页签、条件显隐、自定义渲染时再上 .js

别在配置里传 JSON 字符串。 $_POST 全局被清洗过,用表单数组的形式提交。

订阅钩子

php
<?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
<?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

渲染模板:

php
echo $this->render("标题", "index.html", ['key' => 'value']);

前台注入 UI 的注意事项

插件往前台注入的 HTML/CSS,会和主题的样式打架。两条经验:

  1. CSS 一律写成 #你的根ID .你的类 的形式提高特异性,别写裸类名
  2. reset 用 :where() 包起来,避免误伤主题自己的元素

另外:scrollIntoView 会把 overflow:hidden 的祖先一起滚动,前台弹窗里慎用。

打包与发布

用开发者中心提交插件时,服务端会整目录自动打包,只会剔掉 Config/Config.php

所以提交前必须先把大文件挪走 —— 二进制、runtime.log、缓存这些,否则包会大得离谱。

调试

  • 报错看网站根目录的 runtime.log
  • 改了 Hook 文件后,停用再启用插件才会重建钩子注册表
  • 改了模板,删掉 runtime/view/compile 下的内容

基于 MIT 协议开源