Search by

fiberphp / router

fiberphp

🛣️ FiberPHP 路由器 —— 属性路由声明,支持中间件管道、控制器依赖注入、路由缓存、RESTful 约定。

Package info

gitee.com/fiberphp/router.git

Issues

pkg:composer/fiberphp/router

Statistics

Installs: 5

Dependents: 1

Suggesters: 1

dev-master 2026-10-07 02:46 UTC

This package is auto-updated.

Last update: 2026-10-07 02:46:34 UTC


README

注解路由层:扫描控制器注解(#[Get] 等)、加载 route/*.php 路由文件,统一注册到 fiberphp/http 的路由核心; 提供 route:list / route:cache / route:clear 命令。路由注册表、分发、中间件管线与控制器参数注入由 http 包承载。

环境要求

  • PHP >= 8.3
  • 必需依赖:fiberphp/framework(含包发现机制、事件总线与 #[Package] 特性)、fiberphp/http、psr/log
  • 控制台命令(route:list / route:cache / route:clear)已随 framework 内置

安装

composer require fiberphp/router

包源码托管在 Gitee(gitee.com/fiberphp/router)。若尚未发布到 Packagist,在应用 composer.json 中添加 VCS 仓库源:

{
    "repositories": [
        { "type": "vcs", "url": "https://gitee.com/fiberphp/router.git" }
    ]
}

当前各包以 dev-master 互依赖,应用需设置 "minimum-stability": "dev"(骨架包 fiberphp/skeleton 已内置);发布稳定版后将切换为语义化版本约束。

配置

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

  • 控制器扫描目录固定为 App\Controller\
  • 无注解控制器由 http 包约定式扫描兜底(类级 #[\FiberPHP\Http\Attribute\NoDefaultRoute] 可关闭该类的约定路由)

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

use FiberPHP\Http\Response;
use FiberPHP\Http\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 User
{
    #[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 User
{
    // → 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 Post
{
    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\Webhook;
use FiberPHP\Http\Router;

Router::get('/ping', fn () => 'pong');
Router::post('/webhook', [Webhook::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\User        → /v1/users    名称前缀: v1.
App\Controller\V2\User        → /v2/users    名称前缀: v2.
App\Controller\V2\Admin\Role  → /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 User
{
    #[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 User
{
    #[Route\Get('/users', name: 'users.index')]  // → GET /v2/users, 名:v2.users.index
    public function index() { ... }
}

显式版本覆盖

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

约定式兜底路由

没有任何方法注解的控制器,由 http 包约定式扫描器按文件路径与方法名自动注册路由(如 Admin/Role.php 的 getList() → GET /admin/role/list)。类级标注 #[\FiberPHP\Http\Attribute\NoDefaultRoute] 可关闭该类的约定路由:

use FiberPHP\Http\Attribute\NoDefaultRoute;

#[NoDefaultRoute]
class Api
{
    // 不会自动注册任何约定路由
    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) { ... }

// 通过辅助函数生成 URL
$url = route('users.show', ['id' => 42]);
// /users/42

// 通过名称获取路由对象
$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.php 的 middleware.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 Admin
{
    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 User { ... }

中间件实现

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

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

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

        return $next($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 User
{
    // 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) { ... }
}

缺少必填参数或类型不匹配时抛 FiberPHP\Http\Exception\HttpException(HTTP 400,消息透传,出错参数名等细节随 context 仅在 debug 模式输出)。

路由缓存

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

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

缓存文件生成在 runtime/cache/routes.php,存在时优先于注解扫描加载。

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

命令行工具

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

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

License

MIT