🔌 插件注册

ThinkAdmin 的插件注册基于 Composer 与 ThinkPHP 服务机制。插件在 composer.json 中声明服务类,执行发布流程后服务类会写入 vendor/services.php,再由 ThinkPHP 在运行时加载并执行 register() / boot()

📚 基础概念

🤔 基本介绍

插件注册主要解决三件事:让插件类可以自动加载,让插件服务参与 ThinkPHP 启动流程,让插件中心能够读取插件编码、路径、包名和菜单定义。

// 1. 插件在 composer.json 中声明 extra.think.services
// 2. php think xadmin:publish 汇总服务到 vendor/services.php
// 3. ThinkPHP 加载服务类并执行 register()/boot()
// 4. 继承 think\admin\Plugin 的服务类会记录插件元信息,并要求提供静态 menu()

🎯 核心功能

功能1:声明服务类

extra.think.services 是 ThinkPHP 服务声明。它可以指向普通 think\Service 子类,也可以指向继承 think\admin\Plugin 的插件服务类。

{
  "extra": {
    "think": {
      "services": [
        "plugin\\account\\Service"
      ]
    }
  }
}

功能2:注册启动逻辑

服务类可以在 register() 中注册命令、路由、事件监听或中间件,也可以在 boot() 中注册启动后的逻辑。具体调用时机遵循 ThinkPHP 服务机制。

public function register(): void
{
    $this->commands([Fans::class, Auto::class, Clear::class]);

    $this->app->event->listen('WechatFansSubscribe', static function ($openid) {
        AutoService::register($openid);
    });

    $this->app->route->any('/plugin-wxpay-notify/:vars', static function (Request $request) {
        // 处理支付通知
    });
}

功能3:暴露插件菜单

继承 think\admin\Plugin 的服务类必须实现静态 menu(): array。插件中心通过该方法展示插件入口;如果要把菜单写入后台菜单表,需要在数据库迁移中显式调用 PhinxExtend::write2menu()

public static function menu(): array
{
    $code = app(static::class)->appCode;
    return [
        [
            'name' => '用户管理',
            'subs' => [
                ['name' => '用户账号管理', 'node' => "{$code}/master/index"],
                ['name' => '终端账号管理', 'node' => "{$code}/device/index"],
            ],
        ],
    ];
}

🚀 主要功能

  • 服务层集成: 基于 ThinkPHP 服务层加载插件服务
  • 插件元信息: think\admin\Plugin 记录插件编码、路径、命名空间、包名和服务类
  • 菜单声明: menu() 为插件中心提供插件入口菜单
  • 扩展注册: 可注册命令、路由、事件监听和中间件
  • 包信息展示: xadmin:publish 会生成 vendor/versions.php,供插件中心读取包信息

📋 插件类型

ThinkAdmin 常见插件来源分为三类:

1. 本地插件

通过 Composer repositories.type=path 引入本地目录,适合插件开发和本地调试。插件根目录需要包含 composer.json,通常需要配置 autoload。本地插件不建议启用 extra.plugin.clear,避免源目录被清理。

