5: Routing
Routing in Phlo maps a space-separated path + HTTP method to a target (usually a method).
Routes from all .phlo files are collected; the router is activated with app::route().
5.1: Basic form
route [async|both] [GET|POST|PUT|DELETE|PATCH|QUERY] pad [pad2 ...] => target
- Paths are written with spaces (no
/in the route definition). - Target: a direct call or statement block.
Example:
route GET home => $this->main
method main => view($this->home)
QUERY (RFC 10008) is a safe, idempotent read that carries a request body, for a lookup whose
parameters do not fit cleanly in a URL. %payload decodes the QUERY body the same way it decodes
POST (JSON, form-urlencoded, multipart), and HTTP() takes a matching QUERY: argument for calling
out to another QUERY endpoint.
route QUERY search => $this->search
method search => dx(%payload->filters)5.2: sync / async / both
| Keyword | Behavior |
|---|---|
| (omitted) | Sync only (regular HTTP) |
async |
Async only (requests from a Phlo frontend) |
both |
Sync and async allowed |
route both GET data => $this->loadData
route async POST items save => $this->saveItems5.3: Variables
Phlo parses every path segment. Segments that start with $ are variables with extra capabilities:
4.3.1 Required (passed to the target)
route GET user $id => $this->showUser($id)
method showUser($id) => view($this->profile)
4.3.2 Optional presence with ? → boolean
route GET search $full? => $this->search($full)
- Matches
/searchand/search/full. $fullis true when the segmentfullis present, otherwise false. (The implementation literally checks whether the request segment equals the name without the?.)
4.3.3 Rest (variable length) with =*
route GET file $path=* => $this->serveFile($path)
- Matches all remaining segments as one string in
$path.
4.3.4 Default value with =
route GET page $slug=home => $this->page($slug)
- Without a segment →
$slug = 'home'. - With a segment →
$slug = '<value>'.
4.3.5 Length requirement with .N
route GET code $pin.6 => $this->enter($pin)
- Matches only when the length of
$pinis exactly 6.
4.3.6 Value lists with :a,b,c
route GET report $range:daily,weekly,monthly => $this->report($range)
- The segment must be one of the listed values.
- When the segment is absent (empty) and a default is set, the default is applied.
- Otherwise the match fails.
You can combine these forms. Examples:
Enum with a required id:
route GET export $fmt:csv,json $id => $this->export($fmt, $id)
Enum with a default:
route GET theme $name:light,dark=light => $this->theme($name)5.4: Payload check with `@`
You specify exact body keys with a single @ and a comma-separated list.
The router compares this 1-to-1 with the keys from %payload (exact set; order as supplied by the engine).
route POST user @name,email => $this->createUser
method createUser => dx(%payload->name, %payload->email)
Body keys are not bound as method parameters; you read them via
%payload.
5.5: Targets
Local method
route GET profile show => $this->show
method show => view($this->profile)
External class method (static)
route GET api version $major => api::getVersion($major)
- Static calls: parentheses required, even without arguments.
- Pass path variables explicitly.
What a route may return
The dispatcher inspects the return value for exactly one thing: false means "not my route" and matching continues with the next candidate. Every other return value is discarded.
Lesson. The assumption "a route returns its response body" produces
route GET hello => 'Hello': the route matches and serves a 200 with an empty page. A route produces output exclusively throughview(),apply(),output(),location()or the%resAPI.
The false contract is also a tool: a catch-all route GET guide $slug that returns false for unknown slugs lets a literal route in a later file (GET guide index.json) still match the same URL.
Principle: respond without a redirect. Request and response are decoupled, so a route can forward the work to another routine and return its output, answering in one request instead of redirecting. For an async request, return
apply()/view()directly withpath:(the SPA swaps in place, no second request); for a sync POST that just re-shows a page, forward to that page's routine rather than callinglocation().location()stays the right tool for a sync request that must canonicalize the URL (POST-redirect-GET, or a hard navigation), which the engine itself does when a syncview()receives a differing stringpath:. The aim is to avoid the unnecessary roundtrip, not to ban redirects.
5.6: Activating the router
Routes are only matched after:
app::route()
Place this call, for example, in app.phlo (or another central controller) after your app initialization and before a fallback for 404 handling.
5.7: Recommended structure
- Put routes at the top of each file.
-
Keep path and method name logically aligned:
route GET users list => $this->listUsers route POST users add => $this->addUser - Always pass variables in the target.
- Use
bothonly when an endpoint deliberately needs to be both sync and async. - Use value lists
:…instead of separateifbranches for fixed variants.