Search by

kingbes / annotation

KingBes

Thinkphp plugin kingbes/annotation

Package info

github.com/KingBes/annotation

pkg:composer/kingbes/annotation

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.2 2026-09-09 02:23 UTC

This package is auto-updated.

Last update: 2026-09-09 02:24:27 UTC


README

基于 ThinkPHP 8 的 PHP 8 原生 注解 路由方案。

使用一个通用的 #[Annotation] 注解即可声明路由、请求方法、中间件、路由名,同时兼容单应用与多应用的目录结构。

环境要求

  • PHP >= 8.0
  • ThinkPHP 8

安装

composer require kingbes/annotation

安装完成后,插件会自动注册 RouteLoaded 事件监听,无需手动绑定。

配置

插件通过 src/config.php 提供配置(由 composer 的 extra.think.config 自动加载,无需手动发布):

配置 类型 默认值 说明
enable bool true 总开关,false 时关闭注解路由
controllers array [] 额外扫描的控制器目录(绝对路径,或相对项目根的路径)
cache bool 跟随 app_debug 扫描结果缓存。调试模式默认关闭,生产默认开启;显式设置可强制开关

默认扫描目录为应用的控制器目录(app/**/controller)。如需覆盖配置,在项目 config/annotation.php 中写同名键即可(ThinkPHP 会自动合并):

// config/annotation.php
return [
    'enable' => true,
    'controllers' => [],
];

controllers 中的目录以该目录自身作为路由前缀的推导基准,因此可以指向任意位置(不要求在当前应用目录内)。例如把 app/index/controller 填进去,其中 Index::index 推导出的前缀就是 /index,而不是磁盘绝对路径。

快速开始

在控制器类或方法上使用 #[Annotation([...])] 声明路由信息。

<?php

namespace app\controller;

use Kingbes\Annotation\Annotation;

#[Annotation([
    'title' => '首页',
])]
class User
{
    // GET /user/get_list
    #[Annotation([
        'path' => [],
        'title' => '用户列表',
        'request' => ['GET', 'POST'],
        'middleware' => ['CheckAuth'],
        'name' => 'user.getList',
    ])]
    public function getList()
    {
        return 'user list';
    }

    public function index()
    {
        return 'index';
    }
}

说明:

  • User 对应类名去掉尾部 Controller 后再做 大驼峰转下划线 的控制器名,即 UserControlleruser
  • 若你的类本身就叫 UserController(未配置 route.controller_suffix),默认会自动去掉尾部字面量 Controller;若你配置了 route.controller_suffix = 'Controller',则按配置为准。
  • 上面的类名为 User、方法名为 getList 时,默认路由为 /user/get_list

注解参数

参数 类型 默认值 说明
title string 路由标题/说明(仅注释性质)
path array [] 自定义附加路由路径,会额外注册
request array string ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']
middleware array string []
name string 生成的点号路径(如 user.get_list 自定义路由名;未设置时默认用「路径去首尾 / 并把 / 换成 .」,空路径(如 / 别名)不设名

注意:name 会作为路由标识(route_name);path / request / middleware / name 参与路由注册与解析。

自定义属性

#[Annotation] 接收任意数组,除上述标准键外,其它自定义键会原样保存,方便携带权限标识、业务标记等自定义数据(仅记录,不参与路由逻辑)。例如:

#[Annotation([
    'title' => '删除用户',
    'auth'  => 'admin',
    'other' => ['Asd'],   // 自定义键,会保留在 Data 中
])]
public function delete()
{
}

访问全部注解信息仍使用 Data::$data / Data::$routemethods 内会包含这些自定义键。

注意:注解数组键值必须使用 => 语法(原生 PHP Attribute 只接受常量表达式,键与值请写在同一数组字面量中)。

可重复注解

#[Annotation] 声明为 IS_REPEATABLE,可以在同一个类或方法上叠加多个注解,扫描时会自动合并。

  • 标量键:后者的值覆盖前者。
  • path 键:多次书写会以追加方式合并。
#[Annotation(['request' => ['GET']])]
#[Annotation(['path' => ['/user/list']])]
public function getList() {}

获取全部注解信息

通过 Data 类获取扫描到的所有注解信息:

use Kingbes\Annotation\Data;

Data::$data;   // 全部注解数据(class / name / path / methods)
Data::$route;  // 路由数据(同上,供路由注册使用)

控制器项结构:

[
    'class'   => 'app\admin\controller\Role', // 完整类名
    'name'    => 'role',                      // 控制器名(下划线)
    'path'    => '/role',                     // 路由前缀(不含应用名)
    'app'     => 'admin',                     // 所属应用名段(单应用为空字符串)
    'title'   => '角色管理',                   // 类级 #[Annotation] 的自定义键,平铺在此
    'auth'    => true,                        // 同上
    'methods' => [ /* 见下 */ ],
]

类级注解的自定义键会平铺到控制器项class / name / path / app / methods 这几个保留键不会被覆盖),因此 #[Annotation(['title' => '角色管理'])] 可以直接用 $item['title'] 读取,常用于菜单、权限树等按控制器分组的场景。

methods 中每项为类级与方法级注解合并后的结果,包含 path / request / middleware / name(PHP 方法名)/ route_name(自定义 name,未设置时无此键),以及全部自定义键。方法级未写的键会继承类级的值:

#[Annotation(['title' => '角色管理', 'auth' => true])]   // 类级
class Role
{
    #[Annotation(['title' => '列表', 'menu' => true])]   // 方法级
    public function view() {}
}
  • 控制器项:title = '角色管理'auth = true
  • view 方法:title = '列表'(方法级覆盖)、auth = true(继承自类级)、menu = true

默认路由规则

默认路径由「控制器文件在当前应用内相对 controller_layer 的目录段 + 控制器名 + 方法名」推导而来,controller_layer(默认 controller)及之前的段会被剔除;子目录段逐段转下划线后拼接。路由规则本身不含应用名

  • 单应用:扫描当前应用的 controller 目录,无需应用段。
  • 多应用(topthink/think-multi-app):每个应用只扫描并注册自己的控制器,规则不带应用名;实际访问 URL 由多应用机制自动带上应用段。
  • 路由标识 name 在多应用下会带上应用段装饰(如 admin.user.get_list),可用 url('admin.user.get_list') 生成 URL。

单应用

注解位置 默认路由规则 默认 name
app/controller/UserController::getList /user/get_list user.get_list
app/controller/Index::index /index/index index.index

多应用

注解位置 注册的路由规则 实际访问 URL 默认 name
app/index/controller/Diary::index /diary/index /index/diary/index index.diary.index
app/admin/controller/UserController::getList /user/get_list /admin/user/get_list admin.user.get_list

index 别名

index 方法会自动额外注册一条去掉末尾连续 /index 段的别名路径。例如:

  • Index::index:主路由 /index/index,别名 /index
  • app/index/controller/Diary::index:主路由 /diary/index,别名 /diary。访问 /index/diary/index/diary/index

跨应用 URL 生成

ThinkPHP 多应用(topthink/think-multi-app)下,url('跨应用.路由名') 会被框架强制加上「当前应用」前缀。例如当前在 install 应用时调用 url('admin.login.index'),框架会用 rule 路径 /login/index 拼上当前应用名,错误生成为 /install/login/index

插件提供 Kingbes\Annotation\Url::build(),分两步处理:

  1. 跨应用路由名(首段是其它应用目录,如 admin.login.index)→ 转为绝对路径 /admin/login/index,走框架绝对路径分支
  2. 全局路由名(首段不是应用目录,如 files.download)→ 生成后去掉框架强加的当前应用前缀

要全局生效,请在应用公共文件 common.php 中覆盖 url()

<?php
// 位于 应用/common.php 或 项目根/common.php
if (!function_exists('url')) {
    function url(string $url = '', array $vars = [], $suffix = true, $domain = false)
    {
        return \Kingbes\Annotation\Url::build($url, $vars, $suffix, $domain);
    }
}

覆盖生效前提:该 common.php先于框架 helper.php 加载。ThinkPHP 的 App::load()include 应用 common.php、再 include 框架 helper.php,因此应用 common.php 中的定义会优先生效。这个由 common.php 放置位置决定:

  • 单应用:app/common.php
  • 多应用:默认只会加载当前应用目录下的 common.php,若每个应用都要用,可在每个应用放一份,或放到各应用共同引入的位置。

覆盖后,以下调用均正确:

调用 生成
url('admin.login.index') /admin/login/index(跨应用,转绝对路径)
url('admin.index.menu') /admin/index/menu(跨应用)
url('files.download', ['id'=>1, 'ext'=>'png']) /files/1.png(全局路由名,去掉应用前缀)
url('/admin/login/index') /admin/login/index(绝对路径本就正确)
当前应用下的 url('admin.index.menu') admin 应用内调用时维持框架原行为

多应用下全局路由文件加载

think-multi-app 会把 Http::loadRoutes() 的路由目录改为 app/{应用}/route/,导致项目根 route/*.php(如自定义的 route/app.php不会在 HTTP 请求中加载,其中定义的路由名(如 files.download)无法用 url() 生成。

插件在 RouteLoaded 事件中自动补加载全局 route/*.php(用 include_once,与框架已加载的场景不冲突),使全局路由名在请求中可用。php think route:list 能看到但请求中用不到的场景即由此修复。

多应用下 URL 生成注意

  • 跨应用链接:url('admin.login.index') 由插件自动转 /admin/login/index,无影响。
  • 全局路由名:url('files.download', [...]) 由插件去掉框架强加的当前应用前缀,生成 /files/...
  • 以上均需先按上文在应用 common.php 覆盖 url()

测试 / 调试

查看注解生成的路由:

php think route:list

路由有改动后可清缓存:

php think clear

扫描缓存

注解扫描结果会缓存到 runtime/annotation.php,缓存是否失效由「控制器文件清单 + 各自的 mtime + controller_layer / controller_suffix 配置」共同决定,改动任一文件都会自动重扫,无需手动清理。

  • 未配置 cache 时跟随 app_debug:调试模式关闭缓存(改完注解立即生效),生产模式开启。
  • 显式配置 cache 可强制开关。
  • 仍可用 php think clear 清空 runtime 强制重建。

限制

  • 仅扫描 app/ 下映射到基础命名空间(默认 app)的控制器文件。
  • controllers 额外目录可以放在任意位置,但其中定义的类必须能被 Composer 自动加载,否则会被跳过。
  • 注解数组键值必须使用 => 语法。
  • 仅扫描公共方法,忽略 __construct / __destruct,忽略抽象类、接口与 trait。
  • 每个文件只取第一个具名类(跳过匿名类与 Foo::class 中的 class 关键字)。
  • 多应用下,php think route:list 与真实 HTTP 请求使用同一套规则(规则本身不含应用名,由多应用机制补上应用段)。