当前 ThinkAdminDeveloper 根项目就是这种开发聚合模式:根 composer.json 声明多个 path 仓库指向 plugin/*,用于把各独立包放在一个工作区内联调。用户安装项目通常不需要复制这组 path 仓库配置,除非正在本地开发插件。

2. 开放插件

发布到 Packagist 或其他 Composer 仓库,通过 composer require vendor/package 安装。依赖解析、版本锁定和更新都由 Composer 处理。

3. 私有插件

可结合 ThinkAdmin 插件中心及授权平台分发,安装方式和授权能力以对应平台及插件说明为准。

⚙️ 配置要求

应用插件一般包含以下配置:

  • type: ThinkAdmin 应用插件使用 think-admin-plugin,普通开发组件可使用其他 Composer 类型。
  • name: Composer 包名,如 zoujingli/think-plugs-admin
  • autoload: PSR-4 或 files 自动加载规则。
  • extra.think.services: ThinkPHP 服务类列表,由 xadmin:publish 汇总到 vendor/services.php
  • extra.config: 插件中心展示信息,由 xadmin:publish 汇总到 vendor/versions.php
  • extra.plugin: 安装器规则,支持 initcopyclearevent 等配置。

extra.think.servicesextra.configxadmin:publish 汇总到 vendor/services.phpvendor/versions.phpextra.plugin 则由安装器在 Composer 安装/更新阶段处理。两者职责不同:前者影响运行时服务加载和插件中心展示,后者影响文件复制、初始化和清理。

本地插件示例

{
  "type": "project",
  "require": {
    "zoujingli/think-plugs-admin": "dev-master"
  },
  "repositories": {
    "ThinkAdminPlugs": {
      "type": "path",
      "url": "../think-plugs-admin"
    }
  },
  "autoload": {
    "psr-0": {
      "": "extend"
    },
    "psr-4": {
      "app\\": "app"
    }
  }
}

插件 composer 示例

下面示例来自后台管理模块的真实结构,重点展示服务声明、插件中心展示信息和安装器复制规则。

{
  "type": "think-admin-plugin",
  "name": "zoujingli/think-plugs-admin",
  "license": "MIT",
  "homepage": "https://thinkadmin.top",
  "description": "Admin backend module with system, menu, auth, file and queue management for ThinkAdmin",
  "require": {
    "php": ">7.1",
    "ext-json": "*",
    "topthink/framework": "^6.0|^8.0",
    "topthink/think-view": "^1.0|^2.0",
    "zoujingli/ip2region": "^1.0|^2.0|^3.0|@dev",
    "zoujingli/think-install": "^1.0|@dev",
    "zoujingli/think-library": "^6.1|@dev",
    "zoujingli/think-plugs-static": "^1.0|@dev"
  },
  "autoload": {
    "psr-4": {
      "app\\admin\\": "src"
    }
  },
  "extra": {
    "think": {
      "services": [
        "app\\admin\\Service"
      ]
    },
    "config": {
      "type": "module",
      "name": "系统后台管理",
      "document": "https://thinkadmin.top/plugin/think-plugs-admin.html",
      "description": "后台基础管理模块,提供系统配置、任务、日志、字典、文件、菜单、权限和用户管理。"
    },
    "plugin": {
      "copy": {
        "src": "!app/admin",
        "stc/database": "database/migrations"
      },
      "clear": true
    }
  }
}

配置说明:

  • type: think-admin-plugin 会由 think-install 安装器处理 extra.plugin 规则。
  • autoload: 定义插件类的自动加载规则;模块插件可以映射到 app\admin\app\wechat\ 等应用命名空间。
  • extra.think.services: ThinkPHP 服务声明,不限定必须继承 think\admin\Plugin;例如 Helper 插件的服务类继承普通 think\Service
  • extra.config: 插件中心展示信息,type 常见值有 modulepluginservicelibrary
  • extra.plugin.copy: 安装时复制文件,目标以 ! 开头时表示先删除目标再复制。
  • extra.plugin.clear: 仅在安装路径位于 vendor 下时清理原安装目录。

🧩 插件服务类

继承 think\admin\Plugin 的服务类会在构造时记录插件元信息,并必须实现 menu(): array。普通工具组件可以只继承 think\Service,不需要 menu()

基础属性

属性类型必填说明
$packagestring插件包名,如 zoujingli/think-plugs-admin;未设置时会从服务类上级目录内的 composer.json 自动读取
$appCodestring插件编码,未设置时按命名空间自动计算;如果命名空间以项目默认应用命名空间开头,会去掉该前缀后用 - 连接
$appNamestring插件名称,用于插件注册信息
$appPathstring插件路径,未设置时按服务类文件自动计算
$appAliasstring插件别名;非空且不同于 $appCode 时会写入 app.app_map
$appSpacestring插件命名空间,未设置时按服务类命名空间计算
$appServicestring服务注册类,默认为当前类

插件服务实例化后会写入 Plugin::get() 的内部注册表。调用 Plugin::get($code, true) 时,会再关联 vendor/versions.php 中对应包的安装展示信息;如果 $appName 为空,则会尝试使用安装信息里的名称补齐。

基础示例

<?php

declare(strict_types=1);

namespace plugin\account;

use think\admin\Plugin;

class Service extends Plugin
{
    protected $appName = '账号管理';

    protected $package = 'zoujingli/think-plugs-account';

    public static function menu(): array
    {
        $code = app(static::class)->appCode;
        return [
            [
                'name' => '用户管理',
                'subs' => [
                    ['name' => '用户账号管理', 'icon' => 'layui-icon layui-icon-user', 'node' => "{$code}/master/index"],
                    ['name' => '终端账号管理', 'icon' => 'layui-icon layui-icon-cellphone', 'node' => "{$code}/device/index"],
                    ['name' => '手机短信管理', 'icon' => 'layui-icon layui-icon-email', 'node' => "{$code}/message/index"],
                ],
            ],
        ];
    }
}

带服务注册示例

<?php

declare(strict_types=1);

namespace app\wechat;

use app\wechat\command\Auto;
use app\wechat\command\Clear;
use app\wechat\command\Fans;
use app\wechat\service\AutoService;
use app\wechat\service\PaymentService;
use think\admin\extend\CodeExtend;
use think\admin\Plugin;
use think\Request;

class Service extends Plugin
{
    protected $appName = '微信管理';

    protected $package = 'zoujingli/think-plugs-wechat';

    public function register(): void
    {
        $this->commands([Fans::class, Auto::class, Clear::class]);

        $this->app->event->listen('WechatFansSubscribe', static function ($openid) {
            AutoService::register($openid);
        });

        $this->app->route->any('/plugin-wxpay-notify/:vars', static function (Request $request) {
            try {
                $data = json_decode(CodeExtend::deSafe64($request->param('vars')), true);
                return PaymentService::notify($data);
            } catch (\Error|\Exception $exception) {
                return "Error: {$exception->getMessage()}";
            }
        });
    }

    public static function menu(): array
    {
        $code = app(static::class)->appCode;
        return [
            [
                'name' => '微信管理',
                'subs' => [
                    ['name' => '微信接口配置', 'icon' => 'layui-icon layui-icon-set', 'node' => "{$code}/config/options"],
                    ['name' => '微信支付配置', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/config/payment"],
                ],
            ],
            [
                'name' => '微信定制',
                'subs' => [
                    ['name' => '微信粉丝管理', 'icon' => 'layui-icon layui-icon-username', 'node' => "{$code}/fans/index"],
                    ['name' => '微信图文管理', 'icon' => 'layui-icon layui-icon-template-1', 'node' => "{$code}/news/index"],
                    ['name' => '微信菜单配置', 'icon' => 'layui-icon layui-icon-cellphone', 'node' => "{$code}/menu/index"],
                    ['name' => '回复规则管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/keys/index"],
                    ['name' => '关注自动回复', 'icon' => 'layui-icon layui-icon-release', 'node' => "{$code}/auto/index"],
                ],
            ],
            [
                'name' => '微信支付',
                'subs' => [
                    ['name' => '微信支付行为', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/payment.record/index"],
                    ['name' => '微信退款管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/payment.refund/index"],
                ],
            ],
        ];
    }
}

普通 ThinkPHP 服务示例

Helper 插件是开发辅助组件,composer.json 中同样声明了 extra.think.services,但服务类继承普通 think\Service,只在 boot() 中注册命令,不提供插件中心菜单。

<?php

namespace plugin\helper;

class Service extends \think\Service
{
    public function boot()
    {
        $this->commands([
            DbModelStruct::class,
            DbIndexStruct::class,
        ]);
    }
}

📌 register() 常见用途

注册命令

$this->commands([Fans::class, Auto::class, Clear::class]);
$this->commands(['xadmin:worker' => Worker::class]);

注册事件监听

$this->app->event->listen('WechatFansSubscribe', static function ($openid) {
    AutoService::register($openid);
});

注册路由

$this->app->route->any('/plugin-payment-notify/:vars', function (Request $request) {
    try {
        $data = json_decode(CodeExtend::deSafe64($request->param('vars')), true);
        return Payment::mk($data['channel'])->notify($data);
    } catch (\Error|\Exception $exception) {
        return 'Error: ' . $exception->getMessage();
    }
});

注册中间件

$this->app->middleware->add(function (Request $request, \Closure $next) {
    // 处理请求数据
    return $next($request);
}, 'route');

📌 菜单定义说明

使用 appCode 动态获取插件编码

public static function menu(): array
{
    $code = app(static::class)->appCode;
    return [
        [
            'name' => '用户管理',
            'subs' => [
                ['name' => '用户账号管理', 'node' => "{$code}/master/index"],
                ['name' => '终端账号管理', 'node' => "{$code}/device/index"],
            ],
        ],
    ];
}

合并其他插件菜单

支付插件会合并账号插件菜单,使账号相关入口和支付入口能一起展示。

<?php

namespace plugin\payment;

use plugin\account\Service as AccountService;
use think\admin\Plugin;

class Service extends Plugin
{
    protected $appName = '支付管理';

    protected $package = 'zoujingli/think-plugs-payment';

    public static function menu(): array
    {
        $code = app(static::class)->appCode;
        return array_merge(AccountService::menu(), [
            [
                'name' => '支付管理',
                'subs' => [
                    ['name' => '支付配置管理', 'node' => "{$code}/config/index"],
                    ['name' => '支付行为管理', 'node' => "{$code}/record/index"],
                    ['name' => '支付退款管理', 'node' => "{$code}/refund/index"],
                ],
            ],
        ]);
    }
}

插件中心展示与系统菜单写入

menu() 返回值由插件中心读取,用于插件入口展示和权限过滤。系统菜单表不会因为定义 menu() 自动写入;如需安装时写入后台全局菜单,应在迁移脚本中调用 PhinxExtend::write2menu()

PhinxExtend::write2menu([
    [
        'name' => '应用管理',
        'icon' => 'layui-icon layui-icon-app',
        'node' => '',
        'subs' => [
            ['name' => '插件应用管理', 'node' => 'plugin-center/index/index'],
        ],
    ],
]);

⚠️ 注意事项

  • extra.think.services 需要配合 php think xadmin:publish 生成 vendor/services.php
  • think\admin\Plugin 服务类必须实现 menu(): array,没有菜单的服务也应返回空数组。
  • 当前 think\admin\Plugin 没有 $this->menus() 方法,菜单通过静态 menu() 方法声明。
  • extra.config 写入 vendor/versions.php,用于插件中心展示名称、类型、授权、文档和描述等信息。
  • 菜单节点建议使用 app(static::class)->appCodeself::getAppCode() 拼接,避免插件编码调整后节点失配。
最近更新:
Contributors: 邹景立, Anyon