HTTP routing
Pionia v3 ships a native routing layer — no Symfony Routing dependency. Routes are collected in a RouteTable, matched by RouteMatcher, and dispatched through RouteDispatcher.
Mental model
public/index.php (or pionia / worker.php)
→ require bootstrap/application.php
→ AppRealm::create() boots once (singleton)
→ [app_switches] in settings.ini → router()->switch(...)
→ framework default routes (/docs, /stats, static, SPA fallback)
→ provider routes (optional, during bootOnce)
→ WebKernel → CompiledRouteMatcher → RouteDispatcherMoonlight API traffic still flows Switch → Service → Action. Routing decides which controller handles the HTTP path (API switch, static files, /docs, /stats, etc.).
Registering API switches (default)
New apps scaffold [app_switches] in environment/settings.ini. No bootstrap/routes.php file is required.
[app_switches]
v1=Application\Switches\MainSwitchDuring AppRealm::boot(), the framework reads this section and calls router($app)->switch(MainSwitch::class, 'v1') for each entry.
| INI key | INI value | Result |
|---|---|---|
v1 | Application\Switches\MainSwitch | Routes under /api/v1/ |
v2 | Application\Switches\V2Switch | Routes under /api/v2/ |
Keys are API version segments; values are switch class names (must implement SwitchContract / extend ApiSwitch).
Multiple versions
[app_switches]
v1=Application\Switches\MainSwitch
v2=Application\Switches\V2SwitchEach switch registers:
| Route | Method | Purpose |
|---|---|---|
/api/v1/ping | GET | Health check |
/api/v1/ | POST | { "service", "action", ... } dispatch |
/api/v1/{service}/{action}/ | GET | Optional query-string dispatch |
/api/v1/__catalog | GET | JSON action catalog (debug/docs gate) |
Version v2 gets the same pattern under /api/v2/.
See API versioning in Moonlight for when to add a version.
Bootstrap entry points
All HTTP, CLI, and worker processes boot the same realm:
// bootstrap/application.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Pionia\Realm\AppRealm;
return AppRealm::create(__DIR__);// public/index.php
(require __DIR__ . '/../bootstrap/application.php')->bootHttp();// pionia (CLI)
exit((require __DIR__.'/bootstrap/application.php')->bootConsole());AppRealm::create() is a singleton — repeated require of application.php returns the same instance. Helpers (app(), realm(), router()) are available after the realm boots.
Other ways to register routes
| Approach | When to use |
|---|---|
[app_switches] in settings.ini | Default for app API versions (scaffolded) |
Provider::routes() | Composer packages or shared app hooks |
router($app)->switch() in code | Rare; dynamic registration in a provider onBooted() hook |
Package / provider routes
public function routes(PioniaRouter $router): PioniaRouter
{
return $router->switch(BillingSwitch::class, 'v1');
}Provider routes register during WebApplication::bootOnce() after app switches from settings are already wired.
Programmatic switches (advanced)
If you must register switches in PHP (not INI), do it from a service provider — not a separate routes.php file:
public function onBooted(): void
{
router(app())->switch(LegacySwitch::class, 'v1');
}Core classes
| Class | Role |
|---|---|
RouteDefinition | Single route (path, methods, defaults, requirements) |
RouteTable | Named collection of routes |
RouteMatcher | Compiles regexes and matches path + HTTP method |
CompiledRouteMatcher | Singleton matcher; prefers bootstrap cache when enabled |
RouteDispatcher | Invokes the matched controller |
PioniaRouter | Fluent API for switches and custom routes |
Exceptions
| Exception | HTTP |
|---|---|
RouteNotFoundException | 404 |
MethodNotAllowedException | 405 |
ResourceNotFoundException | 404 (domain resources) |
Production bootstrap cache
When [performance] BOOTSTRAP_CACHE=true (or php pionia optimize --production), php pionia optimize writes:
| File | Contents |
|---|---|
storage/bootstrap/routes.php | Serialized RouteTable |
storage/bootstrap/providers.php | Resolved provider class map |
At boot:
CompiledRouteMatcherloadsstorage/bootstrap/routes.phpwhen presentAppMixin::resolveProviders()loadsstorage/bootstrap/providers.phpwhen present
Regenerate after changing [app_switches], provider routes, or framework route defaults:
php pionia optimize --no-scaffold --no-preload
# or full production preset:
php pionia optimize --productionBoth files are gitignored — generate them on each deploy.
storage/bootstrap/routes.php is a generated route cache, not the old app bootstrap file. Application switches are configured in settings.ini.
Migrating from bootstrap/routes.php
Older v3 apps (or tutorials) may still have:
// bootstrap/routes.php ← removed in current scaffolds
$app = require __DIR__ . '/application.php';
router($app)->switch(MainSwitch::class, 'v1');
return $app;Migrate:
- Add to
environment/settings.ini:
[app_switches]
v1=Application\Switches\MainSwitch- Point entry points at
bootstrap/application.phponly (public/index.php,pionia,worker.php). - Delete
bootstrap/routes.php.
v2 → v3 routing changes
| v2 | v3 |
|---|---|
Symfony Routing (symfony/routing) | Native RouteTable + RouteMatcher |
| Symfony HttpKernel dispatch | RouteDispatcher |
PioniaRouter::wireTo() | router($app)->switch() (via [app_switches] or providers) |
bootstrap/routes.php for switches | [app_switches] in settings.ini |
| Route collection in Symfony format | RouteDefinition objects in RouteTable |
Symfony HttpFoundation, HttpKernel, and Routing packages are removed from framework require. Pionia implements Request, Response, routing, and dispatch natively.
Related
- Requests & responses — Moonlight envelopes and request data
- Application structure — bootstrap layout
- API versioning — multiple switches
- App providers — package routes
- Production performance — preload, bootstrap cache, OPcache
- Upgrading from v2 — bootstrap migration