Route Forge — 功能规格说明书

对外功能承诺,所有实现都应对照此文档验证。

1. 产品概述

Route Forge for Laravel 是 Laravel 命名路由的全链路后端解决方案,提供:

2. 目标用户

画像 典型场景
Laravel 全栈开发者 Laravel + Vue/React SPA 项目,想让前端调用 API 更优雅
大型 SPA 项目维护者 接口数 > 100,首屏性能敏感
多角色系统开发者 有 admin/user/guest 等多种权限级别
TypeScript 重度用户 要求前后端全链路类型安全,需要后端生成 TS 类型声明

3. 后端功能

3.1 核心能力

Route Forge 提供三种互相兼容的路由层级分配方式,任选其一或组合使用。三层级名(如 admin/manage/clientpublic/user/admin )完全由项目自定义,包不预设固定层级(见 DESIGN.md §6.2)。

3.1.1 定义路由时显式标记(->tier() 宏)

通过宏 tier() 在定义路由时显式标记所属层级,链式调用对资源路由同样生效:

Route::post('/auth/login', [AuthController::class, 'login'])
    ->name('auth.login')
    ->tier('public');

Route::resource('users', UserController::class)
    ->tier('manage');

Route::apiResource('posts', PostController::class)
    ->tier('manage');

Route::singleton('profile', ProfileController::class)
    ->tier('client');

宏通过 ServiceProvider 注册到 Illuminate\Routing\Route,仅向 action 数组写入一个 tier 字段,零侵入:

Route::macro('tier', function (string $tier) {
    $this->action['tier'] = $tier;
    return $this;
});

资源路由(resource / apiResource / singleton / apiSingleton)返回的 Pending 注册对象同样注册了 tier() 宏:tier 暂存于资源 options,注册时经 ForgeResourceRegistrar 写入每条资源路由的 action,语义与单条路由的显式 ->tier() 完全一致(包括「显式标注优先于 group 透传」,见 §3.1.4)。非法层级名在链式调用时立即抛 UnknownLevelException

3.1.2 配置文件按规则批量分配

config/forge.php 中按 match 规则把路由批量匹配到对应层级,无需逐条标注:

// config/forge.php
return [
    'levels' => [
        'public' => [
            'description' => '公共接口(无需登录)',
            'match' => [
                'prefix'     => ['auth', 'public'],
                'middleware' => [],
            ],
            'load'  => 'eager',
        ],
        'client' => [
            'description' => '客户端用户接口',
            'match' => [
                'prefix'     => ['client'],
                'middleware' => ['auth'],
            ],
            'load'  => 'lazy',
        ],
        'manage' => [
            'description' => '运营管理接口',
            'match' => [
                'prefix'     => ['manage'],
                'middleware' => ['auth', 'manage'],
            ],
            'load'  => 'lazy',
        ],
        'admin' => [
            'description' => '系统管理接口',
            'match' => [
                'prefix'     => ['admin'],
                'middleware' => ['auth', 'admin'],
                'middleware_match' => 'all',  // 要求同时包含 auth 和 admin
            ],
            'load'  => 'lazy',
            'endpoint_middleware' => ['auth', 'admin'],  // 访问 /_forge/routes/admin 需要 auth + admin
        ],
    ],

    'endpoint_prefix' => '/_forge/routes',  // 路由元信息对外端点前缀
    'endpoint_middleware' => [],              // 摘要端点中间件,null 或空数组 = 不限制
    'cache_ttl'       => 3600,              // 统一缓存 TTL(秒),null=不缓存
    'cache_driver'    => null,              // null=使用默认缓存驱动
    'strict_mode'     => false,             // 严格模式:未命中层级即抛异常
    'classifier'      => null,              // 自定义分类回调,签名 fn(Route $r): ?string
];

匹配规则:

⚠️ unassigned 是保留层级名:它是未命中任何层级的路由的兜底特殊层级(见 §3.1.4 / §3.1.5),不能用作 levels 中的自定义层级名。若在 levels 中定义了名为 unassigned 的层级,它会被当作特殊层级处理——其 matchdescriptionendpoint_middleware 等配置全部失效且无任何提示,请勿使用。

