路由机制
路由(Routing)是现代 PHP 框架的核心组件之一,负责将传入的 HTTP 请求映射到对应的处理逻辑。路由系统使得 URL 与代码解耦,支持灵活的 URL 设计、参数提取、RESTful 风格路由和中间件挂载。本节将深入讲解 PHP 框架中路由机制的通用原理、设计模式和各种实现方式。
基础概念
路由的作用
客户端请求: GET /users/42
↓ Web 服务器 / .htaccess
↓ 路由系统解析
匹配规则: /users/{id} → UserController@show(id=42)
↓ 调用控制器方法
↓ 返回响应路由的核心功能
| 功能 | 说明 |
|---|---|
| URL 匹配 | 将请求 URI 匹配到预定义的路由规则 |
| 参数提取 | 从 URL 中提取变量(如 ID、slug) |
| HTTP 方法 | 区分 GET、POST、PUT、DELETE 等方法 |
| 中间件挂载 | 为路由或路由组绑定中间件 |
| 命名路由 | 为路由定义名称,方便生成 URL |
| URL 生成 | 根据路由名称和参数反向生成 URL |
详细说明
1. 路由匹配算法
php
<?php
declare(strict_types=1);
/**
* 简易路由匹配器实现
*/
class Router
{
/** @var array<array{method: string, pattern: string, handler: callable, regex: string, params: string[]}> */
private array $routes = [];
/**
* 注册 GET 路由
*/
public function get(string $pattern, callable $handler): void
{
$this->addRoute('GET', $pattern, $handler);
}
/**
* 注册 POST 路由
*/
public function post(string $pattern, callable $handler): void
{
$this->addRoute('POST', $pattern, $handler);
}
/**
* 注册路由
*/
public function addRoute(string $method, string $pattern, callable $handler): void
{
// 将路由模式转换为正则表达式
$regex = preg_replace_callback(
'/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/',
fn(array $matches): string => '(?P<' . $matches[1] . '>[^/]+)',
$pattern
);
// 提取参数名
preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $pattern, $matches);
$params = $matches[1];
$this->routes[] = [
'method' => strtoupper($method),
'pattern' => $pattern,
'handler' => $handler,
'regex' => '~^' . $regex . '$~',
'params' => $params,
];
}
/**
* 匹配请求
*/
public function match(string $method, string $uri): ?RouteMatch
{
foreach ($this->routes as $route) {
if ($route['method'] !== strtoupper($method)) {
continue;
}
if (preg_match($route['regex'], $uri, $matches)) {
$params = [];
foreach ($route['params'] as $paramName) {
$params[$paramName] = $matches[$paramName] ?? null;
}
return new RouteMatch($route['handler'], $params);
}
}
return null;
}
}
class RouteMatch
{
public function __construct(
public readonly callable $handler,
public readonly array $params = []
) {}
public function callHandler(): mixed
{
return ($this->handler)(...array_values($this->params));
}
}2. 路由参数
php
<?php
declare(strict_types=1);
// Laravel 风格
Route::get('/users/{id}', [UserController::class, 'show']);
Route::get('/posts/{slug}', [PostController::class, 'show']);
Route::get('/categories/{category}/posts/{id}', [PostController::class, 'showByCategory']);
// Symfony 风格
#[Route('/users/{id}', requirements: ['id' => '\d+'])]
// ThinkPHP 风格
Route::get('users/:id', 'UserController/read');
Route::get('posts/:slug', 'PostController/read');3. 可选参数
php
<?php
// Laravel
Route::get('/users/{id?}', fn(?int $id = null) => $id ? "User {$id}" : 'All users');
Route::get('/category/{category?}/posts', fn(?string $category = 'general') => "Posts in {$category}");
// 正则约束
Route::get('/users/{id}', fn(int $id) => "User {$id}")
->where('id', '[0-9]+');
Route::get('/posts/{slug}', fn(string $slug) => "Post {$slug}")
->where('slug', '[a-z0-9-]+');4. RESTful 资源路由
php
<?php
// Laravel 资源路由
Route::resource('posts', PostController::class);
// 等价于:
Route::get('/posts', [PostController::class, 'index']); // 列表
Route::post('/posts', [PostController::class, 'store']); // 创建
Route::get('/posts/create', [PostController::class, 'create']); // 创建表单
Route::get('/posts/{post}', [PostController::class, 'show']); // 详情
Route::put('/posts/{post}', [PostController::class, 'update']); // 更新
Route::delete('/posts/{post}', [PostController::class, 'destroy']); // 删除
Route::get('/posts/{post}/edit', [PostController::class, 'edit']); // 编辑表单
// API 资源路由(不含 create/edit)
Route::apiResource('posts', PostController::class);5. 路由分组
php
<?php
// Laravel 路由分组
Route::middleware(['auth', 'throttle:60,1'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/profile', [ProfileController::class, 'index']);
});
// 前缀分组
Route::prefix('api/v1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::get('/posts', [PostController::class, 'index']);
});
// 命名空间分组
Route::namespace('Admin')->group(function () {
Route::get('/admin/dashboard', 'DashboardController@index');
});6. 命名路由
php
<?php
// 定义命名路由
Route::get('/users/{id}', [UserController::class, 'show'])->name('users.show');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
// 生成 URL
$url = route('users.show', ['id' => 42]); // http://example.com/users/42
$url = route('posts.store'); // http://example.com/posts实战示例
场景一:从零实现路由器
php
<?php
declare(strict_types=1);
/**
* 完整的路由器实现
*/
class SimpleRouter
{
private array $routes = [];
private array $groupStack = [];
public function get(string $pattern, callable $handler): void
{
$this->addRoute('GET', $pattern, $handler);
}
public function post(string $pattern, callable $handler): void
{
$this->addRoute('POST', $pattern, $handler);
}
public function put(string $pattern, callable $handler): void
{
$this->addRoute('PUT', $pattern, $handler);
}
public function delete(string $pattern, callable $handler): void
{
$this->addRoute('DELETE', $pattern, $handler);
}
public function group(array $attributes, callable $callback): void
{
$this->groupStack[] = $attributes;
$callback($this);
array_pop($this->groupStack);
}
private function addRoute(string $method, string $pattern, callable $handler): void
{
$prefix = '';
$middleware = [];
foreach ($this->groupStack as $group) {
$prefix .= $group['prefix'] ?? '';
$middleware = array_merge($middleware, $group['middleware'] ?? []);
}
$fullPattern = rtrim($prefix, '/') . '/' . ltrim($pattern, '/');
$regex = $this->patternToRegex($fullPattern);
$this->routes[] = [
'method' => $method,
'pattern' => $fullPattern,
'handler' => $handler,
'regex' => $regex,
'middleware' => $middleware,
];
}
private function patternToRegex(string $pattern): string
{
$regex = preg_replace_callback(
'/\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?\}/',
fn($m) => '(?P<' . $m[1] . '>' . ($m[2] ?? '[^/]+') . ')',
$pattern
);
return '~^' . $regex . '$~';
}
public function dispatch(string $method, string $uri): string
{
foreach ($this->routes as $route) {
if ($route['method'] !== strtoupper($method)) {
continue;
}
if (preg_match($route['regex'], rtrim($uri, '/'), $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
return ($route['handler'])(...array_values($params));
}
}
http_response_code(404);
return '404 Not Found';
}
}
// 使用
$router = new SimpleRouter();
$router->group(['prefix' => '/api'], function (SimpleRouter $router) {
$router->get('/users', fn() => json_encode(['users' => []]));
$router->get('/users/{id}', fn(int $id) => json_encode(['user_id' => $id]));
$router->post('/users', fn() => 'User created');
});
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
echo $router->dispatch($method, $uri);场景二:Nginx/Apache URL 重写
nginx
# Nginx 配置
location / {
try_files $uri $uri/ /index.php?$query_string;
}apache
# Apache .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]注意事项
1. 路由缓存
bash
# Laravel 路由缓存(生产环境)
php artisan route:cache
# Symfony 路由缓存
php bin/console cache:warmup
# ThinkPHP 路由缓存
php think route:build2. 路由性能
| 优化方式 | 说明 |
|---|---|
| 路由缓存 | 将路由编译为静态文件,避免每次请求重新解析 |
| 精确匹配优先 | 静态路由(无参数)优先于动态路由 |
| 正则缓存 | 编译后的正则表达式缓存 |
| 路由数量控制 | 避免注册过多路由 |
详细说明(续)
7. 路由约束与正则
php
<?php
// Laravel 中的路由约束
// 单个参数约束
Route::get('/users/{id}', fn(int $id) => "User {$id}")
->where('id', '[0-9]+');
// 多个参数约束
Route::get('/posts/{slug}/comments/{id}', fn(string $slug, int $id) => "...")
->where([
'slug' => '[a-z0-9-]+',
'id' => '[0-9]+',
]);
// 内置约束快捷方式
Route::get('/users/{id}', fn(int $id) => "...")
->whereNumber('id');
Route::get('/posts/{slug}', fn(string $slug) => "...")
->whereAlphaNumeric('slug');
Route::get('/files/{path}', fn(string $path) => "...")
->whereIn('path', ['jpg', 'png', 'gif']);8. 回退路由
php
<?php
// Laravel 回退路由
Route::fallback(function () {
return response()->json(['error' => 'Not Found'], 404);
});9. 路由模型绑定
php
<?php
// 隐式绑定:参数名与模型变量名一致
Route::get('/users/{user}', fn(User $user) => $user->name);
// 自定义键名
Route::get('/posts/{post:slug}', fn(Post $post) => $post->title);
// 显式绑定(在 RouteServiceProvider 中)
Route::bind('user', function (string $value) {
return User::where('slug', $value)->firstOrFail();
});10. 各框架路由对比
| 特性 | Laravel | Symfony | ThinkPHP | Yii |
|---|---|---|---|---|
| 定义方式 | PHP 代码 / Annotation | PHP 代码 / Attribute | PHP 代码 | PHP 代码 / Config |
| 参数语法 | {id} | {id} | :id | <id:\d+> |
| 资源路由 | Route::resource() | #[Route] Attribute | Route::resource() | Yii::$app->urlManager |
| 路由缓存 | route:cache | cache:warmup | route:build | 配置缓存 |
| RESTful | 原生支持 | 配置式 | 原生支持 | 配置式 |
| URL 生成 | route() | path() | url() | Url::to() |
最佳实践
1. RESTful 设计
php
<?php
// 好的 RESTful 路由设计
Route::apiResource('users', UserController::class);
Route::apiResource('posts', PostController::class);
// 避免动词出现在 URL 中
// ❌ 错误
Route::get('/getUser', ...);
Route::post('/createUser', ...);
Route::post('/deleteUser', ...);2. API 版本管理
php
<?php
// Laravel API 版本管理
Route::prefix('api/v1')->group(function () {
Route::apiResource('users', V1\UserController::class);
Route::apiResource('posts', V1\PostController::class);
});
Route::prefix('api/v2')->group(function () {
Route::apiResource('users', V2\UserController::class);
Route::apiResource('posts', V2\PostController::class);
});3. 路由文件组织
routes/
├── web.php # Web 路由(CSRF 保护、Session)
├── api.php # API 路由(无状态认证)
├── channels.php # 广播频道路由
├── console.php # Artisan 命令路由
└── admin.php # 后台管理路由下一节
继续学习:中间件 — 了解 HTTP 中间件的概念和实现。