{extend name="common/base"/} {block name="style"} {/block} {block name="body"}
hello(名称"打招呼")为例,目录名和命名空间都跟着这个标识走。新建文件 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}
在 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 代码或直接数据库访问的沙箱。
① 菜单定义 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。① 路由 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 即可打开。① 标签类 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}。注意别和别的插件的类名撞车。在 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 里的方法签名。
① 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),避免和主表冲突。install.sql → 写插件记录 → 启用插件 → 注册钩子。后台菜单不会自动加,要回列表点「添加后台菜单」按钮(已加过的会变成减号,点一下就是删除)。route.php 的规则访问。| 钩子名 | 触发时机 | 插件类里写的方法名 |
|---|---|---|
{$h.name} |
{$h.title} | {$h.method}() |
document_add → documentAdd)。带 & 的参数是引用传参,插件里改了调用方拿到的是改后的值。| 用途 | 写法 |
|---|---|
| 插件内取配置 | $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 就这么写,不用自己配路由。save_config)。鉴权按节点 src 匹配且会转小写,驼峰 action 会匹配不上。hello_log),避免和主表冲突。/ 开头(如 View::fetch('/index'))。不加的话框架会按「当前控制器」自动加前缀 —— 后台插件页地址是 admin/plugin/run,控制器是 plugin,传 'index' 会被拼成 plugin/index.html,报「模板文件不存在」。后台继承 PluginAdminBase 用 $this->render() 的已自动处理,手写前台控制器时要注意。html,body{height:auto},否则后台全局的 height:100% 会让 layer.iframeAuto 量不出真实高度;另外 maxHeight 要写数字,不能写 '700px'。Plugin.php 有语法错误也会扫不到。