Routing

Router files map URL sub-paths to PHP closures. A page's view property determines which router handles its requests.

How routing works

On each request, PhlatPage walks the URL from the deepest segment upward until it finds a matching page. It then loads the router file for that page's view and matches the remaining sub-path against registered routes.

A page at /shop/ with view shop is handled by site/views/shop/router.php. The sub-path passed to the router is relative to the page URL, the same router file works wherever the page sits in the tree.

Router file structure

A router file returns a callable that receives $app and $page:

<?php

use Phlat\App;
use Phlat\Page;

return function (App $app, Page $page): void {
    $page->router->get('',        fn() => $page);
    $page->router->get('archive', fn() => ...);
    $page->router->post('contact', fn() => ...);
};

HTTP methods

get, post, put, patch, delete register one route for that HTTP verb; any matches every verb.

Route parameters

Token Matches
{any} one path segment, including empty
{all} everything after this point, across slashes, including empty
{alpha} one non-empty segment, letters only
{alphanum} one non-empty segment, letters and digits
{num} one non-empty segment, digits only
{slug} one non-empty segment, [a-zA-Z0-9_-]

Prefer the narrower tokens over {any} for genuine identifiers (field names, usernames, slugs) so a malformed or empty value 404s at the routing layer instead of reaching the handler. Captured values are passed as positional arguments:

$page->router->get('post/{slug}',        fn($slug) => ...);
$page->router->patch('pages/{all}',      fn($path) => ...);
$page->router->get('archive/{num}/{slug}', fn($year, $slug) => ...);

Handler return values

Return value Effect
Page (e.g. $page) Render that page
a different Page Render that page instead

Calling Res::redirect() or an Hx:: exit helper exits internally, no return value needed.

Auth guards: before() / after() hooks

Register a before hook on $page->router from any router file. It runs once, right before route matching, after every router file in the dispatch chain (the global views/router.php, then ancestor pages root-first, then this page) has loaded. A hook that wants to block the request just redirects or throws, there's no return-value convention to learn.

// Runs for every page under this section
$page->router->before(function ($app, $page) {
    if (!$app->session->isLoggedIn())
        Res::redirect('/-/login');
});

after hooks run once a route's handler has resolved a result page, right before it's returned, useful for logging or post-processing without repeating it in every handler.

Response helpers

Use Toolkit classes inside handlers, never raw PHP headers, superglobals, or echo:

Res::redirect('/thank-you');         // 302 redirect and exit
Res::html('<p>ok</p>');              // HTML fragment and exit
Res::code(403);                      // set response code only

$app->session->flash('flash', 'Saved.');           // write a flash message
$app->session->verifyCsrf($app->request->post('_csrf') ?? '');  // CSRF check (returns bool)

San::text($app->request->post('name') ?? '');      // sanitise user input
San::email($app->request->post('email') ?? '');    // sanitise email input
<?php

use Phlat\App;
use Phlat\Page;
use Phlat\Toolkit\Res;
use Phlat\Toolkit\San;

return function (App $app, Page $page): void {

    // Auth guard, runs once, before route matching
    $page->router->before(function ($app, $page) {
        if (!$app->session->isLoggedIn())
            Res::redirect('/-/login');
    });

    $page->router->get('', fn() => $page);

    $page->router->post('', function () use ($app, $page) {
        if (!$app->session->verifyCsrf($app->request->post('_csrf') ?? ''))
            Res::redirect($page->url());

        $name = San::text($app->request->post('name') ?? '');
        $app->session->flash('flash', "Hello, {$name}.");
        Res::redirect($page->url());
    });
};