{extend name="common/base"/} {block name="style"} {/block} {block name="body"}
{tips tips="字母开头,只含字母/数字/下划线;会生成目录 plugins/标识/,也是插件的唯一标识。不能用系统保留词(PHP 关键字如 list/class/array、框架目录名、应用名等)" /}
{tips tips="后台插件列表里显示的名称" /}
{tips tips="插件版本号,自己维护" /}
{tips tips="选填" /}
{tips tips="选填;需填写完整的 http:// 或 https:// 网址,生成后写入 Plugin.php 的 $authorUrl 属性" /}
{tips tips="加后台菜单时用这个图标;只能点「选择图标」挑,不能手填" /}
{tips tips="勾选要一起生成的文件;生成完去「插件管理」列表点「安装」" /}
{tips tips="选填" /}

选填,支持 Markdown;生成后会显示在插件配置页的帮助说明中。

一、最快上手

  1. 切到左边「生成插件骨架」tab,填标识、名称,勾选要生成的内容,点「生成骨架」。
  2. 去「插件管理」列表找到新插件,点「安装」。
  3. 左侧菜单会多出插件菜单(勾了后台菜单的话),前台按路由地址访问(勾了前台路由的话)。
生成的是可运行骨架。下面是从零手写的完整教程,也是"看懂生成出来的代码"的说明。

二、从零手写一个插件(8 步)

下面以插件标识 hello(名称"打招呼")为例,目录名和命名空间都跟着这个标识走。

步骤 1:建目录和主类(必须)

新建文件 plugins/hello/Plugin.php:

{literal}<?php
declare(strict_types=1);

namespace plugins\hello;          // 必须是 plugins\目录名

use app\extend\plugin\PluginBase;

class Plugin extends PluginBase     // 类名必须叫 Plugin
{
    protected string $name        = 'hello';
    protected string $title       = '打招呼';
    protected string $version     = '1.0.0';
    protected string $author      = '你的名字';
    protected string $authorUrl   = 'https://example.com'; // 作者官网,可留空
    protected string $description = '一个演示插件';
}{/literal}
只要这个文件存在且语法正确,「插件管理」列表就能扫到它 —— 这已经是一个能安装的最小插件了。

步骤 2:加配置项(可选)

在 Plugin 类里加 getConfigFields(),后台「插件管理 → 配置」会按它自动生成表单。支持 text / textarea / switch / number / radio / select / password / help_area:

{literal}public function getConfigFields(): array
{
    return [
        [
            'name'    => 'word',         // 配置键
            'title'   => '打招呼用语',    // 后台显示的名称
            'type'    => 'text',         // text / textarea / switch / number / select
            'default' => '你好',
            'tips'    => '前台会显示这个词',
        ],
        [
            'name'    => 'enable',
            'title'   => '是否启用',
            'type'    => 'switch',
            'default' => 1,
        ],
    ];
}{/literal}

help_area 是只读帮助区域,default 填 Markdown 原文,系统会安全转换后显示在插件配置页;它不会作为插件设置写入数据库。创建插件时填写的帮助说明会自动写入生成的 plugins/插件标识/Plugin.php。

读取配置:

{literal}// 插件类内部
$word = $this->getConfig('word', '你好');

// 插件外部(前台控制器等)
$config = \app\extend\plugin\PluginManager::instance()->getPluginConfig('hello');
$word   = $config['word'] ?? '你好';{/literal}

插件代码通过 PluginManager 读取记录时,只能读取自身标识对应的配置;查询其他插件会被拒绝,查询已安装插件列表也只会返回自身记录。插件运行在 CMS 的 PHP 进程中,这项限制约束插件系统提供的读取接口,不构成 PHP 代码或直接数据库访问的沙箱。

步骤 3:加后台菜单和页面(可选)

① 菜单定义 plugins/hello/admin/menu.php:

{literal}<?php
return [
    [
        'title'    => '打招呼',
        'url'      => '',              // 有子菜单时留空
        'children' => [
            // 插件自己的后台页面用这个约定地址,不用自己配路由
            ['title' => '设置页', 'url' => 'admin/plugin/run?name=hello&a=index'],
        ],
    ],
];{/literal}

② 后台控制器 plugins/hello/admin/controller/Index.php:

{literal}<?php
declare(strict_types=1);

namespace plugins\hello\admin\controller;

use app\extend\plugin\PluginAdminBase;
use app\extend\plugin\PluginManager;

class Index extends PluginAdminBase      // 继承它 = 自带后台登录校验 + $this->render()
{
    protected string $plugin = 'hello';

    public function index()
    {
        return $this->render('index', [
            'config' => PluginManager::instance()->getPluginConfig('hello'),
        ]);
    }
}{/literal}

③ 后台视图 plugins/hello/admin/view/index.html:

{literal}<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="{__ADMIN__}/css/miu.css">
    <script src="{__ADMIN__}/layui/layui.js"></script>
