}> */ private array $routes = []; public function add(string $method, string $pattern, array|callable|string $handler, array $middleware = []): void { $this->routes[] = [ 'method' => strtoupper($method), 'pattern' => $pattern, 'regex' => $this->compile($pattern), 'handler' => $this->normalizeHandler($handler, $pattern), 'middleware' => $middleware, ]; } public function get(string $pattern, array|callable|string $handler, array $middleware = []): void { $this->add('GET', $pattern, $handler, $middleware); } public function post(string $pattern, array|callable|string $handler, array $middleware = []): void { $this->add('POST', $pattern, $handler, $middleware); } public function any(string $pattern, array|callable|string $handler, array $middleware = []): void { $this->add('*', $pattern, $handler, $middleware); } /** * 把声明式处理器规范化为闭包。 * PHP 8 起 ['类名', '非静态方法'] 不能经类名字符串直接调用(不再自动实例化), * 这里统一包装为实例调用,路由表保持简洁声明。 */ private function normalizeHandler(array|callable|string $handler, string $pattern): callable { if (is_array($handler) && isset($handler[0], $handler[1]) && is_string($handler[0]) && is_string($handler[1])) { [$class, $method] = $handler; return static fn(array $params = []) => (new $class())->{$method}($params); } if (!is_callable($handler)) { throw new \InvalidArgumentException("路由 {$pattern} 的处理器不是可调用项"); } return $handler; } /** * 按注册顺序匹配 URL 路径。 * @return array{handler:callable, params:array, middleware:list}|null */ public function match(string $path, string $method): ?array { $method = strtoupper($method); foreach ($this->routes as $route) { if ($route['method'] !== '*' && $route['method'] !== $method) { continue; } if (preg_match($route['regex'], $path, $matches)) { $params = []; foreach ($matches as $key => $value) { if (is_string($key)) { // 路径参数做百分号解码(rawurldecode 不把 + 当空格); // 输出安全由模板默认转义兜底。 $params[$key] = rawurldecode($value); } } return [ 'handler' => $route['handler'], 'params' => $params, 'middleware' => $route['middleware'], ]; } } return null; } /** 编译 URL 模式为正则(支持静态段、:param、[可选段],可选段不可嵌套)。 */ private function compile(string $pattern): string { $out = ''; $rest = $pattern; while ($rest !== '') { $pos = strpos($rest, '['); if ($pos === false) { $out .= $this->compileSegment($rest); break; } $out .= $this->compileSegment(substr($rest, 0, $pos)); $end = strpos($rest, ']', $pos); if ($end === false) { throw new \InvalidArgumentException("路由模式存在未闭合的 '[': {$pattern}"); } $inner = substr($rest, $pos + 1, $end - $pos - 1); $out .= '(?:' . $this->compileSegment($inner) . ')?'; $rest = substr($rest, $end + 1); } return '~^' . $out . '$~'; } /** 编译不含可选段的一段模式:按 / 分段,:name → 命名捕获组;前导/尾部空段忽略。 */ private function compileSegment(string $segment): string { if ($segment === '') { return ''; } $parts = []; foreach (explode('/', $segment) as $piece) { if ($piece === '') { continue; // 忽略前导/重复/尾部斜杠 } if ($piece[0] === ':') { $name = substr($piece, 1); if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) { throw new \InvalidArgumentException("非法路由参数名: {$name}"); } $parts[] = '/(?P<' . $name . '>[^/]+)'; } else { $parts[] = '/' . preg_quote($piece, '~'); } } return $parts === [] ? '/' : implode('', $parts); } }