|
|
@@ -0,0 +1,141 @@
|
|
|
+<?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)) {
|
|
|
+ $params[$key] = $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);
|
|
|
+ }
|
|
|
+}
|