Routing

Route registration, parameters, groups, and attribute-based routing.

Table of contents

Manual Route Registration

Use RouteRegistrar to register routes with a fluent API. Shortcut methods are provided for common HTTP verbs:

use AsceticSoft\Waypoint\RouteRegistrar;

$registrar = new RouteRegistrar();

// Full form
$registrar->addRoute('/users', [UserController::class, 'list'], methods: ['GET']);

// Shortcuts
$registrar->get('/users',          [UserController::class, 'list']);
$registrar->post('/users',         [UserController::class, 'create']);
$registrar->put('/users/{id}',     [UserController::class, 'update']);
$registrar->delete('/users/{id}',  [UserController::class, 'destroy']);

// Any other HTTP method (PATCH, OPTIONS, etc.)
$registrar->addRoute('/users/{id}', [UserController::class, 'patch'], methods: ['PATCH']);

Once routes are registered, pass the collection to Router:

$router = new Router($container, $registrar->getRouteCollection());

Method Parameters

Parameter Type Default Description
$path string Route pattern (e.g. /users/{id})
$handler array\|Closure [Class::class, 'method'] or a closure
$middleware string[] [] Route-specific middleware class names
$name string '' Optional route name
$priority int 0 Matching priority (higher = first)

Route Parameters

Parameters use FastRoute-style placeholders:

// Basic parameter — matches any non-slash segment
$registrar->get('/users/{id}', [UserController::class, 'show']);

// Constrained parameter — only digits
$registrar->get('/users/{id:\d+}', [UserController::class, 'show']);

// Multiple parameters
$registrar->get('/posts/{year:\d{4}}/{slug}', [PostController::class, 'show']);

Parameters are automatically injected into the handler by name, with type coercion for scalar types:

$registrar->get('/users/{id:\d+}', function (int $id) {
    // $id is automatically cast to int
});

Use regex constraints like \d+ to restrict parameter formats. This prevents ambiguous matches and improves performance.


Attribute-Based Routing

Declare routes directly on controller classes using the #[Route] attribute:

use AsceticSoft\Waypoint\Attribute\Route;

#[Route('/api/users', middleware: [AuthMiddleware::class])]
class UserController
{
    #[Route('/', methods: ['GET'], name: 'users.list')]
    public function list(): ResponseInterface { /* ... */ }

    #[Route('/{id:\d+}', methods: ['GET'], name: 'users.show')]
    public function show(int $id): ResponseInterface { /* ... */ }

    #[Route('/', methods: ['POST'], name: 'users.create')]
    public function create(ServerRequestInterface $request): ResponseInterface { /* ... */ }

    #[Route('/{id:\d+}', methods: ['PUT'], name: 'users.update')]
    public function update(int $id, ServerRequestInterface $request): ResponseInterface { /* ... */ }

    #[Route('/{id:\d+}', methods: ['DELETE'], name: 'users.delete')]
    public function delete(int $id): ResponseInterface { /* ... */ }
}

The class-level #[Route] sets a path prefix and shared middleware. Method-level attributes define concrete routes. The attribute is repeatable, so a single method can handle multiple routes.

Loading Attributes

$registrar = new RouteRegistrar();

// Load specific controller classes
$registrar->loadAttributes(
    UserController::class,
    PostController::class,
);

// Or scan an entire directory
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers');

// Optionally filter by filename pattern
$registrar->scanDirectory(__DIR__ . '/Controllers', 'App\\Controllers', '*Controller.php');

Attribute Parameters

Parameter Type Default Description
$path string '' Path pattern (prefix on class, route on method)
$methods string[] ['GET'] HTTP methods (ignored on class-level)
$name string '' Route name
$middleware string[] [] Middleware (class-level prepended to method-level)
$priority int 0 Matching priority (higher = first)

Route Groups

Group related routes under a shared prefix and middleware:

$registrar->group('/api', function (RouteRegistrar $registrar) {

    $registrar->group('/v1', function (RouteRegistrar $registrar) {
        $registrar->get('/users', [UserController::class, 'list']);
        // Matches: /api/v1/users
    });

    $registrar->group('/v2', function (RouteRegistrar $registrar) {
        $registrar->get('/users', [UserV2Controller::class, 'list']);
        // Matches: /api/v2/users
    });

}, middleware: [ApiAuthMiddleware::class]);

Groups can be nested. Prefixes and middleware accumulate from outer to inner groups.


Priority-Based Matching

When multiple routes could match the same URL, use the $priority parameter to control which one wins:

// Higher priority = matched first
$registrar->get('/users/{action}',  [UserController::class, 'action'], priority: 0);
$registrar->get('/users/settings',  [UserController::class, 'settings'], priority: 10);

Routes with higher priority values are tested before routes with lower values. Routes with the same priority are matched in registration order.