Routing

Preflow supports two routing styles that work side by side: file-based routing for pages and attribute-based routing for controllers. File routes resolve to component mode (templates); attribute routes resolve to action mode (controller methods).

File-Based Routing

Templates in app/pages/ map to URLs by their file path. No configuration needed — drop a .twig file and it becomes a route.

Conventions

File URL Pattern
index.twig / (or parent directory path)
about.twig /about
blog/index.twig /blog
blog/[slug].twig /blog/{slug}
docs/[...path].twig /docs/{path} (catches nested paths)
_layout.twig Excluded (underscore prefix)

Dynamic Segments

Use brackets for dynamic URL segments:

app/pages/
  blog/
    [slug].twig        → /blog/hello-world
  users/
    [id].twig          → /users/42

The parameter is available in the template as a variable:

<h1>{{ slug }}</h1>

Catch-All Segments

A [...param] file matches any depth of nested paths:

app/pages/
  docs/
    [...path].twig     → /docs/getting-started
                        → /docs/guides/routing
                        → /docs/a/b/c/d

The full matched path is available as the path variable.

Layout Files

Files prefixed with _ are excluded from routing. Use _layout.twig as a base layout:

{# app/pages/_layout.twig #}
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>{% block title %}App{% endblock %}</title>
  {{ head() }}
</head>
<body>
  {% block content %}{% endblock %}
  {{ assets() }}
</body>
</html>

Page templates extend the layout:

{# app/pages/about.twig #}
{% extends '_layout.twig' %}

{% block title %}About{% endblock %}

{% block content %}
  <h1>About Us</h1>
{% endblock %}

Attribute-Based Routing

Controllers use PHP attributes for route definitions. The class gets a #[Route] prefix; methods get HTTP method attributes.

use Preflow\Routing\Attributes\Delete;
use Preflow\Routing\Attributes\Get;
use Preflow\Routing\Attributes\Middleware;
use Preflow\Routing\Attributes\Post;
use Preflow\Routing\Attributes\Put;
use Preflow\Routing\Attributes\Route;

#[Route('/api/posts')]
#[Middleware(ApiAuthMiddleware::class)]
final class PostController
{
    #[Get('/')]
    public function index(): ResponseInterface { /* ... */ }

    #[Get('/{id}')]
    public function show(): ResponseInterface { /* ... */ }

    #[Post('/')]
    public function create(): ResponseInterface { /* ... */ }

    #[Put('/{id}')]
    #[Middleware(OwnerMiddleware::class)]
    public function update(): ResponseInterface { /* ... */ }

    #[Delete('/{id}')]
    public function destroy(): ResponseInterface { /* ... */ }
}

Method paths append to the class prefix: #[Get('/{id}')] on #[Route('/api/posts')] becomes /api/posts/{id}. A method path of '/' resolves to the prefix alone.

Route Middleware

#[Middleware] is repeatable on both class and method. Method middleware stacks on top of class middleware:

#[Route('/admin')]
#[Middleware(AuthMiddleware::class)]       // applied to all methods
final class AdminController
{
    #[Get('/')]
    public function dashboard(): ResponseInterface { /* ... */ }

    #[Delete('/users/{id}')]
    #[Middleware(AdminOnlyMiddleware::class)]  // stacked on AuthMiddleware
    public function deleteUser(): ResponseInterface { /* ... */ }
}

Route Priority

When multiple routes could match a request, RouteMatcher uses three priority passes:

  1. Static -- exact string match, no parameters
  2. Dynamic -- routes with {param} segments, matched by regex
  3. Catch-all -- routes with {...param}, matches across slashes

The first match wins. If nothing matches, a NotFoundHttpException is thrown.

Route Caching

In production, compile routes to a PHP file to skip directory scanning:

use Preflow\Routing\RouteCompiler;

$compiler = new RouteCompiler();

// Generate cache
$compiler->compile($router->getCollection(), __DIR__ . '/storage/routes.php');

// Invalidate
$compiler->clear(__DIR__ . '/storage/routes.php');

The compiled file is a plain PHP return statement -- OPcache-friendly with no eval.

Listing Routes

Use the CLI to see all registered routes:

php preflow routes:list