Route storage — a data structure for routes
Concept
Route storage is the data structure that holds all registered routes. A router needs to store route definitions, retrieve them for matching, and organize them efficiently.
What a route stores:
- HTTP method(s):
GET,POST,PUT, etc. - URI pattern:
/users/{id}or regex-compiled form. - Action: closure or
Controller@methodstring. - Middleware: array of middleware class names.
- Name: optional route name for URL generation.
- Constraints: parameter patterns (
where('id', '[0-9]+')).
Route Collection: Routes are stored in a collection class (e.g., RouteCollection). It typically maintains multiple indexes for fast lookup:
- By method:
['GET' => [Route, Route, ...], 'POST' => [Route, ...]] - By name:
['dashboard' => Route, 'users.show' => Route] - By URI: for exact match fast path.
Route class: A value object representing a single route. Immutable after registration. Contains the pattern, action, constraints, and name.
Why index by method first: Reduces the routes to check during matching. A GET request only needs to check GET routes, cutting the search space.
Lazy compilation: URI patterns with {param} are compiled to regex on demand (or at registration time). Storing the compiled regex avoids recompiling per request.
Code Example
<?php
namespace Framework\Routing;
class Route
{
public ?string $name = null;
private ?string $compiledPattern = null;
private array $paramNames = [];
public function __construct(
public readonly string $method,
public readonly string $uri,
public readonly mixed $action,
public array $middleware = [],
public array $constraints = [],
) {}
public function name(string $name): static
{
$this->name = $name;
return $this;
}
public function where(string $param, string $pattern): static
{
$this->constraints[$param] = $pattern;
return $this;
}
public function getCompiledPattern(): string
{
if ($this->compiledPattern === null) {
$this->compile();
}
return $this->compiledPattern;
}
public function getParamNames(): array
{
if ($this->compiledPattern === null) {
$this->compile();
}
return $this->paramNames;
}
private function compile(): void
{
$pattern = $this->uri;
preg_match_all('/\{(\w+)\??\}/', $pattern, $matches);
$this->paramNames = $matches[1];
foreach ($matches[0] as $i => $placeholder) {
$paramName = $matches[1][$i];
$regex = $this->constraints[$paramName] ?? '[^/]+';
$optional = str_ends_with($placeholder, '?}');
$replacement = $optional
? "(?:/{$regex})?"
: "(?P<{$paramName}>{$regex})";
$pattern = str_replace($placeholder, $replacement, $pattern);
}
$this->compiledPattern = '#^' . $pattern . '$#';
}
}
class RouteCollection
{
private array $routes = []; // [method => [Route, ...]]
private array $namedRoutes = []; // [name => Route]
public function add(Route $route): void
{
$this->routes[$route->method][] = $route;
if ($route->name !== null) {
$this->namedRoutes[$route->name] = $route;
}
}
public function getByMethod(string $method): array
{
return $this->routes[strtoupper($method)] ?? [];
}
public function getByName(string $name): ?Route
{
return $this->namedRoutes[$name] ?? null;
}
public function all(): array
{
return array_merge(...array_values($this->routes));
}
}