fiberphp/router

FiberPHP router: attribute-based routing with dispatcher, middleware pipeline, controller DI injection and route caching.

Maintainers

Package info

gitee.com/FiberPHP/router

Issues

pkg:composer/fiberphp/router

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

dev-master 2026-08-22 10:11 UTC

This package is not auto-updated.

Last update: 2026-08-22 13:29:44 UTC


README

应用导航层,负责路由注册、匹配、调度、中间件管线与控制器参数注入。通过 RequestHandlerInterfacefiberphp/http 解耦——HTTP 层只管协议通信,路由层只管请求分发。

环境要求

  • PHP >= 8.3
  • 依赖 fiberphp/frameworkfiberphp/httpfiberphp/eventfiberphp/container

架构

fiberphp/http (物理通信层)
  └─ onMessage → container()->get(RequestHandlerInterface)
       └─ RouterProvider 绑定 → Router::handleRequest()
            ├─ Dispatcher (路由匹配:静态路径 O(1) + 变量路径正则)
            ├─ Pipeline (中间件管线:全局 → 控制器 → 路由)
            └─ Controller DI (反射注入:Request/标量/Enum/Model/类)

路由定义策略

优先级方式适用场景
一级(主力)PHP 8 Attribute 注解所有业务 API、Web 页面、Restful 资源
二级(辅助)闭包/数组文件 route/app.php系统探针、健康检查、极简重定向

