| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- <?php
- declare(strict_types=1);
- namespace Glacier\Core;
- /**
- * URL 模式引擎(自研,零依赖)。
- *
- * 核心原则:URL 与物理路径完全解耦 —— 本类只消费 URL 模式与参数,
- * 从不把 URL 段拼进文件系统路径。
- *
- * 模式语法:
- * - 静态段: /about
- * - 参数段: /user/:id (:name 为命名参数)
- * - 可选段: /news[/:page]
- * - 方法: GET / POST / '*'(任意)
- */
- final class Router
- {
- /** @var list<array{method:string, pattern:string, regex:string, handler:callable, middleware:list<string>}> */
- 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<string,string>, middleware:list<string>}|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);
- }
- }
|