Advanced Routing

Advanced routing in Canvas covers wildcard matching, controller prefixes, request access, config-driven paths, and priority resolution.

explanation

Wildcard Routes

* matches exactly one segment; ** or {name:**} matches zero or more segments and captures the value.

/** @Route("/files/*") */
public function singleFile(): Response {
    // /files/image.jpg ✓   /files/a/b.jpg ✗
}

/** @Route("/files/{path:**}") */
public function nestedFiles(string $path): Response {
    // /files/docs/2024/report.pdf → $path = "docs/2024/report.pdf"
}

Route Prefixes

Apply @RoutePrefix to a controller to prepend a segment to every route it contains. Prefixes stack through inheritance.

/** @RoutePrefix("/api") */
class ApiController {}

/** @RoutePrefix("/v1") */
class ApiV1Controller extends ApiController {

    /** @Route("/users") */
    public function users(): Response {
        // Resolves to: /api/v1/users
    }
}

Config-Based Routes

Use file::key syntax to resolve a route path from a config file instead of hardcoding it. The prefix before :: determines which file Canvas reads from /config/; the suffix is the key within that file.

// /config/payment.php
return ['mollie_webhook' => '/webhooks/mollie'];

/** @Route("payment::mollie_webhook") */
public function mollieWebhook(): Response {}

/** @Route("payment::mollie_webhook", fallback="/webhooks/mollie") */
public function mollieWebhook(): Response {}

Without a fallback, a missing file or unknown key throws a RuntimeException. For routes that belong to a single action and won't change, an inline path is simpler.

Route Matching Priority

When multiple routes match, Canvas picks the most specific one (highest → lowest):

  1. Static/users/active
  2. Constrained parameter/users/{id:int}
  3. Unconstrained parameter/users/{name}
  4. Wildcard/users/* or /users/**
/** @Route("/users/active") */        // hits first for GET /users/active
public function activeUsers(): Response {}

/** @Route("/users/{id:int}") */      // hits for /users/42, not /users/active
public function userById(int $id): Response {}

/** @Route("/users/{name}") */        // fallback for any other single segment
public function userByName(string $name): Response {}