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());
});
};