Advanced Features
Dependency injection, URL generation, route caching, and diagnostics.
Table of contents
Dependency Injection
The RouteHandler automatically resolves controller method parameters in the following order:
ServerRequestInterface— the current PSR-7 request- Route parameters — matched by parameter name, with type coercion (
int,float,bool) - Container services — resolved from the PSR-11 container by type-hint
- Default values — used if available
- Nullable parameters — receive
null
#[Route('/orders/{id:\d+}', methods: ['GET'])]
public function show(
int $id, // route parameter (auto-cast)
ServerRequestInterface $request, // current request
OrderRepository $repo, // resolved from container
?LoggerInterface $logger = null, // container or default
): ResponseInterface {
// ...
}
Parameter resolution order means you can mix route parameters, the request object, and container services in any order in your method signature.
URL Generation
Generate URLs from named routes (reverse routing):
$registrar = new RouteRegistrar();
// Register named routes
$registrar->get('/users', [UserController::class, 'list'], name: 'users.list');
$registrar->get('/users/{id:\d+}', [UserController::class, 'show'], name: 'users.show');
// Create router
$router = new Router($container, $registrar->getRouteCollection());
// Generate URLs via the URL generator
$url = $router->getUrlGenerator()->generate('users.show', ['id' => 42]);
// => /users/42
$url = $router->getUrlGenerator()->generate('users.list', query: ['page' => 2, 'limit' => 10]);
// => /users?page=2&limit=10
Parameters are automatically URL-encoded. Extra parameters not in the route pattern are ignored. Missing required parameters throw MissingParametersException.
Absolute URLs
Set a base URL (scheme + host) to generate fully-qualified URLs:
$router->setBaseUrl('https://example.com');
$url = $router->getUrlGenerator()->generate('users.show', ['id' => 42], absolute: true);
// => https://example.com/users/42
If absolute: true is used without a configured base URL, BaseUrlNotSetException is thrown.
Using UrlGenerator Directly
use AsceticSoft\Waypoint\UrlGenerator;
$generator = new UrlGenerator($router->getRouteCollection(), 'https://example.com');
$url = $generator->generate('users.show', ['id' => 42]); // relative
$url = $generator->generate('users.show', ['id' => 42], absolute: true); // absolute
URL generation works with cached routes — route names and patterns are preserved in the cache file.
Route Caching
Compile routes to a PHP file for zero-overhead loading in production.
Compiling the Cache
use AsceticSoft\Waypoint\Cache\RouteCompiler;
$registrar = new RouteRegistrar();
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers');
$compiler = new RouteCompiler();
$compiler->compile($registrar->getRouteCollection(), __DIR__ . '/cache/routes.php');
Loading from Cache
$cacheFile = __DIR__ . '/cache/routes.php';
$compiler = new RouteCompiler();
$router = new Router($container);
if ($compiler->isFresh($cacheFile)) {
$router->loadCache($cacheFile);
} else {
$registrar = new RouteRegistrar();
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers');
$compiler->compile($registrar->getRouteCollection(), $cacheFile);
$router = new Router($container, $registrar->getRouteCollection());
}
The compiler generates a self-contained PHP class with match expressions and pre-computed argument resolution plans. The resulting file loads through OPcache with zero overhead, bypassing all Reflection and attribute parsing at runtime.
Remember to clear the cache file after adding or modifying routes. During development, skip caching entirely.
Route Diagnostics
Inspect registered routes and detect potential issues:
use AsceticSoft\Waypoint\Diagnostic\RouteDiagnostics;
$diagnostics = new RouteDiagnostics($router->getRouteCollection());
// Print a formatted route table
$diagnostics->listRoutes();
// Detect conflicts
$report = $diagnostics->findConflicts();
if ($report->hasIssues()) {
$diagnostics->printReport();
}
Detected Issues
| Issue | Description |
|---|---|
| Duplicate paths | Routes with identical patterns and overlapping HTTP methods |
| Duplicate names | Multiple routes sharing the same name |
| Shadowed routes | A more general pattern registered earlier hides a more specific one |
Run diagnostics during development or in your CI pipeline to catch routing conflicts early.
Exception Handling
Waypoint throws specific exceptions for routing failures:
| Exception | HTTP Code | When |
|---|---|---|
RouteNotFoundException |
404 | No route pattern matches the URI |
MethodNotAllowedException |
405 | URI matches but HTTP method is not allowed |
RouteNameNotFoundException |
— | No route with the given name (URL generation) |
MissingParametersException |
— | Required route parameters not provided |
BaseUrlNotSetException |
— | Absolute URL requested but base URL not configured |
use AsceticSoft\Waypoint\Exception\RouteNotFoundException;
use AsceticSoft\Waypoint\Exception\MethodNotAllowedException;
try {
$response = $router->handle($request);
} catch (RouteNotFoundException $e) {
// Return 404 response
} catch (MethodNotAllowedException $e) {
// Return 405 response with Allow header
$allowed = implode(', ', $e->getAllowedMethods());
}
HEAD → GET fallback: Per RFC 7231 §4.3.2, if no route explicitly handles HEAD but a GET route matches the same URI, the GET route is used automatically. No additional configuration is required.
Architecture
Waypoint is designed around a modular architecture where each component has a single responsibility:
RouteRegistrar — fluent route registration, attribute loading, groups
Router (PSR-15 RequestHandlerInterface)
├── RouteCollection
│ ├── RouteTrie — prefix-tree for fast segment matching
│ └── Route[] — fallback linear matching for complex patterns
├── MiddlewarePipeline — FIFO PSR-15 middleware execution
├── RouteHandler — invokes controller with DI
├── UrlGenerator — reverse routing (name + params → URL)
├── RouteCompiler — compiles/loads route cache
└── RouteDiagnostics — conflict detection and reporting
The RouteRegistrar handles route building (manual registration, attribute loading, groups) and produces a RouteCollection. The Router is a pure PSR-15 RequestHandlerInterface responsible only for matching and dispatching.
The RouteTrie handles the majority of routes with O(1) per-segment lookups. Routes with patterns that cannot be expressed in the trie (mixed static/parameter segments like prefix-{name}.txt, or cross-segment captures) automatically fall back to linear regex matching.