Router.php 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. /*
  3. * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
  4. * SPDX-License-Identifier: Apache-2.0
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * See the LICENSE file in the project root for the full license text.
  9. */
  10. declare(strict_types=1);
  11. namespace App\Http;
  12. /**
  13. * Tiny pattern router.
  14. *
  15. * Patterns use `{name}` for path segments. Numeric-looking captures are cast
  16. * to int; everything else stays a string. Handlers receive (Request, array $params).
  17. */
  18. final class Router
  19. {
  20. /** @var list<array{method:string,regex:string,names:list<string>,handler:callable}> */
  21. private array $routes = [];
  22. public function add(string $method, string $pattern, callable $handler): void
  23. {
  24. [$regex, $names] = self::compile($pattern);
  25. $this->routes[] = [
  26. 'method' => strtoupper($method),
  27. 'regex' => $regex,
  28. 'names' => $names,
  29. 'handler' => $handler,
  30. ];
  31. }
  32. public function get(string $p, callable $h): void { $this->add('GET', $p, $h); }
  33. public function post(string $p, callable $h): void { $this->add('POST', $p, $h); }
  34. public function patch(string $p, callable $h): void { $this->add('PATCH', $p, $h); }
  35. public function delete(string $p, callable $h): void { $this->add('DELETE', $p, $h); }
  36. public function dispatch(Request $req): Response
  37. {
  38. $methodMatchedButNotPath = false;
  39. foreach ($this->routes as $r) {
  40. if (!preg_match($r['regex'], $req->path, $m)) {
  41. continue;
  42. }
  43. if ($r['method'] !== $req->method) {
  44. $methodMatchedButNotPath = true;
  45. continue;
  46. }
  47. $params = [];
  48. foreach ($r['names'] as $i => $name) {
  49. $val = $m[$i + 1] ?? '';
  50. $params[$name] = ctype_digit($val) ? (int) $val : $val;
  51. }
  52. $result = ($r['handler'])($req, $params);
  53. if ($result instanceof Response) {
  54. return $result;
  55. }
  56. if (is_string($result)) {
  57. return Response::html($result);
  58. }
  59. if (is_array($result) || is_object($result) || is_bool($result) || is_null($result)) {
  60. return Response::json($result);
  61. }
  62. return Response::text((string) $result);
  63. }
  64. if ($methodMatchedButNotPath) {
  65. return Response::text('Method Not Allowed', 405);
  66. }
  67. return Response::text('Not Found', 404);
  68. }
  69. /** @return array{0:string,1:list<string>} */
  70. private static function compile(string $pattern): array
  71. {
  72. $names = [];
  73. $regex = preg_replace_callback(
  74. '/\{([A-Za-z_][A-Za-z0-9_]*)\}/',
  75. function ($m) use (&$names) {
  76. $names[] = $m[1];
  77. return '([^/]+)';
  78. },
  79. $pattern
  80. );
  81. return ['#^' . $regex . '$#', $names];
  82. }
  83. }