中间件匹配模式(middleware_match

每个层级可在 match 中配置 middleware_match,控制 middleware 数组的匹配逻辑。不配置时默认 'any'

简单模式:

含义
'any'(默认) 路由中间件集合包含 middleware 数组中任意一项即命中(OR 逻辑)
'all' 路由中间件集合包含 middleware 数组中全部项才命中(AND 逻辑)

高级模式(数组 DNF 结构):

middleware 数组索引(从 0 开始)为操作数,用嵌套数组表达布尔逻辑——内层数组为 AND,外层数组为 OR(析取范式 / DNF):

配置示例 等价逻辑
[[0, 1]] middleware[0] AND middleware[1]
[[0], [1]] middleware[0] OR middleware[1]
[[0, 1], [2]] (middleware[0] AND middleware[1]) OR middleware[2]
[[0], [1, 2]] middleware[0] OR (middleware[1] AND middleware[2])
// 示例 1:admin 层级要求同时有 auth 和 admin 中间件(AND)
'admin' => [
    'match' => [
        'prefix'     => ['admin'],
        'middleware' => ['auth', 'admin'],
        'middleware_match' => 'all',
    ],
    'load' => 'lazy',
],

// 示例 2:复杂条件 (有 auth 且有 admin) 或 (有 super_admin)
'admin' => [
    'match' => [
        'prefix'     => ['admin'],
        'middleware' => ['auth', 'admin', 'super_admin'],
        'middleware_match' => [[0, 1], [2]],
    ],
    'load' => 'lazy',
],

选择 DNF 数组而非字符串表达式的原因:PHP 原生数组无需额外解析器,写错时 PHP 直接报类型错误,不会静默失败。DNF 可表达所有布尔组合,覆盖实际使用场景。

3.1.3 路由分组分配层级(Route::group 透传)

Route::group 上支持 tier 选项,整组路由继承层级,避免重复标注。命名空间、中间件、前缀等 Laravel 原生 group 选项继续正常工作:

Route::group([
    'prefix'     => 'admin',
    'middleware' => ['auth', 'admin'],
    'tier'       => 'admin',   // ← 新增选项
], function () {
    Route::get('/users', [AdminUserController::class, 'index'])
         ->name('admin.users.index');
    Route::post('/users', [AdminUserController::class, 'store'])
         ->name('admin.users.store');
    // 组内所有路由自动归属 admin 层级
});

嵌套 group 行为(与 Laravel 中 middleware 合并策略一致):

Route::group(['tier' => 'admin'], function () {
    Route::get('/a', ...);              // admin

    Route::group(['tier' => 'manage'], function () {
        Route::get('/b', ...);          // manage(内层覆盖外层)
    });
});

实现方式:register 阶段把容器中的 router 单例重绑为 ForgeRouter(继承 Laravel Router,覆盖 group 属性合并逻辑),tier 透传到组内每条路由的 action,等价于自动给组内每条路由调用 ->tier()。分组标记与单条显式 ->tier() 相比,单条优先级更高。

⚠️ 尾部链式写法不受支持Route::group([...], fn)->tier('x') 不会将 tier 应用于该组。 Laravel 的 group() 返回时组内路由已注册完毕、组属性已出栈,其后链式创建的 Registrar 属性无消费方, 会被静默丢弃。Forge 对此场景做运行时检测:持有属性却从未注册 group/路由的 Registrar 被销毁时, 向 Laravel 日志写入一条告警(含被丢弃的属性名与正确写法提示)——默认 warning 级别; strict_mode=true 时升级为 error 级别(不在析构中抛异常:PHP 析构期间抛异常在栈展开场景会 直接致命错误且无法被 catch,日志告警更可靠;DiscardedRegistrarAttributesException / RF_BE_007 保留供兼容,不再自动抛出)。 组级 tier 只支持两种写法:数组选项 Route::group(['tier' => ...], fn) 与前置链式 Route::tier(...)->group(fn)

3.1.4 层级分配优先级

当多种分配方式并存时,按以下优先级决定一条路由的最终层级(高优先级覆盖低优先级):

  1. 显式 ->tier() 调用(最高):直接返回,不继续后续匹配
  2. Route::group tier 选项(继承自最近一层 group,内层覆盖外层)
  3. classifier 自定义回调返回非 null 值
  4. 配置文件 match 规则匹配(受 middleware_match 控制)
  5. 兜底:归入 unassigned 特殊层级(经层级端点获取,见 §3.1.5 / §3.1.6)

优先级设计意图:显式标注胜过隐式分组,分组胜过全局规则,全局规则胜过兜底。 未标记路由仍可被前端调用,只需通过摘要端点中的 unassigned 特殊层级发现, 并经其层级端点(GET /{endpoint_prefix}/unassigned)获取明细。 兜底机制只有 unassigned 一种(早期版本的 fallback_level 配置因与之语义重复已移除)。

⚠️ 安全考量:未标记路由会通过 unassigned 层级端点暴露路由名和 URI 模板。生产环境建议配合 strict_mode=true 或显式标记所有路由,避免信息泄露。

3.1.5 层级元信息查询端点

包在 endpoint_prefix(默认 /_forge/routes)下注册端点,按层级返回路由元信息(名称 + URI + method + 参数定义),供前端按需懒加载:

GET /_forge/routes/{level}   # 返回该层级下所有命名路由的元信息

{level} 支持特殊值 unassigned:返回所有未命中任何层级的命名路由,响应结构与已定义层级完全一致。

内部路由排除规则:包自身端点路由(forge.routes.* / forge.manager.*)与框架内部路由(如 Laravel 12+ 的 storage.*)不属于用户业务路由,在所有元信息端点扫描中一律排除——不出现在任何层级端点、unassigned 明细、摘要 route_count 统计中,与 route:forge:list / route:forge:types / 管理器页面的过滤口径一致。这一规则同时保证 strict_mode=true 时包自身路由(永远不带 tier)不会触发 RF_BE_001,严格模式仅校验用户业务路由。

另:routes 字段为按路由名索引的对象;层级下无路由时序列化为 {}(而非 [])。

返回示例:

{
  "level": "admin",
  "routes": {
    "admin.users.index": {
      "uri": "admin/users",
      "methods": [
        "GET",
        "HEAD"
      ],
      "parameters": [],
      "parameter_defaults": {}
    },
    "admin.users.show": {
      "uri": "admin/users/{user}",
      "methods": [
        "GET",
        "HEAD"
      ],
      "parameters": [
        "user"
      ],
      "parameter_defaults": {}
    },
    "admin.posts.index": {
      "uri": "admin/posts/{page?}",
      "methods": [
        "GET",
        "HEAD"
      ],
      "parameters": [
        "page"
      ],
      "parameter_defaults": {
        "page": "1"
      }
    }
  }
}

缓存:所有层级端点与摘要端点统一使用 cache_ttl 配置项控制 TTL(单位秒,null 不缓存,0 永久缓存)。

⚠ cache_ttl: 0 遵循 Laravel Cache TTL 惯例(永久缓存),非 HTTP Cache-Control: max-age=0 含义。Route Forge 缓存仅通过包内部管理(Cache facade / 配置的 cache_driver),不使用 HTTP 响应头。

端点中间件保护(endpoint_middleware

每个层级可配置 endpoint_middleware 字段,控制访问该层级元信息端点时要求的中间件。未配置或为空时不限制访问:

'admin' => [
    // ... match / load 配置 ...
    'endpoint_middleware' => ['auth', 'admin'],  // GET /_forge/routes/admin 需要 auth + admin 中间件
],
'public' => [
    // ... match / load 配置 ...
    // 未配置 endpoint_middleware → GET /_forge/routes/public 无中间件限制
],

摘要端点(GET /_forge/routes)受顶层 endpoint_middleware 配置保护(见 §5)。

实现方式:按层级注册独立路由(方案 B),每个层级路由直接挂载对应的中间件。未配置 endpoint_middleware 的层级不附加任何中间件,保持原有行为。

3.1.6 层级摘要端点

包同时注册一个摘要端点,返回所有层级的概览信息与后端全局配置,供前端初始化时自动发现层级、读取后端配置:

GET /_forge/routes   # 返回所有层级摘要 + 全局配置

返回示例:

{
  "schemeVersion": 1,
  "levels": {
    "public": {
      "description": "公共接口(无需登录)",
      "load": "eager",
      "route_count": 12,
      "route": {
        "uri": "/_forge/routes/public",
        "methods": ["GET", "HEAD"]
      }
    },
    "client": {
      "description": "客户端用户接口",
      "load": "lazy",
      "route_count": 45,
      "route": {
        "uri": "/_forge/routes/client",
        "methods": ["GET", "HEAD"]
      }
    },
    "manage": {
      "description": "运营管理接口",
      "load": "lazy",
      "route_count": 38,
      "route": {
        "uri": "/_forge/routes/manage",
        "methods": ["GET", "HEAD"]
      }
    },
    "admin": {
      "description": "系统管理接口",
      "load": "lazy",
      "route_count": 27,
      "route": {
        "uri": "/_forge/routes/admin",
        "methods": ["GET", "HEAD"]
      }
    },
    "unassigned": {
      "description": "未命中任何层级的路由",
      "load": "lazy",
      "route_count": 1,
      "route": {
        "uri": "/_forge/routes/unassigned",
        "methods": ["GET", "HEAD"]
      }
    }
  },
  "config": {
    "strict_mode": false,
    "endpoint_prefix": "/_forge/routes",
    "url_prefix": "https://api.example.com/v1",
    "cache_ttl": 3600
  }
}

字段说明:

摘要端点同样受 cache_drivercache_ttl 控制缓存。

3.1.7 路由别名(forgeAlias / aliases

路由改名迭代(如 admin.users.indexadmin.members.index)时,别名机制让前端调用方无需修改路由名即可继续工作:别名作为额外键注入目标路由所在层级的元信息,与真实路由名并存,指向完全一致的元信息。

两种用法定位(均可长期使用):

  1. 改名迁移过渡:改名后声明旧名,前端零改动;改名稳定后清理别名,恢复单名。
  2. 长期稳定对外名:给易变路由固定一个对外调用名(键),真实路由名仅作内部实现。 此后路由改名、甚至跨层级迁移,只需更新别名映射的目标值,前端永不感知。 推荐约定:前端代码统一只调用别名,真实路由名不出现在前端代码中。
// 长期稳定对外名示例:前端永远调用 client.orders.list
// aliases: ['client.orders.list' => 'client.orders.index']

// 需求迭代把路由改名为 v2、并迁移到 manage 层级——前端零改动:
Route::put('/manage/orders/v2/{id}', [OrderController::class, 'updateV2'])
    ->name('manage.orders.update')
    ->tier('manage');
// 仅更新一行映射即可:
// aliases: ['client.orders.list' => 'manage.orders.update']

声明通道(二选一或并用):

// 通道一:路由宏(显式,优先级高于 config)——改名时在路由定义处就近声明
Route::get('/admin/members', [MemberController::class, 'index'])
    ->name('admin.members.index')
    ->tier('admin')
    ->forgeAlias('admin.users.index');          // 可一次声明多个旧名
// 通道二:config/forge.php 集中声明(批量迁移与长期稳定对外名都建议集中在此维护)
'aliases' => [
    'admin.users.index' => 'admin.members.index',   // 键=别名(旧名),值=真实路由名(新名)
],

合并与冲突规则(对齐 §3.1.4 的显式 > 配置哲学):

行为细节:

清理时机只针对用法一:迁移过渡型别名应在改名稳定后清理(route:forge:list --aliases 查看、grep forgeAlias);用法二的长期稳定对外名不需要也不会过期——它是路由名与对外契约之间的解耦层。schemeVersion 不因别名递增——routes 多出条目对既有前端无破坏,属向后兼容增量。

3.1.8 首页内嵌摘要(Blade 指令 @forgeSummary

对「Laravel 服务端渲染 HTML、JS 只在浏览器执行」的首页,可选地把摘要端点的返回值直接内嵌进 HTML,让 @route-forge/core 在浏览器初始化时读内嵌数据、跳过首屏的一次摘要 HTTP 往返。这是摘要端点之外的可选投递方式,不改变既有端点契约(DESIGN §6.3)。

用法:在首页/布局的 <head> 内、早于前端 bundle 处书写指令:

<head>
    {{-- ... --}}
    @forgeSummary
</head>

指令输出一段 <script>,以一次性、消费即自删、不可枚举window 访问器暴露摘要:

<script>
Object.defineProperty(window, '__ROUTE_FORGE__', {
  configurable: true,
  enumerable: false,
  get: function () {
    var v = JSON.parse('…');   // 摘要 JSON,经 Js::from 做 script-safe 转义
    delete window.__ROUTE_FORGE__;
    return v;
  }
});
</script>

契约与约束

安全边界(如实说明):一次性自删只缩小数据在 window 上的运行时驻留面;摘要数据仍随 HTML 源码可见,非抗 XSS / 抗网络窃取的硬边界。XSS 一旦可在消费前执行脚本即可读取该值。文档与实现均不得将其夸称为加密或安全机制。

3.2 Artisan命令

php artisan route:forge:list

查看所有路由的层级分配结果,用于开发调试和验证配置是否正确:

# 查看所有路由的层级分配
php artisan route:forge:list
# 仅查看指定层级
php artisan route:forge:list --level=admin
# JSON 格式输出(便于脚本处理)
php artisan route:forge:list --json
# 仅显示未分配层级的路由
php artisan route:forge:list --unassigned
# 仅显示别名条目(旧名 → 真实路由名,见 §3.1.7)
php artisan route:forge:list --aliases

输出示例:

Name/Alias Level Methods URI Alias Of
auth.login public POST auth/login
admin.users.index admin GET|HEAD admin/members admin.members.index
admin.members.index admin GET|HEAD admin/members
client.orders.store client POST client/orders
debug.info unassigned GET|HEAD _debug/info

表格说明:Name/Alias 列中,别名整行以黄色显示;真实路由行保持默认颜色,但被别名 依赖的真实路由名以绿色显示(改名时需同步更新别名映射的审计信号);被忽略的撞车声明 以红色行显示在表格末尾(该行不代表可用路由名,不进入 JSON routes)。颜色仅 table 模式 存在,--json 输出保持纯文本。别名行与真实路由行的 URI 相同——别名在元信息中就是 一个真实存在的路由名(§3.1.7 契约),Alias Of 只是给人看的辅助标记。

行为说明:

--json 输出结构

--json 输出结构化对象(便于脚本消费),而非纯数组:

{
  "levels": ["public", "client", "manage", "admin", "unassigned"],
  "filter": null,
  "count": 5,
  "tier_counts": {"public": 1, "client": 1, "manage": 0, "admin": 2, "unassigned": 1},
  "warnings": [],
  "routes": [
    {
      "name": "auth.login",
      "level": "public",
      "methods": ["POST"],
      "uri": "auth/login",
      "alias_of": null
    },
    {
      "name": "admin.users.index",
      "level": "admin",
      "methods": ["GET"],
      "uri": "admin/members",
      "alias_of": "admin.members.index"
    },
    {
      "name": "debug.info",
      "level": "unassigned",
      "methods": ["GET"],
      "uri": "_debug/info",
      "alias_of": null
    }
  ]
}

字段说明:

alias_of 语义(从别名视角命名):本行 name 是一个别名时,alias_of 即它所指向的真实路由名。 换言之:Name/Alias 列回答「这一行叫什么」,Alias Of 列回答「若它是别名,真身是谁」。 该字段仅存在于 list --json 与管理器数据中,端点元信息(routes 对象)没有此字段—— 别名与真实路由在元信息层不可区分,是「前端零改动」契约的前提。

设计意图:开发阶段最常被问到的问题是"我的路由到底被分到了哪个层级"。这个命令让开发者无需启动前端、无需打开浏览器,一条命令即可验证配置效果。

php artisan route:forge:types

从 Laravel 路由注册表生成 TS 类型声明文件,为前端 forge.api() 调用提供编译期类型安全:

# 生成所有层级的路由类型(默认输出到 stdout,便于预览和管道处理)
php artisan route:forge:types
# 仅生成指定层级
php artisan route:forge:types --level=admin
# 写入文件(跨项目写入前端目录)
php artisan route:forge:types --out=../frontend/src/types/forge-routes.d.ts
# JSON 格式输出(便于脚本或工具链二次消费)
php artisan route:forge:types --json

行为说明:

生成结果示例(d.ts):

// AUTO-GENERATED by route:forge:types. Do not edit.
// 生成时间: 2026-08-22T12:00:00.000Z
// 端点: /_forge/routes
import { ForgeRouteMap } from "@route-forge/core";

// ─── 层级名联合类型 ───────────────────────────────────────────
export type ForgeLevel = 'public' | 'manage' | 'admin';

// ─── 各层级路由名联合类型 ─────────────────────────────────────
export type ForgeRouteName<L extends ForgeLevel> = L extends keyof ForgeRouteMap
  ? keyof ForgeRouteMap[L] & string
  : never;

// ─── 单条路由元信息(端点返回的 routes 值结构) ────────────────────
export interface ForgeRouteMeta {
  method: string;
  uri: string;
  parameters: string[];
  parameter_defaults: Record<string, unknown>;
}

// ─── 按层级 → 路由名 → 类型约束的映射 ────────────────────────
// 通过 module augmentation 增强 @route-forge/core 的 ForgeRouteMap,
// 使 useForge / useForgeApi 自动获得路由名和参数的类型推断。
// ⚠ 依赖 @route-forge/core 包,请确保前端项目已安装该依赖。
declare module '@route-forge/core' {
  interface ForgeRouteMap {
    public: {
      'auth.login': {
        method: 'POST';
        params: {};
        body: unknown;
        response: unknown;
      };
    };
    admin: {
      'admin.users.show': {
        method: 'GET';
        params: { user: string | number };
        response: unknown;
      };
      'admin.users.store': {
        method: 'POST';
        params: {};
        body: unknown;
        response: unknown;
      };
    };
    manage: {
      'manage.users.store': {
        method: 'POST';
        params: {};
        body: unknown;
        response: unknown;
      };
    };
  }
}

// ─── 本地别名(向后兼容) ──────────────────────────────────────
export type ForgeRoutes = ForgeRouteMap;

// ─── 工具类型:从 ForgeRouteMap 提取具体字段 ────────────────────

/** 提取指定层级 + 路由名的 method */
export type ForgeMethod<
  L extends ForgeLevel,
  N extends ForgeRouteName<L>,
> = L extends keyof ForgeRouteMap
  ? N extends keyof ForgeRouteMap[L] ? ForgeRouteMap[L][N]['method'] : never
  : never;

/** 提取指定层级 + 路由名的路径参数 */
export type ForgeParams<
  L extends ForgeLevel,
  N extends ForgeRouteName<L>,
> = L extends keyof ForgeRouteMap
  ? N extends keyof ForgeRouteMap[L] ? ForgeRouteMap[L][N]['params'] : never
  : never;

/** 提取指定层级 + 路由名的 body 类型(GET/DELETE 为 never) */
export type ForgeBody<
  L extends ForgeLevel,
  N extends ForgeRouteName<L>,
> = L extends keyof ForgeRouteMap
  ? N extends keyof ForgeRouteMap[L]
    ? 'body' extends keyof ForgeRouteMap[L][N]
      ? ForgeRouteMap[L][N]['body']
      : never
    : never
  : never;

/** 提取指定层级 + 路由名的 response 类型 */
export type ForgeResponse<
  L extends ForgeLevel,
  N extends ForgeRouteName<L>,
> = L extends keyof ForgeRouteMap
  ? N extends keyof ForgeRouteMap[L] ? ForgeRouteMap[L][N]['response'] : never
  : never;

结构说明:

行为说明:

设计意图:路由定义是后端数据,类型必须由后端这个唯一真相源生成——后端改了路由,跑一次命令前端类型即同步,杜绝手写类型与路由表脱节。前端调用时需要同时传入层级名和路由名( forge.api(level, name, params)),层级名是加载指定层级路由的前提。相比前端 CLI 请求端点生成,Artisan 命令离线可用,CI 中 PHP 构建阶段无需 Node 环境。

php artisan route:forge:clear

清除 Route Forge 路由元信息缓存,支持全量清除或按层级清除:

# 清除全部缓存(含摘要端点)
php artisan route:forge:clear
# 仅清除指定层级缓存
php artisan route:forge:clear --level=admin

行为说明:

3.3 管理器页面(开发环境)

仅在 APP_DEBUG=true(开发环境)下可用的可视化路由管理面板,提供:

访问地址:

GET /_forge/manager              # 管理器页面(HTML)
GET /_forge/manager/api/routes   # 所有路由及层级分配(JSON)
GET /_forge/manager/api/config   # 当前配置(JSON)
PUT /_forge/manager/api/config   # 更新配置文件

行为说明:

设计意图:开发阶段除了 Artisan 命令行,开发者还需要一个更直观的可视化界面来理解路由层级分配、调试配置匹配规则、快速编辑配置。管理器页面填补了这一需求,同时严格限制为开发环境专属,不影响生产安全。

5. 配置项参考

类型 默认值 说明
levels array<string, LevelConfig> 见 3.1.2 层级定义表,键为层级名(自定义),值为该层级的匹配规则与缓存策略
levels.{name}.description string '' 层级描述,仅用于文档与调试输出
levels.{name}.match.prefix string[] [] URI 前缀匹配列表,命中任一即归入此层级
levels.{name}.match.middleware string[] [] 中间件匹配列表,匹配逻辑受 middleware_match 控制
levels.{name}.match.middleware_match string|array 'any' 中间件匹配模式:'any'(OR)/ 'all'(AND)/ DNF 数组(见 §3.1.2 中间件匹配模式)
levels.{name}.load 'eager'|'lazy' 'lazy' 是否在摘要端点中标记为「前端应预加载」;前端自动发现时据此决定预加载策略
levels.{name}.endpoint_middleware string[]|null [] 访问该层级元信息端点(GET /{endpoint_prefix}/{level})时要求的中间件列表;未配置或空数组则不限制
endpoint_prefix string '/_forge/routes' 路由元信息对外端点前缀(同时用于层级端点和摘要端点)
url_prefix string|null null 应用的路由前缀,通过摘要端点 config.url_prefix 下发。支持完整 URL(含协议域名)或仅路径前缀;null 或空字符串 = 不下发
endpoint_middleware string[] [] 摘要端点(GET /{endpoint_prefix})中间件;空数组或 null 不限制
cache_ttl int|null 3600 统一缓存 TTL(秒);null 不缓存,0 永久缓存,负值视为 null(不缓存)。同时作用于所有层级端点与摘要端点。⚠️ 0 遵循 Laravel Cache TTL 惯例(永久),非 HTTP Cache-Control: max-age=0 含义
cache_driver string|null null 缓存驱动;null 用默认驱动,可指定 redis/file/array
strict_mode bool false 严格模式;未命中层级时抛异常(true)或归入 unassigned 特殊层级(false)
scheme_version int 1 摘要端点返回的响应格式版本号(schemeVersion 字段);后续迭代引入不兼容的格式变更时递增,前端据此做版本兼容
classifier callable|null null 自定义分类回调,签名 fn(Route $r): ?string,返回层级名或 null。返回的层级名必须在 levels 配置中存在,否则抛 UnknownClassifierTierException
aliases array<string, string> [] 路由别名映射表(见 §3.1.7):键=别名(旧路由名),值=真实路由名(新名)。与 ->forgeAlias() 宏并用时宏优先;悬空别名抛 AliasTargetException
manager_allowed_ips string[]|null ['127.0.0.1', '::1'] 管理器页面 IP 白名单(仅 APP_DEBUG=true 有意义,线上不注册管理器路由可无视):精确匹配来源 IP,'*' 放行任意来源,null/空数组不做限制

6. 错误码

所有 Route Forge 抛出的异常都实现 ForgeExceptionContract 契约,提供 code()(错误码)与 httpStatus()(对应 HTTP 状态码)方法,便于调用方 catch 后统一处理与错误响应映射。

错误类 code 触发场景 HTTP 状态
RouteTierNotAssignedException RF_BE_001 strict_mode=true 且路由未命中任何层级 500
UnknownLevelException RF_BE_002 请求的层级名不在 levels 配置中 404
CacheDriverException RF_BE_003 指定的 cache_driver 不可用 500
ClassifierException RF_BE_004 classifier 回调抛错 500
RouteMissingNameException RF_BE_005 strict_mode=true 且路由设置了 tier 但没有路由名 500
UnknownClassifierTierException RF_BE_006 classifier 返回的层级名不在 levels 配置中 500
DiscardedRegistrarAttributesException RF_BE_007 尾部链式属性被丢弃(Route::group(...)->tier(...)):自 v1.4.x 起不再自动抛出,改为日志告警(strict_mode=trueerror、默认记 warning),异常类保留供兼容
AliasTargetException RF_BE_008 别名指向的路由名不存在(悬空别名,见 §3.1.7) 500

7. 测试矩阵

测试维度 覆盖点
层级分配 显式 ->tier()、资源路由(resource/apiResource/singleton/apiSingleton)tier 与 ->only() 组合、配置 match、Route::group(数组/链式)透传、classifier(含返回非串降级、抛错包装为 ClassifierException)、unassigned 兜底、优先级覆盖、多层级同时命中取最后一个
路由别名 ->forgeAlias() 宏(单个/多个/空参报错)、config aliases宏优先于 config、别名与真实路由名撞车忽略(list warnings)、悬空别名 RF_BE_008、别名元信息与目标纯复制一致、跟随目标层级(含 unassigned)、摘要 route_count 计入别名、route:forge:types 别名条目、route:forge:list --aliases 过滤、别名随扫描缓存
Artisan 命令 route:forge:list 输出格式(table/json)、按层级过滤、unassigned 路由显示、--level 参数过滤
Artisan 命令 route:forge:types 生成 d.ts 二级结构(层级 → 路由名)、--level 过滤、--json 二级 JSON 输出、--out 写文件
Artisan 命令 route:forge:clear 全量清除缓存、按层级清除、无效层级名报错
中间件匹配 middleware_match 简单模式(any/all)、高级模式(DNF 数组)、边界情况(空数组、单元素、越界索引、空子句跳过、未知模式降级为 any);prefix 按段匹配admin 不命中 administrator
端点响应 /_forge/routes/{level} 返回结构、/_forge/routes 摘要端点返回结构、层级+摘要缓存命中、未声明层级 404、层级端点中间件保护、摘要端点中间件保护、自定义 endpoint_prefix 注册与规范化parameter_defaults 空值序列化为 {}空层级 routes 序列化为 {}
严格模式 strict_mode=true 未命中抛异常、false 未命中归入 unassigned 特殊层级、包自身路由豁免严格校验(全部用户路由已分配时端点 200)
Laravel 兼容 CI(GitHub Actions)跑 PHP 8.2–8.5 × Laravel 11/12/13 矩阵(排除 PHP 8.2 × Laravel 13 组合);资源路由、嵌套 group、链式语法、Router 重绑共享 RouteCollection
缓存 array/file 驱动(store 无关,redis 复用同一 Repository 接口,无专属逻辑)、TTL 正数过期、手动失效、0 永久缓存、cache_ttl=null 不缓存、keys 索引清理、debug 模式跳过读写
管理器页面 GET /_forge/manager 页面渲染、/api/routes(含 forge 自身路由过滤与 unassigned 归属)、/api/config 读取、配置生成器层级名转义(php -l 实测防注入)与不可编辑项(endpoint_middleware / manager_allowed_ips)保存透传IP 白名单(默认仅回环、局域网 IP 精确匹配、* 放行、空值不限制);仅 APP_DEBUG=true 注册

8. 版本与发布

8.1 v1.0 能力清单(MVP)

8.2 v1.x 路线图

路线图已清空,包转入维护态(兼容矩阵跟进 Laravel 新版本、缺陷修复、issue 支持)。

已评估并放弃的方向:

  • OpenAPI 桥接(从 OpenAPI spec 生成 body/response 类型):价值前提是项目已 持有 OpenAPI 文档(如经 Scramble 自动生成),未持有则为每个接口手写 schema 的 成本高于直接在前端手写 TS 类型,等于双重维护;且响应类型推断与包「路由元信息 单一真相源」的核心定位正交(SPEC §3.2 设计上 body/response 即为 unknown, 由业务侧自行补类型)。2026-08-30 评估后放弃。
  • Vite 插件(dev 时自动 codegen,HMR 同步路由变更):属前端工具链,不在 后端包范围。

8.3 兼容性承诺