插件开发
从 plugin.json 到打包发布,用一个能跑通的「公告条」插件走完全流程:路由、后台页、配置、钩子、数据表、模板标签。
本页目录(28 分钟读完)
这一章只做一件事:带你写出一个能跑的插件。
给会 PHP 的人看。每步都是「建什么文件、贴什么代码、跑什么命令」。 页面打不开 → 看 排错手册。
插件是什么#
一句话:一个目录 + 一个入口文件 + 一堆注册动作。
| 注册什么 | 写在哪 | 作用 |
|---|---|---|
| 钩子 | plugin.php | 挂到系统事件上 |
| 路由 | plugin.php | 加自己的页面 |
| 菜单 | plugin.php | 后台左侧入口 |
| 配置 | plugin.json | 后台设置表单 |
| 数据表 | tables.php | 存自己的数据 |
不改核心代码。插件删掉就干净。
第 1 步:建目录#
作用:先有地方放代码。
步骤:
- 进
plugin/目录 - 新建
notice文件夹(这是插件标识) - 里面建这几个文件
plugin/notice/
├── plugin.json ← 必须
├── plugin.php ← 必须
├── src/
│ ├── Hooks.php
│ └── AdminController.php
└── view/default/
└── admin/plugin/notice/index.html命名规则:
| 目录名 | 命名空间 |
|---|---|
notice | Plugin\Notice\ |
member-generator | Plugin\MemberGenerator\ |
multi-site | Plugin\MultiSite\ |
规则:短横线去掉,每个单词首字母大写。
打不开? → 看 排错手册
第 2 步:写 plugin.json#
作用:告诉系统这个插件叫什么、有什么配置。
步骤:
- 新建
plugin/notice/plugin.json - 贴下面的内容
- 保存
{
"name": "notice",
"title": "公告条",
"version": "1.0.0",
"author": "你的名字",
"description": "在页面顶部显示一条可关闭的公告。",
"priority": 60,
"require": {
"php": ">=7.2"
},
"autoload": {
"Plugin\\Notice\\": "src/"
},
"view": "view",
"config_fields": {
"enabled": { "label": "启用公告", "type": "switch", "tips": "关闭后前台不再显示" },
"text": { "label": "公告文字", "type": "text", "tips": "20 字以内最好看" },
"link": { "label": "跳转链接", "type": "text", "tips": "留空则不跳转" }
},
"config": {
"enabled": 1,
"text": "本站已启用全新会员系统",
"link": "/register"
}
}字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 插件标识,必须和目录名一样 |
title | 是 | 后台显示的名字 |
version | 是 | 版本号 |
priority | 否 | 加载顺序,数字越小越先 |
require | 否 | 依赖:php / plugins / ext |
autoload | 否 | 命名空间 → 目录 |
config_fields | 否 | 配置表单,后台自动渲染 |
config | 否 | 配置默认值 |
依赖别的插件:
"require": { "plugins": ["member"] }依赖不满足时,插件列表显示「依赖不满足」,拒绝启用。
打不开? → 看 排错手册
第 3 步:写 plugin.php#
作用:注册钩子、路由、菜单。这是唯一入口。
步骤:
- 新建
plugin/notice/plugin.php - 贴下面的内容
- 保存
<?php
/**
* 公告条插件 — 入口
*
* @var \Aucms\Plugin\PluginInstance $plugin
*/
// 1) 钩子
$plugin->hook('app_init', 'Plugin\\Notice\\Hooks@onAppInit', 20);
$plugin->hook('view_render', 'Plugin\\Notice\\Hooks@onRender', 60);
// 2) 路由(后台必须显式挂 AdminAuth)
$plugin->route(function (\Aucms\Routing\Router $router) {
$admin = function_exists('config') ? config('app.admin_path', 'admin') : 'admin';
$router->group(
array('prefix' => $admin, 'middleware' => array('App\\Middleware\\AdminAuth')),
function (\Aucms\Routing\Router $router) {
$router->get('/plugin/notice', 'Plugin\\Notice\\AdminController@index');
$router->post('/plugin/notice/save', 'Plugin\\Notice\\AdminController@save');
}
);
}, 60);
// 3) 后台菜单
$plugin->menu(array(
'title' => '公告条',
'url' => 'plugin/notice',
'icon' => 'content',
'group' => '插件',
));【最容易踩的坑】后台路由必须写在 route() 回调里。
写在回调外面不会报错,但所有后台路由会集体 404。打不开? → 看 排错手册
第 4 步:写钩子#
作用:让插件在系统事件发生时干活。
三种钩子:
| 方式 | 干什么 | 例子 |
|---|---|---|
run | 通知一声,不影响流程 | 内容创建后记日志 |
filter | 改值,返回新值 | 改页面 HTML |
first | 返回非 null 就中断 | 准入判断 |
优先级:数字越小越先执行,默认 100。
真实可用的钩子(照着用不会失效):
| 钩子 | 类型 | 什么时候触发 |
|---|---|---|
app_init | run | 每次请求开始 |
after_dispatch | run | 响应发出前 |
plugin_booted | run | 所有插件加载完 |
content_create | run | 内容新建后 |
content_update | run | 内容更新后 |
content_delete | run | 内容删除后 |
content_detail | filter | 详情装配后 |
view_render | filter | 页面渲染完 |
admin_menu | filter | 后台菜单生成 |
field_types | filter | 字段类型注册 |
upload_after | run | 上传完成 |
user_register | run | 注册成功 |
user_login_success | run | 前台登录成功 |
schedule_tasks | filter | 调度任务注册 |
不要凭白名单写代码。
要找钩子,去源码里搜 hook('名字')。写 Hooks.php:
- 新建
plugin/notice/src/Hooks.php - 贴下面的内容
- 保存
<?php
namespace Plugin\Notice;
class Hooks
{
public static function onAppInit()
{
$container = \aucms_container();
if ($container === null || $container->has('notice.config')) {
return;
}
$container->singleton('notice.config', function () {
return (array) self::config();
});
}
public static function config()
{
try {
$loaded = \plugin()->loaded();
if (isset($loaded['notice'])) {
return (array) $loaded['notice']->config();
}
} catch (\Throwable $e) {
// 落到默认
}
return array('enabled' => 0, 'text' => '', 'link' => '');
}
public static function onRender($html, $template = '')
{
$cfg = self::config();
if ((int) $cfg['enabled'] !== 1 || trim((string) $cfg['text']) === '') {
return $html;
}
if (strpos(ltrim($template, '/'), 'admin/') === 0) {
return $html;
}
if (strpos($html, 'data-notice-bar') !== false) {
return $html;
}
$text = htmlspecialchars((string) $cfg['text'], ENT_QUOTES, 'UTF-8');
$link = trim((string) $cfg['link']);
$inner = $link !== ''
? '<a class="notice-bar__link" href="' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8') . '">' . $text . '</a>'
: '<span>' . $text . '</span>';
$bar = '<div class="notice-bar" data-notice-bar>'
. $inner
. '<button type="button" class="notice-bar__close" aria-label="关闭">×</button>'
. '</div>'
. '<style>.notice-bar{display:flex;align-items:center;justify-content:center;gap:12px;'
. 'padding:9px 16px;background:#eef4ff;color:#1d4ed8;font-size:14px}'
. '.notice-bar a{color:#1d4ed8}.notice-bar__close{border:0;background:none;font-size:18px;'
. 'cursor:pointer;color:#5b7bd5;line-height:1}</style>'
. '<script>(function(){var b=document.querySelector("[data-notice-bar]");'
. 'if(!b)return;b.querySelector(".notice-bar__close").onclick=function(){b.remove()};})();</script>';
$pos = stripos($html, '<body');
if ($pos !== false) {
$gt = strpos($html, '>', $pos);
if ($gt !== false) {
return substr($html, 0, $gt + 1) . $bar . substr($html, $gt + 1);
}
}
return $bar . $html;
}
}打不开? → 看 排错手册
第 5 步:写后台配置页#
作用:在后台能改公告文字。
步骤:
5.1 写控制器
- 新建
plugin/notice/src/AdminController.php - 贴下面的内容
- 保存
<?php
namespace Plugin\Notice;
use Aucms\Http\Request;
class AdminController extends \App\Controller\Admin\BaseController
{
const FORM_SCOPE = 'plugin.notice.save';
public function index(Request $request)
{
$plugin = \plugin()->loaded()['notice'];
return $this->admin('plugin/notice/index', array(
'cfg' => (array) $plugin->config(),
'form_token' => $this->security->formToken(self::FORM_SCOPE),
));
}
public function save(Request $request)
{
if (!$this->checkFormToken($request, self::FORM_SCOPE)) {
return $this->error('表单已过期,请刷新页面后重试');
}
$plugin = \plugin()->loaded()['notice'];
$plugin->saveConfig(array(
'enabled' => (int) $request->post('enabled', 0) === 1 ? 1 : 0,
'text' => \Aucms\Security::strip((string) $request->post('text', ''), 100),
'link' => \Aucms\Security::strip((string) $request->post('link', ''), 255),
));
return $this->success(array('redirect' => '/admin/plugin/notice'), '已保存');
}
}5.2 写模板
- 新建
plugin/notice/view/default/admin/plugin/notice/index.html - 贴下面的内容
- 保存
{layout admin/layout}
<div class="nv-card">
<div class="nv-card-head"><h3 class="nv-card-title">公告条设置</h3></div>
<div class="nv-card-body">
<form method="post" action="/admin/plugin/notice/save">
<input type="hidden" name="_token" value="{$csrf}">
<input type="hidden" name="_form_token" value="{$form_token}">
<div class="nv-form-row">
<label class="nv-label">启用</label>
<select class="nv-input" name="enabled">
<option value="1" {if $cfg.enabled == 1}selected{/if}>开启</option>
<option value="0" {if $cfg.enabled == 0}selected{/if}>关闭</option>
</select>
</div>
<div class="nv-form-row">
<label class="nv-label">公告文字</label>
<input class="nv-input" type="text" name="text" value="{$cfg.text}" maxlength="100">
</div>
<div class="nv-form-row">
<label class="nv-label">跳转链接</label>
<input class="nv-input" type="text" name="link" value="{$cfg.link}" placeholder="/register">
<p class="nv-tips">留空则只显示文字</p>
</div>
<div class="nv-form-actions">
<button class="nv-btn nv-btn-primary" type="submit">保存</button>
</div>
</form>
</div>
</div>$form_token 不会自动注入,必须在 index() 里显式传进模板。
少了它,保存会 419「表单已过期」。
两个令牌都要有:
_token → 防跨站
_form_token → 防重复提交打不开? → 看 排错手册
第 6 步:启用测试#
作用:让插件跑起来。
步骤:
- 启用插件
php aucms plugin:enable notice- 重建缓存
php aucms cache:rebuild- 前台随便开一页,顶部有公告条
- 后台左侧「插件」组下有「公告条」菜单
- 点进去改文字,保存,刷新前台看变化
打不开? → 看 排错手册
第 7 步:加数据表(可选)#
作用:要存数据时才建表。纯展示插件跳过这步。
步骤:
7.1 写 tables.php
<?php
return array(
'hit' => array(
'columns' => array(
'id' => 'id',
'target_id' => 'int not null default 0',
'ip' => 'string:64 not null default \'\'',
'created_at' => 'timestamp not null default 0',
),
'options' => array(
'primary' => 'id',
'index' => array('target' => array('target_id')),
),
),
);表名自动加前缀:{前缀}notice_hit。代码里用 $plugin->table('hit') 取。
7.2 写 install.php
<?php
/**
* @var \Aucms\Plugin\PluginInstance $plugin
* @var \Aucms\Plugin\PluginManager $manager
*/
// 【必须】install 时 autoload 还没注册,要手动 require
require_once __DIR__ . '/src/Hooks.php';
$db = $plugin->container()->make('db');
$schema = new \Aucms\Db\Schema($db->connection(), $db->prefix());
$tables = include __DIR__ . '/tables.php';
foreach ($tables as $name => $def) {
if ($schema->hasTable($name)) {
continue; // 重复安装不报错
}
$schema->create($name, $def['columns'], isset($def['options']) ? $def['options'] : array());
}
return true; // 返回 false 表示安装失败三条原则:
| 原则 | 为什么 |
|---|---|
| 重复执行不报错 | 用户可能装多次 |
| 先建表再写种子数据 | 顺序反了会报错 |
| 单表失败不整体中断 | 少一张表只影响一个功能 |
打不开? → 看 排错手册
第 8 步:加模板标签(可选)#
作用:让主题里能写 {notice /}。
步骤:
在 plugin.php 或 Hooks.php 里加:
$plugin->hook('app_init', function () {
$view = \app('view');
$view->tag('notice', function ($attrs, $body) {
$text = isset($attrs['text']) ? $attrs['text'] : '';
return '<span class="notice">' . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '</span>';
});
});模板里用:
{notice text="限时活动" /}属性一律是字符串,数字要自己转。
标签名不要和内置的 if/loop/include/layout 重名。第 9 步:加字段类型(可选)#
作用:在模型字段里多一种类型。
步骤:
$plugin->hook('field_types', function ($types) {
$types['rating'] = array(
'label' => '星级评分',
'render' => function ($value) {
$n = max(0, min(5, (int) $value));
return str_repeat('★', $n) . str_repeat('☆', 5 - $n);
},
'form' => function ($field, $value) {
$name = htmlspecialchars($field['name'], ENT_QUOTES, 'UTF-8');
$html = '<select class="nv-input" name="' . $name . '">';
for ($i = 0; $i <= 5; $i++) {
$html .= '<option value="' . $i . '"'
. ((string) $i === (string) $value ? ' selected' : '') . '>' . $i . ' 星</option>';
}
return $html . '</select>';
},
);
return $types;
});效果:后台模型字段管理里多出「星级评分」。
怎么调试#
| 现象 | 查哪里 |
|---|---|
| 插件加载失败 | 后台 → 插件,顶部有原因 |
| 后台页面 404 | 路由没写在 route() 回调里 |
| 保存 419 | 少了 _form_token |
| 想看路由有没有注册 | php aucms route:list | grep notice |
| 想看真实报错 | 用 server_debug.php 访问一次 |
| 改完代码先检查 | 跑下面三条 |
php .workbuddy/tools/lint_classes.php
php .workbuddy/tools/lint_methods.php
php .workbuddy/tools/lint_redirects.php还没解决?→ 看 排错手册
怎么打包发布#
作用:把插件给别人用。
步骤:
- 确认目录干净
不要把 data/plugin_config/notice.php 打进去(那是调试配置)
不要留 var_dump
plugin.json 的 version 记得 +1- 把
plugin/notice/整个文件夹压成 zip - 发给别人
对方怎么装:
| 方式 | 步骤 |
|---|---|
| 后台上传 | 插件 → 上传 zip |
| 手动拷贝 | 把目录拷进 plugin/,插件列表点「安装」 |
五条纪律#
| 纪律 | 说明 |
|---|---|
| 不改核心 | 要改行为就挂钩子 |
| 后台路由挂 AdminAuth | 超管操作再 requireSuperAdmin() |
| 输出过 htmlspecialchars | 富文本用 Security::purify() |
| 异常自己 catch | 你的 bug 不该让整站 500 |
路径参数用 $request->routeParam('id') | 不要直接拼 URL |
接下来看哪一章#
server_debug.php 复现一次,页面会直接告诉你异常文件与行号。