插件开发

从 plugin.json 到打包发布,用一个能跑通的「公告条」插件走完全流程:路由、后台页、配置、钩子、数据表、模板标签。

约 28 分钟 扩展 代码块右上角可一键复制
本页目录(28 分钟读完)

这一章只做一件事:带你写出一个能跑的插件。

给会 PHP 的人看。每步都是「建什么文件、贴什么代码、跑什么命令」。 页面打不开 → 看 排错手册。

插件是什么#

一句话:一个目录 + 一个入口文件 + 一堆注册动作。

注册什么写在哪作用
钩子plugin.php挂到系统事件上
路由plugin.php加自己的页面
菜单plugin.php后台左侧入口
配置plugin.json后台设置表单
数据表tables.php存自己的数据

不改核心代码。插件删掉就干净。

第 1 步:建目录#

作用:先有地方放代码。

步骤:

  1. 进 plugin/ 目录
  2. 新建 notice 文件夹(这是插件标识)
  3. 里面建这几个文件
文本
plugin/notice/
├── plugin.json                ← 必须
├── plugin.php                 ← 必须
├── src/
│   ├── Hooks.php
│   └── AdminController.php
└── view/default/
    └── admin/plugin/notice/index.html

命名规则:

目录名命名空间
noticePlugin\Notice\
member-generatorPlugin\MemberGenerator\
multi-sitePlugin\MultiSite\

规则:短横线去掉,每个单词首字母大写。

打不开? → 看 排错手册

第 2 步:写 plugin.json#

作用:告诉系统这个插件叫什么、有什么配置。

步骤:

  1. 新建 plugin/notice/plugin.json
  2. 贴下面的内容
  3. 保存
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否配置默认值

依赖别的插件:

JSON
"require": { "plugins": ["member"] }

依赖不满足时,插件列表显示「依赖不满足」,拒绝启用。

打不开? → 看 排错手册

第 3 步:写 plugin.php#

作用:注册钩子、路由、菜单。这是唯一入口。

步骤:

  1. 新建 plugin/notice/plugin.php
  2. 贴下面的内容
  3. 保存
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_initrun每次请求开始
after_dispatchrun响应发出前
plugin_bootedrun所有插件加载完
content_createrun内容新建后
content_updaterun内容更新后
content_deleterun内容删除后
content_detailfilter详情装配后
view_renderfilter页面渲染完
admin_menufilter后台菜单生成
field_typesfilter字段类型注册
upload_afterrun上传完成
user_registerrun注册成功
user_login_successrun前台登录成功
schedule_tasksfilter调度任务注册
文本
不要凭白名单写代码。
要找钩子,去源码里搜 hook('名字')。

写 Hooks.php:

  1. 新建 plugin/notice/src/Hooks.php
  2. 贴下面的内容
  3. 保存
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="关闭">&times;</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 写控制器

  1. 新建 plugin/notice/src/AdminController.php
  2. 贴下面的内容
  3. 保存
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 写模板

  1. 新建 plugin/notice/view/default/admin/plugin/notice/index.html
  2. 贴下面的内容
  3. 保存
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 步:启用测试#

作用:让插件跑起来。

步骤:

  1. 启用插件
Shell
php aucms plugin:enable notice
  1. 重建缓存
Shell
php aucms cache:rebuild
  1. 前台随便开一页,顶部有公告条
  2. 后台左侧「插件」组下有「公告条」菜单
  3. 点进去改文字,保存,刷新前台看变化

打不开? → 看 排错手册

第 7 步:加数据表(可选)#

作用:要存数据时才建表。纯展示插件跳过这步。

步骤:

7.1 写 tables.php

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
<?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 里加:

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>';
    });
});

模板里用:

HTML
{notice text="限时活动" /}
文本
属性一律是字符串,数字要自己转。
标签名不要和内置的 if/loop/include/layout 重名。

第 9 步:加字段类型(可选)#

作用:在模型字段里多一种类型。

步骤:

PHP
$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 访问一次
改完代码先检查跑下面三条
Shell
php .workbuddy/tools/lint_classes.php
php .workbuddy/tools/lint_methods.php
php .workbuddy/tools/lint_redirects.php

还没解决?→ 看 排错手册

怎么打包发布#

作用:把插件给别人用。

步骤:

  1. 确认目录干净
文本
不要把 data/plugin_config/notice.php 打进去(那是调试配置)
不要留 var_dump
plugin.json 的 version 记得 +1
  1. 把 plugin/notice/ 整个文件夹压成 zip
  2. 发给别人

对方怎么装:

方式步骤
后台上传插件 → 上传 zip
手动拷贝把目录拷进 plugin/,插件列表点「安装」

五条纪律#

纪律说明
不改核心要改行为就挂钩子
后台路由挂 AdminAuth超管操作再 requireSuperAdmin()
输出过 htmlspecialchars富文本用 Security::purify()
异常自己 catch你的 bug 不该让整站 500
路径参数用 $request->routeParam('id')不要直接拼 URL

接下来看哪一章#

我想…去看
看真实项目怎么挂钩子读 plugin/tag 源码
做站群/多语言多站点与多语言
接第三方登录读 plugin/socialauth 源码
插件报错排错手册
没找到答案? 在后台「缓存与维护 → 系统体检」里看环境是否正常;仍无解就打开 server_debug.php 复现一次,页面会直接告诉你异常文件与行号。