Skip to content

路由机制

路由(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:build

2. 路由性能

优化方式说明
路由缓存将路由编译为静态文件,避免每次请求重新解析
精确匹配优先静态路由(无参数)优先于动态路由
正则缓存编译后的正则表达式缓存
路由数量控制避免注册过多路由

详细说明(续)

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. 各框架路由对比

特性LaravelSymfonyThinkPHPYii
定义方式PHP 代码 / AnnotationPHP 代码 / AttributePHP 代码PHP 代码 / Config
参数语法{id}{id}:id<id:\d+>
资源路由Route::resource()#[Route] AttributeRoute::resource()Yii::$app->urlManager
路由缓存route:cachecache:warmuproute: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 中间件的概念和实现。

参考链接