本文目录导读:

在 PHP 中实现基于正则表达式的路由匹配是一个常见的需求,以下是几种实现方式,从简单到进阶:
基础正则路由实现
<?php
class Router {
private $routes = [];
// 添加路由规则
public function add($pattern, $callback, $method = 'GET') {
$this->routes[] = [
'pattern' => $pattern,
'callback' => $callback,
'method' => $method
];
}
// 匹配并分发请求
public function dispatch($uri, $method = 'GET') {
// 去除查询字符串
$uri = strtok($uri, '?');
// 去除末尾的斜杠(保留根目录)
if ($uri !== '/' && substr($uri, -1) === '/') {
$uri = rtrim($uri, '/');
}
foreach ($this->routes as $route) {
// 检查请求方法
if ($route['method'] !== $method) {
continue;
}
// 正则匹配
if (preg_match($route['pattern'], $uri, $matches)) {
// 移除第一个完整匹配
array_shift($matches);
// 调用回调函数
return call_user_func_array($route['callback'], $matches);
}
}
// 404 处理
return $this->notFound();
}
private function notFound() {
return "404 Not Found";
}
}
// 使用示例
$router = new Router();
// 简单的首页路由
$router->add('#^/$#', function() {
return "首页";
});
// 带参数的路由
$router->add('#^/user/(\d+)$#', function($id) {
return "用户ID: " . $id;
});
// 多个参数
$router->add('#^/blog/(\d+)/(\w+)$#', function($id, $slug) {
return "文章: ID={$id}, 标题={$slug}";
});
// 可选参数
$router->add('#^/category/(?:(\w+)/)?$#', function($category = null) {
return "分类: " . ($category ?: "全部");
});
// 测试
echo $router->dispatch('/'); // 输出: 首页
echo $router->dispatch('/user/123'); // 输出: 用户ID: 123
echo $router->dispatch('/blog/456/my-post'); // 输出: 文章: ID=456, 标题=my-post
改进版:支持命名参数
<?php
class NamedRouter {
private $routes = [];
// 将命名参数转换为正则
private function compileRoute($route) {
// 将 {name} 转换为命名组 (?P<name>[^/]+)
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route);
return '#^' . $pattern . '$#';
}
public function add($path, $callback, $method = 'GET') {
$this->routes[] = [
'path' => $path,
'regex' => $this->compileRoute($path),
'callback' => $callback,
'method' => $method
];
}
public function dispatch($uri, $method = 'GET') {
$uri = strtok($uri, '?');
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
if (preg_match($route['regex'], $uri, $matches)) {
// 只保留命名参数
$params = array_filter($matches, function($key) {
return !is_int($key);
}, ARRAY_FILTER_USE_KEY);
// 调用回调函数
return call_user_func($route['callback'], $params);
}
}
return $this->notFound();
}
private function notFound() {
http_response_code(404);
return "404 Not Found";
}
}
// 使用示例
$router = new NamedRouter();
// 命名参数路由
$router->add('/user/{id}', function($params) {
return "用户ID: " . $params['id'];
});
$router->add('/blog/{year}/{month}', function($params) {
return "归档: {$params['year']}-{$params['month']}";
});
// 可选参数需要用正则自定义
$router->add('/search/{query}/{page?}', function($params) {
$page = isset($params['page']) ? $params['page'] : 1;
return "搜索: ".$params['query']." 第".$page."页";
});
echo $router->dispatch('/user/123');
echo $router->dispatch('/blog/2024/12');
echo $router->dispatch('/search/php/2');
完整的正则路由框架示例
<?php
class Router {
private $routes = [];
private $errorHandler;
// 支持约束的正则路由
public function add($method, $pattern, $handler, $constraints = []) {
$this->routes[] = [
'method' => strtoupper($method),
'pattern' => $this->buildPattern($pattern, $constraints),
'handler' => $handler,
'dynamic' => $this->hasDynamicSegments($pattern)
];
}
private function buildPattern($pattern, $constraints) {
// 将 {param:regex} 或 {param} 替换
$pattern = preg_replace_callback('/\{(\w+)(?::([^}]+))?\}/', function($matches) use ($constraints) {
$param = $matches[1];
$regex = isset($matches[2]) ? $matches[2] : '[^/]+';
// 应用约束
if (isset($constraints[$param])) {
$regex = $constraints[$param];
}
return '(?P<' . $param . '>' . $regex . ')';
}, $pattern);
return '#^' . $pattern . '$#';
}
private function hasDynamicSegments($pattern) {
return strpos($pattern, '{') !== false;
}
public function get($pattern, $handler, $constraints = []) {
$this->add('GET', $pattern, $handler, $constraints);
}
public function post($pattern, $handler, $constraints = []) {
$this->add('POST', $pattern, $handler, $constraints);
}
public function dispatch($uri, $method = 'GET') {
// 解析 URL
$uri = parse_url($uri, PHP_URL_PATH);
$method = strtoupper($method);
// 缓存匹配结果(可以添加缓存逻辑)
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
if (preg_match($route['pattern'], $uri, $matches)) {
// 提取参数
$params = [];
foreach ($matches as $key => $value) {
if (!is_int($key)) {
$params[$key] = $value;
}
}
// 调用处理函数
$result = call_user_func($route['handler'], $params);
// 处理响应
if ($result instanceof Response) {
return $result->send();
}
return $result;
}
}
// 404 处理
if ($this->errorHandler) {
return call_user_func($this->errorHandler);
}
return "404 Not Found";
}
public function setErrorHandler($handler) {
$this->errorHandler = $handler;
}
public function getRoutes() {
return $this->routes;
}
}
// 简单响应类
class Response {
private $content;
private $status = 200;
private $headers = [];
public function __construct($content, $status = 200) {
$this->content = $content;
$this->status = $status;
}
public function send() {
http_response_code($this->status);
foreach ($this->headers as $key => $value) {
header("$key: $value");
}
echo $this->content;
}
public function withHeader($key, $value) {
$this->headers[$key] = $value;
return $this;
}
}
// 使用示例
$router = new Router();
// 路由规则
$router->get('/', function() {
return "首页";
});
// 数字约束
$router->get('/user/{id}', function($params) {
return "用户: " . $params['id'];
}, ['id' => '\d+']);
// 字母约束
$router->get('/user/name/{name}', function($params) {
return "用户名: " . $params['name'];
}, ['name' => '[a-zA-Z]+']);
// 复杂正则
$router->get('/date/{year}/{month}/{day}', function($params) {
return "日期: {$params['year']}-{$params['month']}-{$params['day']}";
}, [
'year' => '\d{4}',
'month' => '(0[1-9]|1[0-2])',
'day' => '(0[1-9]|[12]\d|3[01])'
]);
// 通配符路由
$router->get('/file/*', function($params) {
return "文件路径";
});
// 设置404处理
$router->setErrorHandler(function() {
return new Response("页面不存在", 404);
});
// 测试
echo $router->dispatch('/');
echo $router->dispatch('/user/123'); // 匹配成功
echo $router->dispatch('/user/abc'); // 不匹配(期望数字)
echo $router->dispatch('/date/2024/12/25'); // 匹配成功
高级功能:路由分组和中间件
<?php
class AdvancedRouter {
private $routes = [];
private $middlewares = [];
private $prefix = '';
public function group($prefix, $callback) {
$previousPrefix = $this->prefix;
$this->prefix = $previousPrefix . $prefix;
$callback($this);
$this->prefix = $previousPrefix;
}
public function use($middleware) {
$this->middlewares[] = $middleware;
}
public function add($method, $pattern, $handler, $middlewares = []) {
$pattern = $this->prefix . $pattern;
// 转换为正则
$regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
$this->routes[] = [
'method' => strtoupper($method),
'regex' => $regex,
'handler' => $handler,
'middlewares' => array_merge($this->middlewares, $middlewares)
];
}
public function dispatch($uri, $method) {
$uri = parse_url($uri, PHP_URL_PATH);
foreach ($this->routes as $route) {
if ($route['method'] !== strtoupper($method)) {
continue;
}
if (preg_match($route['regex'], $uri, $matches)) {
// 提取参数
$params = [];
foreach ($matches as $key => $value) {
if (!is_int($key)) {
$params[$key] = $value;
}
}
// 执行中间件
$request = ['params' => $params, 'uri' => $uri];
foreach ($route['middlewares'] as $middleware) {
$request = call_user_func($middleware, $request);
if ($request === false) {
return "Middleware 拦截";
}
}
// 调用处理函数
return call_user_func($route['handler'], $request['params']);
}
}
return "404 Not Found";
}
}
// 使用示例
$router = new AdvancedRouter();
// 全局中间件(日志)
$router->use(function($request) {
// error_log("访问: " . $request['uri']);
return $request;
});
// 路由分组
$router->group('/api', function($router) {
$router->add('GET', '/users/{id}', function($params) {
return json_encode(['id' => $params['id'], 'name' => 'User ' . $params['id']]);
});
$router->add('POST', '/users', function() {
return "创建用户";
}, [
function($request) { // 路由特定中间件
// 检查权限等
return $request;
}
]);
});
// 测试
echo $router->dispatch('/api/users/123', 'GET');
echo $router->dispatch('/api/users', 'POST');
正则路由的优化技巧
class OptimizedRouter {
private $routes = [];
private $compiledRoutes = [];
public function add($pattern, $handler) {
$this->routes[] = [
'pattern' => $pattern,
'handler' => $handler
];
}
// 预编译路由(提升性能)
public function compileRoutes() {
foreach ($this->routes as $route) {
$this->compiledRoutes[] = [
'regex' => preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route['pattern']),
'handler' => $route['handler']
];
}
}
// 使用缓存的正则匹配
public function match($uri) {
if (empty($this->compiledRoutes)) {
$this->compileRoutes();
}
// 使用数组缓存匹配尝试(如果有多个路由)
foreach ($this->compiledRoutes as $route) {
if (preg_match($route['regex'], $uri, $matches)) {
$params = array_filter($matches, function($key) {
return !is_int($key);
}, ARRAY_FILTER_USE_KEY);
return [
'handler' => $route['handler'],
'params' => $params
];
}
}
return null;
}
}
路由匹配测试工具
function testRouter(Router $router, array $tests) {
foreach ($tests as $test) {
$result = $router->dispatch($test['uri'], $test['method'] ?? 'GET');
$status = ($result === $test['expected']) ? '✓' : '✗';
echo "$status URI: {$test['uri']} 结果: " . var_export($result, true) . "\n";
if ($status === '✗') {
echo " 期望: " . var_export($test['expected'], true) . "\n";
}
}
}
- 基础正则路由:使用
preg_match直接匹配正则表达式 - 命名参数路由:将
{id}转为命名组(?P<id>...) - 约束路由:支持参数约束,如
{id:\d+} - 分组和中间件:支持路由分组和中间件处理
- 性能优化:预编译正则、缓存匹配结果
选择哪种实现取决于你的具体需求,对于简单的应用,基础实现就足够了;对于复杂的框架,可以考虑使用现成的路由库如 FastRoute、Symfony Routing 等。