* SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the LICENSE file in the project root for the full license text. */ declare(strict_types=1); namespace App\Http; /** * Tiny pattern router. * * Patterns use `{name}` for path segments. Numeric-looking captures are cast * to int; everything else stays a string. Handlers receive (Request, array $params). */ final class Router { /** @var list,handler:callable}> */ private array $routes = []; public function add(string $method, string $pattern, callable $handler): void { [$regex, $names] = self::compile($pattern); $this->routes[] = [ 'method' => strtoupper($method), 'regex' => $regex, 'names' => $names, 'handler' => $handler, ]; } public function get(string $p, callable $h): void { $this->add('GET', $p, $h); } public function post(string $p, callable $h): void { $this->add('POST', $p, $h); } public function patch(string $p, callable $h): void { $this->add('PATCH', $p, $h); } public function delete(string $p, callable $h): void { $this->add('DELETE', $p, $h); } public function dispatch(Request $req): Response { $methodMatchedButNotPath = false; foreach ($this->routes as $r) { if (!preg_match($r['regex'], $req->path, $m)) { continue; } if ($r['method'] !== $req->method) { $methodMatchedButNotPath = true; continue; } $params = []; foreach ($r['names'] as $i => $name) { $val = $m[$i + 1] ?? ''; $params[$name] = ctype_digit($val) ? (int) $val : $val; } $result = ($r['handler'])($req, $params); if ($result instanceof Response) { return $result; } if (is_string($result)) { return Response::html($result); } if (is_array($result) || is_object($result) || is_bool($result) || is_null($result)) { return Response::json($result); } return Response::text((string) $result); } if ($methodMatchedButNotPath) { return Response::text('Method Not Allowed', 405); } return Response::text('Not Found', 404); } /** @return array{0:string,1:list} */ private static function compile(string $pattern): array { $names = []; $regex = preg_replace_callback( '/\{([A-Za-z_][A-Za-z0-9_]*)\}/', function ($m) use (&$names) { $names[] = $m[1]; return '([^/]+)'; }, $pattern ); return ['#^' . $regex . '$#', $names]; } }