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 協議開源