Routing
Routing configuration is defined in application modules in the file "etc/routing.php", located under the module's root
directory. This file must return a class that implements Amarant\Framework\Contract\Routing\RoutingConfiguratorInterface.
Important
In production mode, routing configuration is cached under "var/cache" in the application root directory. If changes are made, this cache has to be deleted for the new configuration to take effect.
Create a route
Path
Specifies the route's path.
Controller
The controller class. By default, the "__invoke" method will be called to execute the controller.
If a different method should be called, specify it by adding "::" and the method name.
For example:
Name
A unique name for the route.
Important
Use "_" , "." and alphanumeric characters only as route names. This is very important for page layouts whose
filenames are determined using the route name. The layout builder converts all "_" to "." and expects the layout files
to have a very specific naming convention.
Area
Frontend, backend or global. Specifying a route to be global makes it available in both frontend and backend area.
Methods
A list of HTTP methods the route should support.
Route parameters
To specify route parameters, add them in the path wrapped in curly brackets, like so:
To add parameter requirements, use a regular expression. For example, to require the parameters to be digits only:
To add an optional part of the path, wrap it in square brackets:
Warning
The optional part can only be the last part of the path.
Parameter resolving and injection
All route parameters will be resolved after the route is matched and passed onto the controller's executing method. The controller method should expect these parameters to all be in camelcase.
This means route parameters "param-a", "param-b" will be passed onto the method as "paramA", "paramB", which also means the method's signature should look something like this:
Or something like this if the parameters are supposed to be only digits:
The method can also declare additional parameters to be injected, for example this will inject the current request context:
The order of additional parameters in the method signature does not matter.
Additional parameter requirements
Route parameters can be additionally validated using regex or closures specified in route's "paramRequirements".
paramRequirements: [
// closure
'param-a' => fn ($param) => \is_string($param) && \str_contains($param, 'x'),
// regex
'param-b' => '/[^0-9]/',
]
Access and data scopes
To require the user to have specific access or data scopes to be able to access the route, add the following to route's "metadata":
metadata: [
// require the user to have these access scopes
Route::METADATA_REQUIRED_ACCESS_SCOPES => ['access_scope_a', 'access_scope_b'],
// require the user to have access to the default data scope
Route::METADATA_REQUIRED_DATA_SCOPES => [
\Amarant\Framework\Config\DefaultScopeContextProvider::createScopeValue(
\Amarant\Framework\Contract\Config\ScopeInterface::DEFAULT_SCOPE,
\Amarant\Framework\Contract\Config\ScopeInterface::DEFAULT_SCOPE
)
]
]
Default status code
To set a default status code a response should have when this route is matched, add the following to route's "metadata":
Note
This status code is applied only to layout results returned from controllers.
Hidden routes
Sometimes you may want a route to be able to forward to, but don't want the users to be able to visit it.
To make the router skip a route, add the following to route's "metadata":
Grouping
Routes can be added to groups.
- Unique group name. If the group already exists, it won't be added to the collection. You can check and fetch the existing group using hasGroup and getGroup on the route collection.
- Optional path prefix. The paths of all routes in a group will be prefixed by this value.
Notice how we now add the route to the group and not to the collection. If we should add it to the collection, it would also become a part of a group, but the default one, which has no prefix.
Note
A route is matchable no matter if their path ends with a slash (/) or not. When adding a base route to a group with a prefix, you could declare the group's prefix ending without a slash, and then setting the route's path to just "/".
Menu
To add menu entry for a route:
- Unique menu group id. If a group exists, it will be overwritten. Check if a group exists using getMenuGroup first.
- The group's title, usually rendered as a dropdown menu title.
- Sort order used when rendering menu groups.
- Optional icon class, usually added to the group's title as an "<i>" element.
- Route menus, by default, are meant to be used in the backend (back office).
- The menu group id under which the menu entry will be rendered, usually as a link within a dropdown menu item.
- The title of the entry, usually the title of the link.
- Sort order used when rendering menus inside a menu group.
Note
The menu is rendered only in the backend (back office) theme Hadron. It's possible to use this functionality in the frontend, but you have to render it yourself and set the area parameter of such menu items to frontend.
Alias-only routes
Sometimes a route should be reachable only via an internal forward (see Forwarding) and not directly accessible by users. To exclude a route from the router while keeping it forward-able, add the following to route's metadata:
This is similar to METADATA_NO_ROUTER but the intent is different: METADATA_NO_ROUTER marks routes
that should never be targeted (e.g. internal-only fallbacks), while METADATA_ALIAS_ONLY marks routes
that are valid destinations for forwards but must not be directly visited.
Forwarding
A controller can internally forward the request to another route by returning a ForwardResult.
The application resolves the target route and calls its controller within the same request, without
issuing an HTTP redirect.
- The name of the route to forward to.
- Optional parameters passed to the target controller.
Note
Forwarding does not issue an HTTP redirect. The browser URL stays the same, and only one response is sent. The target route is resolved from the same area as the current request.
Controller argument providers
Beyond route path parameters, the framework automatically resolves additional arguments and injects them into
controller methods. This is done by argument providers — classes implementing
ControllerArgumentProviderInterface, tagged with DiEnum::CONTROLLER_ARGUMENT_PROVIDER.
Each provider implements supports(RequestContextInterface $requestContext): bool to declare when it applies,
and provide(RequestContextInterface $requestContext): array to return a map of parameter name → value.
Providers are checked in order; all that return true from supports() contribute their arguments.
JSON input (input)
JsonInputArgumentProvider applies when:
- The request method is
POST,PATCH,PUT, orDELETE. - The request body is valid JSON.
- The route defines an
inputclass (via->input(MyRequestDto::class)) that implementsJsonSerializableInterface. - The controller method has a parameter named
inputwhose type matches that class.
When it applies, the provider deserializes the JSON body into the input class using SerializerInterface,
then validates the result with Symfony Validator. A validation failure throws InputException.
Denormalization groups are taken from the route's operation configuration when an operationId is set.
public function __invoke(CreateProductInput $input): mixed
{
// $input is fully deserialized and validated
}
Form / multipart input (input)
RawInputArgumentProvider works identically to the JSON provider but applies when the request body is
not valid JSON — i.e. application/x-www-form-urlencoded or multipart/form-data. It merges
$request->request->all() and $request->files->all() into a single array and deserializes it into the
input class.
The controller signature and route definition are the same as for JSON input. The two providers are mutually exclusive: JSON content triggers the JSON provider; everything else triggers the raw provider.
Resource model
ResourceModelArgumentProvider applies when the route's resourceId is a DataModelInterface subclass
(i.e. the route is part of a resource definition). It injects the already-loaded resource item — fetched
earlier in the request lifecycle — directly into any controller parameter whose type matches a
DataModelInterface subclass.
public function __invoke(Product $product): mixed
{
// $product is the loaded entity identified by the route's resource ID
}
No additional route configuration is needed; the provider reads the loaded model from the request context.
Adding a custom argument provider
Implement ControllerArgumentProviderInterface and tag it with DiEnum::CONTROLLER_ARGUMENT_PROVIDER:
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
$container
->bind(
MyArgumentProvider::class,
fn (ConfigurableContainerInterface $container) => $container->getBindBuilder()
->type(MyArgumentProvider::class)
->build()
)
->tag([MyArgumentProvider::class], [DiEnum::CONTROLLER_ARGUMENT_PROVIDER]);
The provide() method returns a map of parameter names to values. Names must match the controller method's
parameter names exactly. Return only the parameters your provider is responsible for; other providers
contribute the rest.
Request lifecycle events
The HTTP application dispatches a series of events during request processing. These can be subscribed
to for things like authentication, rate limiting, response manipulation, or custom error handling.
All events are identified by constants on Amarant\Framework\Enum\EventEnum.
EventEnum::APP_REQUEST — RequestEvent
Fired before route matching begins. Subscribers can short-circuit the entire request by setting a response, or inject a route and parameters directly to skip the router.
| Method | Description |
|---|---|
getRequestContext() |
The current request context. |
setResponse(ResponseInterface) |
Set a response — skips routing and controller entirely. |
setRoute(Route) |
Provide a route — skips the router. |
setRouteParameters(array) |
Set route parameters alongside an injected route. |
EventEnum::APP_REQUEST_NO_ROUTE — NoRouteRequestEvent
Fired when the router finds no matching route. Subscribers can supply a fallback route or a response.
If neither is provided, a RoutingException is thrown.
| Method | Description |
|---|---|
getRequestContext() |
The current request context. |
setRoute(Route) |
Provide a fallback route to use instead. |
setRouteParameters(array) |
Set parameters for the fallback route. |
setResponse(ResponseInterface) |
Alternatively, respond directly. |
EventEnum::APP_REQUEST_ROUTE — RouteRequestEvent
Fired after a route has been matched, before the controller is resolved. Subscribers can swap the route, add or remove route parameters, or respond directly.
| Method | Description |
|---|---|
getRoute() |
The matched route. |
setRoute(Route) |
Replace the matched route. |
getRouteParams() / setRouteParams(array) |
Read or replace route parameters. |
addRouteParam(string, mixed) |
Add a single parameter. |
removeRouteParam(string) |
Remove a single parameter. |
setResponse(ResponseInterface) |
Short-circuit before the controller runs. |
EventEnum::APP_REQUEST_RESPONSE — ResponseRequestEvent
Fired after the controller has run and produced a response. Subscribers can inspect or replace the response.
| Method | Description |
|---|---|
getRoute() |
The matched route. |
getResponse() |
The response produced by the controller. |
setResponse(ResponseInterface) |
Replace the response. |
EventEnum::APP_REQUEST_EXCEPTION — ExceptionRequestEvent
Fired when an unhandled exception occurs anywhere during the request. The exception has already been logged. Subscribers can inspect the exception and replace the error response. The exception is always re-thrown in debug mode regardless of this event.
| Method | Description |
|---|---|
getException() |
The thrown exception. |
getRoute() |
The matched route at the time of the exception, if any. |
getResponse() |
The current error response. |
setResponse(ResponseInterface) |
Replace the error response. |
EventEnum::APP_REQUEST_RESPONSE_FINALIZED_BEFORE — ResponseFinalizedBeforeEvent
Fired just before the framework converts the internal response object to a Symfony Response.
Subscribers can still replace the response at this point.
EventEnum::APP_REQUEST_RESPONSE_FINALIZED — ResponseFinalizedRequestEvent
Fired after the Symfony Response object has been assembled, but before it is prepared for sending.
EventEnum::APP_REQUEST_RESPONSE_SEND_BEFORE — ResponseSendBeforeRequestEvent
Fired as the last step before the response is returned to the web server. Use for final inspection or logging; the response can no longer be replaced.
Request context
Amarant\Framework\Contract\Application\Request\RequestContextInterface is the central object available
throughout a request. It carries the Symfony Request, the matched Route, the resolved identity, and a
free-form metadata bag used to communicate between event subscribers and other request-scoped services.
Injecting the request context
In a constructor, always inject RequestContextContainerInterface and call currentOrNull() when you
need the context. The container is a shared singleton; the request context itself is request-scoped and
will not exist at construction time.
use Amarant\Framework\Contract\Application\Request\RequestContextContainerInterface;
final class SomeService
{
public function __construct(
private readonly RequestContextContainerInterface $requestContextContainer
) {}
public function doSomething(): void
{
$requestContext = $this->requestContextContainer->currentOrNull();
if ($requestContext === null) {
return;
}
// use $requestContext
}
}
In a controller method, RequestContextInterface can be declared directly as a parameter, and it will
be injected automatically alongside any route parameters (see Parameter resolving and injection).
use Amarant\Framework\Contract\Application\Request\RequestContextInterface;
final class SomeController
{
public function __invoke(RequestContextInterface $requestContext): mixed
{
// use $requestContext directly
}
}
Commonly used methods
| Method | Description |
|---|---|
getRequest(): Request |
The underlying Symfony Request object. |
getContext(): ContextInterface |
Application context — area, mode, debug flag. |
getRoute(): ?Route |
The currently matched route. |
getRouteParameters(): array |
Route parameters resolved for this request. |
getIdentity(): ?IdentityInterface |
The authenticated identity, or null for guests. |
setIdentity(?IdentityInterface) |
Set or clear the identity. |
getRequestParameter(string, mixed) |
Read a query/body parameter by name. |
getAllRequestParameters(): array |
All merged request parameters. |
getRequestId(): string |
The unique ID assigned to this request. |
isApi(): bool |
Whether this is an API request. |
getException(): ?Throwable |
The exception that occurred, if any. |
Metadata
The metadata bag (getMeta / setMeta / appendMeta) is the standard way for event subscribers and
services to pass data to later stages of the request without coupling directly to each other.
appendMeta(string $key, mixed $value) appends a value to an array stored under $key.
appendMetaUnique(string $key, int|string $value) does the same but skips duplicates.
The following constants on RequestContextInterface define the well-known metadata keys used by the framework.
META_RESPONSE_SET_COOKIES — add a cookie to the response
Append a Symfony\Component\HttpFoundation\Cookie instance to queue it for the response.
Always create cookies using Amarant\Framework\Contract\Factory\Application\Request\CookieFactoryInterface
rather than instantiating Cookie directly. The factory automatically sets the secure flag based on the
current request scheme and provides consistent defaults across the application.
use Amarant\Framework\Contract\Factory\Application\Request\CookieFactoryInterface;
use Amarant\Framework\Contract\Application\Request\RequestContextContainerInterface;
use Amarant\Framework\Contract\Application\Request\RequestContextInterface;
final class SomeService
{
public function __construct(
private readonly RequestContextContainerInterface $requestContextContainer,
private readonly CookieFactoryInterface $cookieFactory,
) {}
public function setCookie(): void
{
$requestContext = $this->requestContextContainer->currentOrNull();
$requestContext->appendMeta(
RequestContextInterface::META_RESPONSE_SET_COOKIES,
$this->cookieFactory->create(
name: 'my_cookie',
value: 'some-value',
path: '/',
httpOnly: true,
sameSite: Cookie::SAMESITE_STRICT,
ttl: 3600
)
);
}
}
META_RESPONSE_REMOVE_COOKIES — remove a cookie from the response
Append either a Cookie instance created via CookieFactoryInterface (to control path/flags precisely)
or a plain cookie name string to queue a cookie for removal.
// by name only
$requestContext->appendMeta(
RequestContextInterface::META_RESPONSE_REMOVE_COOKIES,
'my_cookie'
);
// with path/flag control — use the factory
$requestContext->appendMeta(
RequestContextInterface::META_RESPONSE_REMOVE_COOKIES,
$this->cookieFactory->create(name: 'my_cookie', value: null, path: '/')
);
Both queues are consumed by Amarant\Framework\EventSubscriber\Request\CookieEventSubscriber, which runs
on EventEnum::APP_REQUEST_RESPONSE_FINALIZED at priority -450. Cookies are applied to the final Symfony
Response object just before it is sent, after most other response processing has completed.
META_RESPONSE_TTL — page cache TTL (seconds)
Set an integer TTL on the current response. Used by CacheEventSubscriber to store the response in a
reverse-proxy cache when caching is enabled.
META_RESPONSE_LAYOUT_RENDERED_PAGE_TTL — layout page TTL (seconds)
Same purpose as META_RESPONSE_TTL but set by the layout renderer for full-page layout responses. Takes
precedence over META_RESPONSE_TTL when both are present.
META_RESPONSE_TAGS — cache tags
Set an array of string tags on the response. Used by CacheEventSubscriber to tag the cached entry so it
can be invalidated by tag.
$requestContext->setMeta(RequestContextInterface::META_RESPONSE_TAGS, ['product_42', 'category_7']);
META_CACHED — response served from cache
Set to true by CacheEventSubscriber when the response was served directly from cache. Other subscribers
can check this flag to skip work that is redundant for cached responses.
META_REQUEST_ID — request identifier
The unique UUID assigned to this request. Also, accessible via getRequestId().
Request context container
Amarant\Framework\Contract\Application\Request\RequestContextContainerInterface is a stack that holds
request contexts. The HTTP application pushes a context at the start of each request and the container
makes it available to any service that injects RequestContextInterface directly.
In tests and any code that dispatches a request manually, you must push a context yourself before calling services that depend on it:
$requestContext = $this->makeClient()->createRequestContext('/');
$container->get(RequestContextContainerInterface::class)->push($requestContext);
| Method | Description |
|---|---|
push(RequestContextInterface) |
Push a context onto the stack. |
current(): RequestContextInterface |
Get the current (top) context. Throws if empty. |
currentOrNull(): ?RequestContextInterface |
Same, returns null instead of throwing. |
pop(): ?RequestContextInterface |
Remove and return the top context. |
isEmpty(): bool |
Whether there is no active context. |
clear(): void |
Clear the entire stack. |