路由
Pin 使用枚举定义路由。
每个枚举 case 表示一条路由,并贯穿路由注册、URL 生成与 HTTP 测试。
namespace App\Routes;
use Pin\Password\Middleware\DecodePassword;
use Pin\Route\IError;
use Pin\Route\InteractsWithRoute;
enum UserRoute: string implements IError
{
use InteractsWithRoute;
case Index = 'GET:/api/users';
#[Middleware(DecodePassword::class)]
case Create = 'POST:/api/users';
}路由定义
每个枚举 case 表示一条路由,支持以下两种格式:
method:urimethod:uri|name
其中:
method表示请求方法uri表示路由 URIname:表示路由名称
路由的请求方法、URI 和名称通过 case 值定义,处理器、中间件、标题等附加信息通过 PHP Attribute(#[...])定义。
请求方法
case Index = 'GET:/api/users';
case Create = 'POST:/api/users';
case Update = 'PUT:/api/users/{id}';
case Delete = 'DELETE:/api/users/{id}';INFO
请求方法会自动转换为大写。
case Index = 'get:/api/users'; // GET
case Index = 'Get:/api/users' // GET路由 URI
路由 URI 定义接口访问路径。
case Index = 'GET:/api/users';
case Update = 'PUT:/api/users/{id}';使用 #[Prefix] 可为整个路由枚举声明统一前缀,最终路由 URI 为 前缀 + 路由 URI。
use Pin\Route\Attributes\Prefix;
#[Prefix('/api/users')]
enum UserRoute: string
{
case Index = 'GET:/';
case Update = 'PUT:/{id}';
}注册的路由:
GET /api/users
PUT /api/users/{id}INFO
路由 URI(包括 #[Prefix])会自动规范化,去除首尾 /,并统一以 /path 形式注册和返回。
case Create = 'POST:/api/users/'; // /api/users
case Update = 'PUT:api/users/{id}'; // /api/users/{id}路由名称
每条路由都对应一个唯一名称。
默认情况下,路由名称根据枚举和路由定义自动生成。
默认名称
如果未显式指定名称,Pin 会按照以下规则生成:
移除 URI 开头的
/api前缀。去除 URI 两侧的
/。将 URI 分隔符
/转换为.。根据请求方法追加操作名称:
POST追加create。PUT追加update。DELETE追加delete。
GET请求中的动态参数{id}在名称生成时转换为detail。
示例:
| case 值 | 路由名称 |
|---|---|
GET:/api/users | users |
GET:/api/users/{id} | users.detail |
POST:/api/users | users.create |
PUT:/api/users/{id} | users.update |
DELETE:/api/users/{id} | users.delete |
GET:/api/v1/users | v1.users |
显式指定名称
如果默认生成的名称不符合语义,可以显式指定。
在 case 值中通过 |name 指定:
case Index = 'GET:/api/users|users.index';或通过 #[Name] 属性指定:
use Pin\Route\Attributes\Name;
#[Name('users.index')]
case Index = 'GET:/api/users';处理器
处理器用于指定路由请求的处理入口。
默认处理器
默认情况下,Pin 会根据默认约定自动推导对应的控制器和方法:
- 根据当前模块推导对应控制器,详见模块与推导。
- 使用枚举 case 名称的小驼峰形式作为方法名。
例如:
case Create = 'POST:/api/users';默认会推导为:
[UserController::class, 'create']显式指定处理器
如果默认约定不满足需求,可通过 #[Handler] 指定处理器。
use Pin\Route\Attributes\Handler;
#[Handler([UserHandler::class, 'handle'])]
case Export = 'GET:/api/users/export';#[Handler] 支持常见的处理器形式:
#[Handler('UserController@store')]
case Create = 'Post:/api/users';
#[Handler([UserHandler::class, 'handle'])]
case Export = 'GET:/api/users/export';
#[Handler(UserInvokableHandler::class)]
case Import = 'POST:/api/users/import';INFO
如需使用自定义处理器(例如闭包),请参阅自定义处理器。
中间件
通过 #[Middleware] 属性为路由指定一个或多个中间件。
#[Middleware] 支持单个中间件或中间件数组:
use Pin\Route\Attributes\Middleware;
use Pin\Password\Middleware\DecodePassword;
#[Middleware(DecodePassword::class)]
case Create = 'POST:/api/users';
#[Middleware(['email', 'verified'])]
case Profile = 'GET:/api/users/profile';附加属性
除了名称、处理器和中间件外,Pin 还提供了一些用于特定场景的路由属性。
标题、权限与测试
路由标题
Title 用于为路由定义一个可读的标题,可用于菜单、权限树、测试报告等需要展示路由信息的场景。
use Pin\Route\Attribute\Title;
#[Title('用户列表')]
case Index = 'GET:/api/users';Action
Action 用于为 HTTP 测试指定默认的业务操作 Action。
use Pin\Route\Attributes\Action;
#[Action(ListUsersAction::class)]
case Index = 'GET:/api/users';断言方法
AssertionMethod 用于为 HTTP 测试指定默认的断言方法。
use Pin\Route\Attributes\AssertionMethod;
#[AssertionMethod(Pin\Route\Testing\AssertionMethod::Successful)]
case Index = 'GET:/api/users';路由注册
Pin 支持两种路由注册方式:单个注册和批量注册。
单个注册
每个枚举 case 都可以单独注册。注册时,还可以指定中间件和权限标识。
使用闭包作为处理器:
UserRoute::Create->register(
fn () => app(UserService::class)->create()
);指定中间件:
use Pin\Password\Middleware\DecodePassword;
UserRoute::Create->register(
handler: [UserController::class, 'store'],
middlewares: [DecodePassword::class, 'verified'],
);指定权限标识:
UserRoute::Create->register(
handler: [UserController::class, 'store'],
accessCode: 'users.create'
);INFO
register() 未显式指定中间件或权限标识时,会使用路由属性中 #[Middleware] 和 #[Access] 声明的配置。
批量注册
对于应用中的常规路由,推荐使用批量注册方式。
// routes/api.php
use Pin\Route\RouteRegistrar;
use Pin\Route\RouteScanner;
RouteRegistrar::register(
new RouteScanner()->scan([
app_path('Routes'),
app_path('Modules'),
])
);INFO
RouteScanner 会扫描以 Route.php 结尾,并实现 Pin\Route\Routable 的枚举类。
也可以直接将一个或多个枚举类传递给 RouteRegistrar::register()。
// routes/api.php
use App\Modules\Product\Routes\ProductRoute;
use App\Routes\HomeRoute;
use App\Routes\User\UserRoute;
use Pin\Route\RouteRegistrar;
use Pin\Route\RouteScanner;
RouteRegistrar::register([
HomeRoute::class,
UserRoute::class,
ProductRoute::class
]);自定义路由注册
默认情况下,Pin 会根据路由枚举中定义的信息注册路由;如需自定义注册方式,可以覆盖 registerRoutes() 方法。
将当前路由枚举中的路由放入 middleware group:
use Illuminate\Support\Facades\Route;
public static function registerRoutes(): void
{
Route::middleware('auth')->group(fn () => self::addRoutes());
}显式注册每个路由:
public static function registerRoutes(): void
{
self::Generate->register(
handler: [CaptchaController::class, 'generate'],
accessCode: false,
);
self::AvailableRules->register(
handler: [CaptchaController::class, 'availableRules'],
middlewares: 'auth',
);
}自定义处理器
默认情况下,Pin 会根据约定解析路由处理器;如需自定义处理器解析方式,可以覆盖 handler() 方法。
使用统一处理入口:
use Illuminate\Http\Request;
use Pin\Http\ApiResponse;
protected function handler()
{
return function (Request $request) {
return ApiResponse::success(
message: $request->route()->getName()
);
};
}自定义控制器方法:
protected function handler()
{
return [
$this->controller(),
'action'.lcfirst($this->name),
];
}查看已注册路由
注册完成后,Pin 会保存枚举 case 和 Laravel Route 实例之间的关联关系。
可以通过 RouteRegistry::items() 获取已注册的路由信息:
use Pin\Http\ApiResponse;
use Pin\Route\RouteRegistryItem;
public function routes(): ApiResponse
{
$data = RouteRegistry::items()->map(fn (RouteRegistryItem $item) => [
'name' => $item->route->getName(),
'action' => $item->route->getActionName(),
'case' => get_class($item->case).'::'.$item->case->name,
'title' => $item->case->title(),
])->values();
return $this->success($data);
}响应示例:
{
"code": 0,
"message": "请求成功",
"data": [
{
"name": "auth.login",
"action": "App\\Modules\\Auth\\LoginController@login",
"case": "App\\Routes\\Auth\\LoginRoute::Login",
"title": "登录"
},
{
"name": "users.create",
"action": "App\\Modules\\User\\UserController@create",
"case": "App\\Routes\\User\\UserRoute::Create",
"title": "新增用户"
}
]
}路由使用
获取请求方法
通过 method() 方法获取请求方法:
case Index = 'GET:/api/users';
UserRoute::Index->method(); // GET获取路由 URI
通过 uri() 方法获取路由 URI:
case Index = 'GET:/api/users';
UserRoute::Index->uri(); // /api/users获取路由名称
通过 name() 方法获取路由名称。
case Index = 'GET:/api/users';
case Show = 'GET:/api/users/{id}';
UserRoute::Index->name(); // users
UserRoute::Show->name(); // users.detail获取路由属性
通过 attribute() 方法获取 #[...] 定义的属性:
#[Attribute('value')]
case Index = 'GET:/api/users';
case Create = 'POST:/api/users';
UserRoute::Index->attribute(); // Attribute('value') 对象
UserRoute::Create->attribute(); // nullURL 生成
通过 route() 方法生成 URL。
case Index = 'GET:/api/users';
case Show = 'GET:/api/users/{id}';基础用法:
UserRoute::Index->route(); // https://example.com/api/users路由参数:
UserRoute::Show->route(['id' => 1]); // https://example.com/api/users/1生成相对 URL:
UserRoute::Index->route(null, false); // /api/users
UserRoute::Show->route(['id' => 1], false); // /api/users/1INFO
route 方法会自动使用当前枚举对应的路由名称生成 URL,用法与 Laravel 的 route() 函数一致。
HTTP 测试
枚举 case 提供 HTTP 测试入口。
常用方法:
testing():创建当前路由的 HTTP 测试实例。testJson():testing()->json()的快捷调用方式。tests():创建多个路由的批量测试套件。
单个路由测试
通过 testJson() 方法:
// UserTest.php
UserRoute::Index->testJson($this)->assertPaginated();通过 testing() 方法:
UserRoute::Create->testing($this)
->withPayload(['username' => null])
->json()
->assertCode(422, 422)
->assertInvalid('username');设置路由参数:
UserRoute::Update->testing($this)
->withRouteParams(['id' => 10000])
->json()
->assertUpdated();自动测试
可以通过 tests() 批量执行路由测试。
指定路由:
UserRoute::tests([
UserRoute::Index,
UserRoute::Create,
])->run();全部路由:
UserRoute::tests()->run();详细用法请参阅 HTTP 测试。