0

Route storage — a data structure for routes

Intermediate5 min read·fw-03-001

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@method string.
  • 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
<?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));
    }
}