</head>
<body>
<div class="p-3">
    当前打招呼用语:{$config.word|default='(未设置)'}
</div>
</body>
</html>{/literal}
安装插件不会自动加菜单。装完去「插件管理」列表点一下「添加后台菜单」按钮(那列里带 + 图标的),确认后菜单才会写进 cms_admin_rule 并授权给超管组,左侧才会出现「打招呼 → 设置页」。加过之后这个按钮会变成减号(删除后台菜单),点它可以只删菜单、不动插件和数据。禁用插件时菜单自动隐藏,卸载时自动删除。
菜单图标用的是插件类里声明的 $icon(创建插件时选的那个),排序固定 100。

步骤 4:加前台页面(可选)

① 路由 plugins/hello/route.php:

{literal}<?php
// 格式:'路由规则' => '完整控制器类@方法'
return [
    'hello/index' => 'plugins\hello\controller\Index@index',
];{/literal}

② 前台控制器 plugins/hello/controller/Index.php:

{literal}<?php
declare(strict_types=1);

namespace plugins\hello\controller;

use app\extend\plugin\PluginManager;
use think\facade\View;

class Index
{
    public function index()
    {
        $pm = PluginManager::instance();

        // 把视图目录指到插件自己的 view/
        View::config([
            'view_path' => $pm->getPluginPath() . 'hello' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR,
        ]);

        // 模板名以 / 开头 = 相对上面设的 view_path;
        // 不加 / 的话框架会按「当前控制器」自动加前缀,容易拼出找不到的路径
        return View::fetch('/index', [
            'config' => $pm->getPluginConfig('hello'),
        ]);
    }
}{/literal}

③ 前台视图 plugins/hello/view/index.html:

{literal}<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>打招呼</title></head>
<body>
<h1>{$config.word|default='你好'}</h1>
</body>
</html>{/literal}
启用插件后访问 http://你的域名/hello/index 即可打开。

步骤 5:加模板标签(可选)

① 标签类 plugins/hello/taglib/Hello.php:

{literal}<?php
declare(strict_types=1);

namespace plugins\hello\taglib;

use think\template\TagLib;

class Hello extends TagLib
{
    // 标签定义:attr 是允许的属性名,close=0 表示自闭合标签
    protected $tags = [
        'word' => ['attr' => 'default', 'close' => 0],
    ];

    // 编译标签:返回的是一段 PHP 代码,不是直接输出
    public function tagWord(array $tag, string $content): string
    {
        $default = (string) ($tag['default'] ?? '你好');
        return '<?php echo \plugins\hello\taglib\Hello::render(' . var_export($default, true) . '); ?>';
    }

    public static function render(string $default = '你好'): string
    {
        $cfg  = \app\extend\plugin\PluginManager::instance()->getPluginConfig('hello');
        $word = $cfg['word'] ?? $default;
        return htmlspecialchars($word, ENT_QUOTES);
    }
}{/literal}

② 在任何前台模板里用:

{literal}{hello:word default='你好' /}{/literal}
标签前缀 = 标签类类名的小写,所以类叫 Hello 就写 {literal}{hello:xxx}{/literal}。注意别和别的插件的类名撞车。

步骤 6:监听钩子(可选)

在 Plugin 类里定义与钩子同名的驼峰方法,系统会自动注册(完整清单见下面第三节):

{literal}    // 文档发布后
    public function documentAdded(int $id, array $data): void
    {
        // $id 是新文档ID,$data['title'] 是标题
    }

    // 用户登录后($user 是引用,改了会带到调用方)
    public function userLogin(array &$user): void
    {
        $user['last_login_from'] = 'plugin';
    }

    // 模板解析完成、输出前($content 是整页 HTML,可插入内容)
    public function templateParse(string &$content): void
    {
        $content = str_replace('</body>', '<div>页脚</div></body>', $content);
    }{/literal}

也可以改用映射方式,新建 plugins/hello/hooks.php:

{literal}<?php
return [
    'document_added' => 'onDocAdded',   // 钩子名 => 插件类里的方法名
];{/literal}
两种方式二选一,别对同一个钩子重复声明,否则回调会被注册两次。
引用传参的坑(重要):带 & 的参数是引用,插件方法里必须也写成 &$x 才能改到调用方的值;只写 $x 拿到的是副本,改了没用。 另外,引用参数只能传「变量」,不能传字面量或表达式——自己在插件里手动触发钩子时,要写 Hook::trigger('user_login', $user), 不能写 Hook::trigger('user_login', $this->getUser()) 或 Hook::trigger('user_login', ['a'=>1]), 否则 PHP 会直接报 Argument #2 cannot be passed by reference。不确定某个钩子的参数是不是引用,先看第二节的清单或 PluginBase 里的方法签名。

步骤 7:建自己的数据表(可选)

① plugins/hello/install.sql(安装时执行):

