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 → RouteDispatcher

Moonlight 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\MainSwitch

During AppRealm::boot(), the framework reads this section and calls router($app)->switch(MainSwitch::class, 'v1') for each entry.

INI keyINI valueResult
v1Application\Switches\MainSwitchRoutes under /api/v1/
v2Application\Switches\V2SwitchRoutes 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\V2Switch

Each switch registers:

RouteMethodPurpose
/api/v1/pingGETHealth check
/api/v1/POST{ "service", "action", ... } dispatch
/api/v1/{service}/{action}/GETOptional query-string dispatch
/api/v1/__catalogGETJSON 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

ApproachWhen to use
[app_switches] in settings.iniDefault for app API versions (scaffolded)
Provider::routes()Composer packages or shared app hooks
router($app)->switch() in codeRare; 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

ClassRole
RouteDefinitionSingle route (path, methods, defaults, requirements)
RouteTableNamed collection of routes
RouteMatcherCompiles regexes and matches path + HTTP method
CompiledRouteMatcherSingleton matcher; prefers bootstrap cache when enabled
RouteDispatcherInvokes the matched controller
PioniaRouterFluent API for switches and custom routes

Exceptions

ExceptionHTTP
RouteNotFoundException404
MethodNotAllowedException405
ResourceNotFoundException404 (domain resources)

Production bootstrap cache

When [performance] BOOTSTRAP_CACHE=true (or php pionia optimize --production), php pionia optimize writes:

FileContents
storage/bootstrap/routes.phpSerialized RouteTable
storage/bootstrap/providers.phpResolved provider class map

At boot:

  • CompiledRouteMatcher loads storage/bootstrap/routes.php when present
  • AppMixin::resolveProviders() loads storage/bootstrap/providers.php when 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 --production

Both 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:

  1. Add to environment/settings.ini:
[app_switches]
v1=Application\Switches\MainSwitch
  1. Point entry points at bootstrap/application.php only (public/index.php, pionia, worker.php).
  2. Delete bootstrap/routes.php.

v2 → v3 routing changes

v2v3
Symfony Routing (symfony/routing)Native RouteTable + RouteMatcher
Symfony HttpKernel dispatchRouteDispatcher
PioniaRouter::wireTo()router($app)->switch() (via [app_switches] or providers)
bootstrap/routes.php for switches[app_switches] in settings.ini
Route collection in Symfony formatRouteDefinition objects in RouteTable

Symfony HttpFoundation, HttpKernel, and Routing packages are removed from framework require. Pionia implements Request, Response, routing, and dispatch natively.