路由加载顺序

  1. 路由缓存存在 → 直接反序列化 runtime/cache/routes.php
  2. 无缓存 → AttributeScanner 扫描控制器注解 → 加载 route/*.php 闭包文件

配置

路由包无需配置文件,默认行为代码内置:

  • 控制器扫描目录固定为 App\Controller\(约定,RouterProvider 内置)
  • 无注解控制器的默认回退路由默认开启(AttributeScanner 内置,可用 #[Route\NoDefaultRoute] 类级注解禁用单个控制器)

应用路由 route/app.php(闭包/数组回调,需安装本包):

use FiberPHP\Http\Response;
use FiberPHP\Router\Router;

Router::get('/ping', fn () => 'pong');
Router::get('/health', fn () => 'ok');
Router::get('/favicon.ico', fn () => new Response(204));

业务路由请使用控制器注解(可缓存);闭包路由无法缓存,生产 route:cache 后此文件不加载。

路由定义

1. Attribute 注解(推荐)

namespace App\Controller;

use FiberPHP\Router\Attribute as Route;
use FiberPHP\Http\Request;
use FiberPHP\Http\Response;

class UserController
{
    #[Route\Get('/users', name: 'users.index')]
    public function index(Request $request): Response
    {
        return json(['data' => []]);
    }

    #[Route\Get('/users/{id}', name: 'users.show')]
    public function show(int $id): Response
    {
        return json(['id' => $id]);
    }

    #[Route\Post('/users', name: 'users.store')]
    public function store(Request $request): Response
    {
        return json(['code' => 0], 201);
    }

    #[Route\Put('/users/{id}', name: 'users.update')]
    public function update(int $id, Request $request): Response
    {
        return json(['id' => $id]);
    }

    #[Route\Delete('/users/{id}', name: 'users.destroy')]
    public function destroy(int $id): Response
    {
        return json(['code' => 0]);
    }
}

可用的 HTTP 方法注解

注解HTTP 方法
#[Route\Get]GET
#[Route\Post]POST
#[Route\Put]PUT
#[Route\Patch]PATCH
#[Route\Delete]DELETE
#[Route\Head]HEAD
#[Route\Options]OPTIONS
#[Route\Any]以上全部

Route 基类参数

所有 HTTP 方法注解继承 Route 抽象类,构造参数:

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
abstract class Route
{
    public function __construct(
        public string $path = '',        // 路由路径
        public array  $middleware = [],   // 路由中间件
        public string $name = '',         // 路由名称
        public array  $where = [],        // 参数正则约束
        public bool   $autoPrefix = true, // 是否自动拼接组前缀
    ) {}
}

同一个方法可叠加多个注解(IS_REPEATABLE):

#[Route\Get('/users', name: 'users.index')]
#[Route\Get('/users/list', name: 'users.list')]
public function index(): Response { ... }

2. 路由分组 #[RouteGroup]

#[Route\RouteGroup(
    prefix: '/api',
    middleware: ['auth'],
    namePrefix: 'api.',
)]
class UserController
{
    // → GET /api/users, 名:api.users.index
    #[Route\Get('/users', name: 'users.index')]
    public function index() { ... }
}
参数说明
prefix路径前缀,拼接到组内所有路由
middleware组级中间件,对组内所有路由生效
namePrefix名称前缀,拼接到组内所有路由名
version显式版本号覆盖(如 'v2'),替换自动推断的版本段
autoPrefix是否启用命名空间自动前缀推断(默认 true)

3. 资源路由 #[Resource]

#[Route\Resource(name: 'posts', only: ['index', 'show', 'store', 'update', 'destroy'])]
class PostController
{
    public function index() { ... }
    public function show(int $id) { ... }
    public function store(Request $request) { ... }
    public function update(int $id, Request $request) { ... }
    public function destroy(int $id) { ... }
}

自动注册的路由:

HTTP 方法路径控制器方法路由名称
GET/postsindexposts.index
GET/posts/createcreateposts.create
POST/postsstoreposts.store
GET/posts/{id}showposts.show
GET/posts/{id}/editeditposts.edit
PUT/posts/{id}updateposts.update
DELETE/posts/{id}destroyposts.destroy
PUT/posts/{id}/recoveryrecoveryposts.recovery

only 白名单控制只注册部分动作。不传 only 则根据控制器是否存在对应方法自动注册。

4. 闭包路由(应用级)

// route/app.php
use App\Controller\WebhookController;
use FiberPHP\Router\Router;

Router::get('/ping', fn () => 'pong');
Router::post('/webhook', [WebhookController::class, 'handle']);
方法说明
Router::get($path, $callback)注册 GET 路由
Router::post($path, $callback)注册 POST 路由
Router::put($path, $callback)注册 PUT 路由
Router::patch($path, $callback)注册 PATCH 路由
Router::delete($path, $callback)注册 DELETE 路由
Router::any($path, $callback)匹配所有方法
Router::add($methods, $path, $callback)自定义方法(可传数组)
Router::group($path, $callback)路由分组(支持嵌套)
Router::resource($name, $controller, $options)资源路由
Router::getByName($name)按名称获取路由对象
Router::getRoutes()获取所有路由

API 版本管理

采用"模块化 + 版本子目录"架构,从控制器命名空间自动推断版本前缀。

自动推断规则

App\Controller\V1\UserController        → /v1/users    名称前缀: v1.
App\Controller\V2\UserController        → /v2/users    名称前缀: v2.
App\Controller\V2\Admin\RoleController  → /v2/admin/roles  名称前缀: v2.admin.
  • V\d+ 命名空间段识别为版本号,转为小写 vN 作为路径和名称前缀
  • 其余段转为 snake_case 拼入路径
  • 类名转为 snake_case 作为资源路径

示例

namespace App\Controller\V1;

use FiberPHP\Router\Attribute as Route;

// 自动推断:前缀 /v1, 名称前缀 v1.
class UserController
{
    #[Route\Get('/users', name: 'users.index')]  // → GET /v1/users, 名:v1.users.index
    public function index() { ... }
}
namespace App\Controller\V2;

use FiberPHP\Router\Attribute as Route;

// 自动推断:前缀 /v2, 名称前缀 v2.
class UserController
{
    #[Route\Get('/users', name: 'users.index')]  // → GET /v2/users, 名:v2.users.index
    public function index() { ... }
}

显式版本覆盖

#[Route\RouteGroup(version: 'v3')]
class UserController
{
    // 即使类在 V1 命名空间下,路径也会是 /v3/users
    #[Route\Get('/users')]
    public function index() { ... }
}

默认回退路由

当控制器没有任何方法注解时,默认开启回退路由,自动将所有 public 方法注册为 ANY 路由:

namespace App\Controller;

class IndexController
{
    public function index()  // → ANY /index
    {
        return 'hello';
    }

    public function healthCheck()  // → ANY /health_check
    {
        return 'ok';
    }
}

方法名自动转为 snake_case 作为路径。使用 #[Route\NoDefaultRoute] 可禁止此行为:

#[Route\NoDefaultRoute]
class ApiController
{
    // 不会自动注册任何路由
    public function internalMethod() { ... }
}

路由参数

动态参数

#[Route\Get('/users/{id}')]
public function show(int $id) { ... }

可选参数

#[Route\Get('/posts/{id?}')]
public function show(int $id = 1) { ... }

正则约束

通过 where 参数约束参数格式:

#[Route\Get('/users/{id}', where: ['id' => '\d+'])]
public function show(int $id) { ... }
// 路径编译为 /users/{id:\d+}

路由命名与 URL 生成

#[Route\Get('/users/{id}', name: 'users.show')]
public function show(int $id) { ... }

// 通过名称获取路由
$route = Router::getByName('users.show');

// 生成 URL
$url = $route->url(['id' => 42]);
// /users/42

// 多余参数自动拼为查询字符串
$url = $route->url(['id' => 42, 'tab' => 'profile']);
// /users/42?tab=profile

中间件

执行顺序:全局中间件 → 控制器中间件 → 路由中间件 → 控制器方法。

全局中间件

config/http.phpmiddleware.global 键注册:

'middleware' => [
    'global' => [
        \App\Middleware\CorsMiddleware::class,
        \App\Middleware\TraceMiddleware::class,
    ],
    'aliases' => [
        'auth' => \App\Middleware\AuthMiddleware::class,
    ],
],

中间件别名

middleware.aliases 将短名映射到中间件类,路由/控制器中间件可用别名替代完整类名:

// config/http.php
'middleware' => [
    'aliases' => [
        'auth'  => \App\Middleware\AuthMiddleware::class,
        'admin' => \App\Middleware\AdminMiddleware::class,
    ],
],

// 路由注解用别名
#[Route\Get('/admin', middleware: ['auth', 'admin'])]

控制器中间件

class AdminController
{
    protected static array $middleware = [
        \App\Middleware\AuthMiddleware::class,
    ];
}

路由中间件

通过注解的 middleware 参数指定:

#[Route\Get('/admin/dashboard', middleware: ['auth', 'admin'])]
public function dashboard() { ... }

通过 #[RouteGroup] 批量指定:

#[Route\RouteGroup(prefix: '/api', middleware: ['auth'])]
class UserController { ... }

中间件实现

中间件类需实现 FiberPHP\Http\Contract\MiddlewareInterface

use FiberPHP\Http\Contract\MiddlewareInterface;
use FiberPHP\Http\Request;
use FiberPHP\Http\Response;

class AuthMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response
    {
        if (!$request->header('authorization')) {
            return new Response(401, [], 'Unauthorized');
        }

        return $handler($request);
    }
}

控制器参数注入

基于反射的自动参数注入,首次请求分析方法签名并缓存元数据。

参数类型注入方式
Request(或其父类)注入当前请求对象
标量(int/float/bool/string/array)从请求参数获取并自动类型转换
mixed/resource直接透传请求参数值
枚举(Enum)按名称或 backed 值匹配
FiberPHP\Model 子类以请求数据为 attributes 构造
其他类(有构造函数)递归解析构造参数,容器构造
其他类(无构造参数)容器实例化
use FiberPHP\Http\Request;
use App\Service\UserService;
use App\Enum\UserStatus;

class UserController
{
    // Request + 路由参数
    public function show(Request $request, int $id) { ... }

    // 标量 + 默认值
    public function search(string $keyword, int $page = 1) { ... }

    // 依赖注入
    public function create(UserService $service) { ... }

    // 枚举注入
    public function status(UserStatus $status) { ... }
}

缺少必填参数抛 InputException(HTTP 400),类型不匹配同样抛 InputException

路由缓存

# 编译路由缓存(仅支持控制器动作,闭包不可缓存)
php fiber route:cache

# 清理路由缓存
php fiber route:clear

缓存文件生成在 runtime/cache/routes.php,存在时直接反序列化加载,跳过注解扫描。

route/app.php 闭包路由无法缓存,route:cache 会抛 RuntimeException。生产环境建议使用控制器动作。

命令行工具

# 查看所有已注册路由
php fiber route:list

输出表格包含 URI、HTTP 方法、回调、中间件、路由名称。

与 fiberphp/http 的关系

fiberphp/http                          fiberphp/router
┌─────────────────────┐           ┌─────────────────────────┐
│  TCP 连接管理        │           │  控制器注解扫描          │
│  HTTP 协议解析       │           │  路由匹配 (Dispatcher)   │
│  全局中间件管线      │           │  控制器/路由中间件       │
│  异常渲染           │  Request  │  控制器 DI 注入          │
│  连接保活           │ ────────→ │  版本前缀推断            │
│                     │  Response │  路由缓存                │
│  RequestHandlerIntf │←──────────│  Router::handleRequest() │
│  (Contract 层)      │  绑定     │  RouterProvider         │
└─────────────────────┘           └─────────────────────────┘

解耦点FiberPHP\Http\Contract\RequestHandlerInterface

  • fiberphp/httpHttp::onMessage() 通过容器获取 RequestHandlerInterface 实现
  • fiberphp/routerRouterProvider::register() 绑定该接口到匿名类,委托 Router::handleRequest()
  • 若未安装 fiberphp/router,容器返回 null,回退到 defaultHandler() 直接抛 NotFoundException(404)

目录结构

src/
├── Attribute/                  # 路由注解
│   ├── Route.php               # 抽象基类(path/middleware/name/where/autoPrefix)
│   ├── Get.php                 # GET 注解
│   ├── Post.php                # POST 注解
│   ├── Put.php                 # PUT 注解
│   ├── Patch.php               # PATCH 注解
│   ├── Delete.php              # DELETE 注解
│   ├── Head.php                # HEAD 注解
│   ├── Options.php             # OPTIONS 注解
│   ├── Any.php                 # 全方法注解
│   ├── RouteGroup.php          # 类级分组(prefix/middleware/namePrefix/version)
│   ├── Resource.php            # 类级 RESTful 资源
│   └── NoDefaultRoute.php      # 禁止默认回退路由
├── Command/                    # 控制台命令
│   ├── RouteList.php           # route:list
│   ├── RouteCache.php          # route:cache
│   └── RouteClear.php          # route:clear
├── AttributeScanner.php        # 控制器注解扫描 + 版本前缀推断 + 回退路由
├── Dispatcher.php              # 路由编译与匹配(静态 O(1) + 变量正则)
├── DispatchStatus.php          # 调度状态枚举(Found/NotFound/MethodNotAllowed)
├── Route.php                   # 路由值对象(name/middleware/params/url)
├── Router.php                  # 路由注册入口 + 请求调度 + 中间件管线 + DI 注入
├── RouterProvider.php         # 服务提供者(绑定接口 + 加载路由)
└── Install.php                 # 安装器(#[Package] 声明)

License

MIT