{literal}CREATE TABLE IF NOT EXISTS `__PREFIX__hello_log` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `content` varchar(255) NOT NULL DEFAULT '',
  `created_at` int(11) UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打招呼日志';{/literal}

② plugins/hello/uninstall.sql(卸载时执行):

{literal}DROP TABLE IF EXISTS `__PREFIX__hello_log`;{/literal}

③ 插件里读写(表名不用写前缀,Db::name() 会自动加):

{literal}use think\facade\Db;

Db::name('hello_log')->insert(['content' => 'hi', 'created_at' => time()]);

$rows = Db::name('hello_log')->order('id', 'desc')->limit(10)->select()->toArray();{/literal}
__PREFIX__ 会被自动替换成项目实际表前缀。表名建议带插件标识(如 hello_log),避免和主表冲突。

步骤 8:安装并测试

  1. 后台 → 插件 → 插件管理 → 找到你的插件 → 点「安装」。
  2. 安装会执行 install.sql → 写插件记录 → 启用插件 → 注册钩子。后台菜单不会自动加,要回列表点「添加后台菜单」按钮(已加过的会变成减号,点一下就是删除)。
  3. 左侧菜单出现插件菜单;前台按 route.php 的规则访问。
  4. 改完代码后:改了模板标签要去后台点「清缓存」(模板要重新编译);改了钩子/菜单去插件管理「禁用」再「启用」。

三、钩子清单(共 {$hooks|count} 个)

{volist name="hooks" id="h"} {/volist}
钩子名触发时机插件类里写的方法名
{$h.name} {$h.title} {$h.method}()
方法名是钩子名的驼峰形式(document_add → documentAdd)。带 & 的参数是引用传参,插件里改了调用方拿到的是改后的值。

四、常用 API 速查

用途写法
插件内取配置$this->getConfig('键名', 默认值)
插件外取配置PluginManager::instance()->getPluginConfig('标识')
取插件实例PluginManager::instance()->makePlugin('标识')
插件根目录PluginManager::instance()->getPluginPath()
已启用的插件PluginManager::instance()->getEnabledPluginNames()
后台渲染插件视图继承 PluginAdminBase 后用 $this->render('模板名', $变量)
前台渲染插件视图View::config(['view_path'=>...]); View::fetch('/index', $变量);(模板名以 / 开头)
数据库Db::name('表名')(不带前缀)

五、命名与约定(必看)

  • 目录名 = 插件标识:字母开头,只含字母/数字/下划线;命名空间必须是 plugins\标识,主类必须叫 Plugin。
  • 后台页面地址:admin/plugin/run?name=<标识>&c=<控制器>&a=<方法>(c 默认 Index,a 默认 index),后台菜单的 url 就这么写,不用自己配路由。
  • 后台 action 用蛇形命名(如 save_config)。鉴权按节点 src 匹配且会转小写,驼峰 action 会匹配不上。
  • 表名带插件前缀(如 hello_log),避免和主表冲突。
  • 渲染插件视图时模板名要以 / 开头(如 View::fetch('/index'))。不加的话框架会按「当前控制器」自动加前缀 —— 后台插件页地址是 admin/plugin/run,控制器是 plugin,传 'index' 会被拼成 plugin/index.html,报「模板文件不存在」。后台继承 PluginAdminBase 用 $this->render() 的已自动处理,手写前台控制器时要注意。
  • type 2 弹层自适应高度:子页面要加 html,body{height:auto},否则后台全局的 height:100% 会让 layer.iframeAuto 量不出真实高度;另外 maxHeight 要写数字,不能写 '700px'。
  • 缓存:装/卸/启用/禁用会自动清插件缓存;模板标签改了要手动点「清缓存」重新编译。

六、常见问题排错

  • 列表里看不到我的插件:检查目录名、命名空间、类名三者是否一致;Plugin.php 有语法错误也会扫不到。
  • 后台菜单没出现:先确认点过列表里的「添加后台菜单」按钮(安装不会自动加);加过之后菜单会授权给「超级管理员」组,如果你用的不是超管账号,去 系统 → 权限角色 给对应角色勾上;也可以去 系统 → 功能节点 看节点在不在。
  • 点按钮提示"你没有权限":每个后台操作动作都要在 系统 → 功能节点 里有对应节点(src 与访问路径一致),否则连超管都会被拦。
  • 模板标签没生效:后台点一下「清缓存」重新编译模板。
  • 弹层高度不对:见上面「命名与约定」里的弹层那条。
  • 报 Class not found:命名空间和目录没对上,或文件名大小写不符。
  • 改完代码没反应:先点「清缓存」;还不行就把插件禁用再启用。

七、参考示例

系统自带一个完整示例插件 plugins/demo/,把配置、钩子、后台菜单、前台路由、模板标签、建表全演示了一遍,直接照抄最快。
{/block} {block name="script"} {/block}