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:

  1. ServerRequestInterface — the current PSR-7 request
  2. Route parameters — matched by parameter name, with type coercion (int, float, bool)
  3. Container services — resolved from the PSR-11 container by type-hint
  4. Default values — used if available
  5. 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.