Routing
Routing maps a URL to a module and an action — the two names Quiote needs to find the code that answers a request. It builds on the Symfony Routing component, but wraps it so routes carry Quiote’s _module / _action metadata and support reverse generation (building a URL from a route name and parameters).
This page covers how to declare routes, how path placeholders reach your action, and how to customise the HTTP-verb-to-method mapping. You declare routes two ways, and they coexist in the same route collection:
- a
Routingsubclass (programmatic), and #[Route]attributes on action classes.
How routing fits in a request
Section titled “How routing fits in a request”Routing is one stop in Quiote’s PSR-15 middleware pipeline. The pipeline is built once and reused; RoutingMiddleware is the stage that turns a URL into a target action:
- It matches the request path against the
RouteCollectionyourroutingfactory role builds (yourRoutingsubclass, plus any merged#[Route]attributes). - From the matched route it reads the
_moduleand_actiondefaults, and maps the HTTP verb to an action-method token (read,write, …) viaHttpMethodMapper. - It packages all of that into an
ActionDescriptorand stores it on the request. Everything downstream — security, validation, dispatch — reads the action from that descriptor.
Where it sits in the pipeline:
… →
ContentNegotiationMiddlewarepicks the output type →RoutingMiddlewareresolves the action and builds theActionDescriptor→OutputTypeSyncMiddlewarelets a route override the negotiated type →SecurityMiddlewarechecks access →ValidationMiddlewarevalidates input (and promotes route placeholders into parameters) →DispatchMiddlewareruns the action and renders the view.
See The request lifecycle for the full path.
A Routing subclass
Section titled “A Routing subclass”The supported, general way to declare routes is a subclass of Quiote\Routing\Routing that builds a RouteCollection in build(). You point the routing factory role at this class (see Configuration).
<?phpnamespace App\Routing;
use Quiote\Routing\AttributeRoutes;use Quiote\Routing\Routing;use Symfony\Component\Routing\Route;use Symfony\Component\Routing\RouteCollection;
final class AppRouting extends Routing{ protected function build(): array { $routes = new RouteCollection(); $meta = [];
$routes->add('index', new Route('/', ['_module' => 'Default', '_action' => 'Index'])); $meta['index'] = ['gen_path' => '/', 'path' => '/', 'cut' => false];
$routes->add('post', new Route('/blog/{slug}', ['_module' => 'Blog', '_action' => 'Post'])); $meta['post'] = ['gen_path' => '/blog/{slug}', 'path' => '/blog/{slug}', 'cut' => false];
// Merge in routes declared via #[Route] attributes: AttributeRoutes::mergeInto($routes, $meta);
return [$routes, $meta]; }}Two things every route needs:
- A path — a Symfony route pattern, with
{placeholders}for dynamic segments. - The
_moduleand_actiondefaults — they tell the dispatcher which action to run. An optional_output_typedefault fixes the output type for the route.
The addRoute() helper
Section titled “The addRoute() helper”Building Route objects and meta entries by hand is explicit but verbose. addRoute() does both in one call and supports a parent/child hierarchy:
protected function build(): array{ $this->addRoute('/', ['name' => 'index', 'module' => 'Default', 'action' => 'Index']); $this->addRoute('/blog/{slug}', ['name' => 'post', 'module' => 'Blog', 'action' => 'Post']);
return [$this->getRouteCollection(), $this->getMeta()];}The signature is:
public function addRoute(string $pattern, array $opts = [], ?string $parent = null): string$pattern— the route pattern. If it does not start with/and a$parentis given, it is appended to the parent’s pattern (so children extend their parent’s path).$opts— supportsname(auto-generated if omitted),defaults, and the promoted keysmodule,action,locale,output_type.$parent— the name of a parent route to nest under.
It returns the final route name.
Attribute routing
Section titled “Attribute routing”For routes that live naturally next to the action they trigger, declare them with #[Route] on the action class. AttributeRoutes::mergeInto() (called from your build()) scans modules and merges these into the same collection.
<?phpnamespace App\Modules\Blog\Actions;
use Quiote\Action\Action;use Quiote\Request\WebRequest;use Quiote\Routing\Attribute\Route;
#[Route('/blog/{slug}', name: 'post', methods: ['GET'])]class PostAction extends Action{ public function executeRead(WebRequest $rd) { return 'Success'; }}Note the attribute goes on the class, not a method. This is deliberate: a Quiote action is one class exposing several HTTP-verb methods (executeRead, executeWrite), the opposite of Symfony’s one-controller-method-per-route model. A class can carry more than one #[Route] (the attribute is repeatable).
module and action are not attribute fields — they are derived from the action’s location in the module tree, the same mapping Controller::createActionInstance() uses, in reverse.
The #[Route] parameters:
| Parameter | Meaning |
|---|---|
path | Symfony route path, e.g. /products/{id} |
name | Route name; derived from module+action if omitted |
methods | HTTP methods accepted; empty means all |
requirements | Per-parameter regex requirements |
defaults | Extra route defaults, merged under module/action |
host | Route host pattern |
condition | Symfony ExpressionLanguage condition |
priority | Higher matches first |
outputType | Output type this route resolves to |
Which style to use
Section titled “Which style to use”They are not mutually exclusive — pick per route:
- Use a
Routingsubclass for the app’s route map, especially routes with hierarchy, shared prefixes, or generated patterns. - Use
#[Route]attributes for routes you want to read right next to the action, and for module-local routes that ship with a reusable module.
Route parameters in an action
Section titled “Route parameters in an action”Path placeholders arrive as request parameters, read through WebRequest::getParameter() — the same call you use for any other input. Reading one requires a declared validator, as shown below:
public function executeRead(WebRequest $rd){ // requires a validator declared for 'slug' (see below) $slug = $rd->getParameter('slug'); // ... return 'Success';}Route placeholders are not exempt from strict parameter validation. Here is what happens to a {slug} on the way to your action:
RoutingMiddlewarematches the route and records the placeholder value.ValidationMiddleware, before your action runs, promotes it into a request parameter withWebRequest::setUnvalidatedParameter(). This stages the value so a validator can see and check it, but does not whitelist it forgetParameter().- A validator declared for
slugchecks the value and whitelists it. With no validator, the staged value is pruned after validation.
So a {slug} with no validator is treated exactly like an unvalidated query or body field: getParameter('slug') throws UnvalidatedParameterAccessException (unless you pass a default).
Customising the HTTP verb mapping
Section titled “Customising the HTTP verb mapping”Which HTTP verb runs which action method is decided by Quiote\Execution\HttpMethodMapper. The defaults are:
| Verb | Token | Method |
|---|---|---|
GET, HEAD, OPTIONS, TRACE | read | executeRead |
POST | write | executeWrite |
PUT, PATCH | update | executeUpdate |
DELETE | remove | executeRemove |
You can override or extend this map with the routing.http_method_map setting. Your entries are merged onto the defaults — you only list what you want to change:
return [ // ... 'routing.http_method_map' => [ 'PATCH' => 'write', // route PATCH to executeWrite instead of executeUpdate 'LOCK' => 'lock', // adds a non-standard verb, calling executeLock() ],];routing.http_method_map: PATCH: write # route PATCH to executeWrite instead of executeUpdate LOCK: lock # adds a non-standard verb, calling executeLock()<!-- Config/settings.xml — note prefix="routing." on the wrapper --><settings prefix="routing."> <setting name="http_method_map"> <ae:parameter name="PATCH">write</ae:parameter> <ae:parameter name="LOCK">lock</ae:parameter> </setting></settings>Or set it programmatically before the map is first read:
use Quiote\Config\Config;
Config::set('routing.http_method_map', ['LOCK' => 'lock']);Rules:
- Keys (verbs) are matched case-insensitively; values (tokens) are lowercased.
- The token becomes the method name,
ucfirst-ed: tokenlockcallsexecuteLock(). Any action meant to handle that verb needs the matching method. - The token also names the per-method validation hooks — token
lockmaps toregisterLockValidators(),validateLock(),handleLockError()(see Validation). - An unmapped verb falls back to the
readtoken.
Note the XML tab: a routing.* key needs the prefix attribute on the enclosing <settings> element, because a bare <setting> compiles to a core. key — see The XML prefix attribute. The Actions and views page describes how the resolved method is called.
Generating URLs
Section titled “Generating URLs”The routing object generates URLs in reverse from a route name and parameters via gen():
public function __construct(private readonly Routing $routing) {}
// ...$url = $this->routing->gen('post', ['slug' => 'hello-world']);// resolves to /blog/hello-worldQuiote\Routing\Routing is bound in the container, so any action, view or service that generates URLs declares it in its constructor.