Overview of Routing
Overview of Routing
Hazaar offers a robust and flexible routing system that enables seamless mapping of URLs to controllers and actions. This feature facilitates the creation of clean, intuitive URLs for your application.
Unlike other frameworks, Hazaar provides multiple methods for configuring routes, ensuring flexibility and ease of use. The router can be configured using the application.json file, with options to define routes in a PHP file, JSON file, or using attributes in controller classes. Hazaar also provides multiple automatic routing options, including basic and advanced routing methods.
By providing a variety of routing options, Hazaar allows developers to choose the most suitable method based on their application requirements and preferences.
By default, Hazaar uses the file router and loads routes from route.php. You can switch router types and change the route source file in app.php to suit each environment.
Router Modules
- File: Define routes in a
route.phpfile, ideal for routes not directly tied to a controller. - Basic: The simplest method, requiring no configuration. Controllers and their actions are automatically available as routes.
- Advanced: Similar to the basic method but supports nesting controllers in subdirectories.
- Attribute: Use attributes in controller classes to define routes, also extending the advanced router.
- Annotated: Use annotations in controller classes to define routes, extending the advanced router.
- JSON: Define routes in a JSON file, useful for dynamic route definitions from external configurations.
Warning
While the annotated router is still available and fully functional, it's use is discouraged in favor of the attribute router that provides a more efficient and structured way to define routes using PHP 8 attributes. Annotated routes use PHPDoc comments which require additional parsing and are less efficient.
Configuring the Router
The router configuration is specified in the app.php file under the router section:
<?php
return [
'development' => [
'router' => [
'type' => 'basic',
'controller' => 'main',
'action' => 'index',
]
],
];Tips
Router configuration lives under an environment key (such as development) in app.php. This lets you define different routing setups (and even different route sets) per environment, so make sure your router config matches the active runtime.
The type key determines the router type. Available types include:
file: Define routes in aroute.phpfile.basic: Automatically maps controllers to URLs with no configuration.advanced: Supports nesting controllers in subdirectories.attribute: Define routes using attributes in controller classes.annotated: Define routes using annotations in controller classes.json: Define routes in a JSON configuration file.
The controller and action keys specify the default controller and action to use when no route is matched. Or if the controller is matched but no action is specified then the method defined in the action key will be executed.
Callables
A callable is a PHP callback that can be used to execute a function or method. A callable can be a string (class name), an array (class and method with optional arguments), or a closure.
The target controller and method must return a Hazaar\Controller\Response based object, such as Hazaar\Controller\Response\Json or Hazaar\Controller\Response\View.
Class Method
A class method callable is defined as an array containing the class name and method name:
[ControllerClass::class, 'actionMethod']Warning
The target controller method must be non-static and public and not start with an underscore (_).
Class Method with Arguments
A class method callable with arguments is defined as an array containing the class name, method name, and arguments:
[ControllerClass::class, 'actionMethod', 'arg1', 'arg2']This callable executes the actionMethod method in the ControllerClass class with arg1 and arg2 as arguments. This is useful for passing additional data to the controller method or supplying default values that can be overridden by the URL.
Closure
A closure callable is defined as an anonymous function:
function() {
return new Hazaar\Controller\Response\Json(['message' => 'Hello, World!']);
}Important
Closure callables can only be used with the file router or when registering routes programmatically. The JSON, attribute and annotated routers cannot reference a closure — use a class method or class string callable with those backends instead.
Class Strings
A class string callable is defined as a string containing the class name:
'ControllerClass'This callable executes the default method in the ControllerClass class.
'ControllerClass::actionMethod'This callable executes the actionMethod method in the ControllerClass class.
'ControllerClass::actionMethod(arg1, arg2)'Response Types & Declaring Route Intent
A route's response type — HTML, JSON, XML, etc. — is normally negotiated per-request from the client's Accept header. But you can also declare a route's intent up front, by return-type-hinting the controller action with one of the Hazaar\Controller\Response subtypes:
use Hazaar\Controller\Response\JSON;
public function status(): JSON
{
return new JSON(['ok' => true]);
}The router inspects this return type via reflection and records it against the matched Route (Route::getResponseType()). This declared intent is more than documentation — it's the response type an authentication/API endpoint like this stays consistent with even when something goes wrong before the action itself gets to run (a middleware failure, a routing error, an uncaught exception). In that situation the framework's generic error controller has no request of its own to negotiate against, so it falls back to whatever the route that was supposed to handle the request declared, ahead of the client's Accept header. This is what guarantees an endpoint that always returns JSON on success also always returns JSON on failure, without every action needing its own try/catch to force the format manually.
Caution
Avoid using mixed return types (or omitting the return type entirely) as the router will not be able to determine the actual response type. Declared intent is skipped for such routes, and both successful and error responses fall back to standard Accept-header negotiation, defaulting to HTML when that header is missing or ambiguous.
Available response types include:
Hazaar\Controller\Response\HTML: HTML response. This is the default response type if no other type can be determined.Hazaar\Controller\Response\JSON: JSON response.Hazaar\Controller\Response\XML: XML response.Hazaar\Controller\Response\Text: Plain text response.Hazaar\Controller\Response\PDF: Binary response. Used if the response is a file download or other binary data such as a PDF.
Tips
Only the response types listed above are recognised by return-type reflection (see ResponseType::getControllerResponseTypeName()). A return type of Hazaar\Controller\Response\File, or any other Response subtype, is treated the same as an undeclared intent and resolves to HTML.
Accept-header negotiation fallback
When a route has no declared intent (or none was matched at all, e.g. a 404), the response format for both successful and error responses is negotiated from the request, in this order:
Acceptheader, parsed as a weighted list (application/json, text/plain;q=0.9) — the first supported media type in preference order wins.- If
Acceptis missing or only*/*: aContent-Type: application/jsonrequest body, or anX-Requested-With: XMLHttpRequestheader, are treated as a preference for JSON. - Otherwise,
HTML.
Route Parameters
Define route parameters in the URL pattern using curly braces {}. The parameter type can be specified using a colon (:) followed by the type. Such as {int:id} or {string:name}. This is not supported by the basic or advanced routers as they do not support custom routes.
Available types include:
int: Integer.float: Floating-point number.string: String.bool: Boolean.
Values are matched based on the type specified. For example, {int:id} will only match integer values. If the value does not match the specified type, the route will not be matched. The value is then passed to the controller method as an argument of the converted type using the name specified in the URL.
Important
Values are passed as arguments to the controller method by name. So a route of /product/{int:id} would pass the id value to the controller method's $id argument.
public function getProduct(int $id) {
// Retrieve the product with the specified ID
}Example:
Router::get('/product/{int:id}', [API::class, 'getProduct']);[
{
"route": "/product/{int:id}",
"controller": "Application\\Controller\\Product",
"action": "getProduct",
"method": "GET"
}
]#[Route('/product/{int:id}', methods: ['GET'])]
public function getProduct(int $id) {
// Retrieve the product with the specified ID
}/**
* @route("/product/{int:id}", methods={"GET"})
*/
public function getProduct(int $id) {
// Retrieve the product with the specified ID
}Default Values
Default values can be specified for route parameters by providing a default value in the callable. This is useful for providing default values when the parameter is not provided in the URL. The array of default values is passed as the third argument to the callable and can be either a numeric array or an associative array.
If the default values are numeric, they are passed to the controller method in the order they are defined. If the default values are associative, they are passed to the controller method by name.
In the file router these are supplied as the third element of the callable array; in the JSON router they are supplied via the args property.
Numeric Array
Numeric arrays are passed to the controller method in the order they are defined.
Router::get('/product/{int:id}', [API::class, 'getProduct', [1234]]);[
{
"route": "/product/{int:id}",
"controller": "Application\\Controller\\Product",
"action": "getProduct",
"method": "GET",
"args": [1234]
}
]Associative Array
Associative arrays are passed to the controller method by name.
Router::get('/product/{int:id}', [API::class, 'getProduct', ['id' => 1234]]);[
{
"route": "/product/{int:id}",
"controller": "Application\\Controller\\Product",
"action": "getProduct",
"method": "GET",
"args": {"id": 1234}
}
]Custom Matchers
Types answer the question "is this an int?". Custom matchers answer "is this the right shape of string?". They constrain a parameter to a specific format, which lets routes that would otherwise collide coexist unambiguously.
Consider two routes, /get/{uuid:id} and /get/history. Without a matcher, {id} (or {string:id}) matches any string, so /get/history could be captured by the first route depending on definition order. A UUID has a very specific format, so constraining the parameter with the built-in uuid matcher means /get/history no longer matches the first route and falls through to the literal one, while /get/{uuid:id} only matches a genuine UUID.
Important
Matchers are validate-only. They decide whether a segment matches but never transform its value — the raw string is passed to your controller. When you need the value coerced to a type, use a native type such as {int:id}, which already handles conversion.
Built-in Matchers
The following matchers are registered on every router by default:
| Matcher | Matches | Example |
|---|---|---|
uuid | A canonical UUID (8-4-4-4-12 hex) | 6ba7b810-9dad-11d1-80b4-00c04fd430c8 |
ulid | A 26-character Crockford base32 ULID | 01ARZ3NDEKTSV4RRFFQ69G5FAV |
slug | A lowercase, hyphen-separated slug | my-first-post |
date | An ISO-8601 date (YYYY-MM-DD) | 2026-08-12 |
Reference a matcher in a route the same way you reference a type — {matcher:name}:
Router::get('/product/{uuid:id}', [API::class, 'getProduct']);[
{
"route": "/product/{uuid:id}",
"controller": "Application\\Controller\\Product",
"action": "getProduct",
"method": "GET"
}
]#[Route('/product/{uuid:id}', methods: ['GET'])]
public function getProduct(string $id) {
// Retrieve the product with the specified UUID
}/**
* @route("/product/{uuid:id}", methods={"GET"})
*/
public function getProduct(string $id) {
// Retrieve the product with the specified UUID
}Registering Custom Matchers
Built-in matchers cover the common cases, but you can also register your own. Because matchers are registered in PHP — via Router::matcher() or a router instance — registration happens in your PHP route file or during bootstrap. See File Router: Custom Matchers for the details. Once registered, a custom matcher can be referenced from any of the routers listed below, exactly like a built-in one.
Important
There is an important distinction between referencing and registering a matcher:
- Referencing a matcher (
{matcher:name}) is supported by the same routers that support custom routes — file, JSON, attribute and annotated — not the basic or advanced routers. - Registering a matcher (built-in or custom) is done in PHP, so new matchers can only be defined via the file router or programmatic bootstrap. You cannot define a matcher inside a JSON file or an attribute.
Matchers apply only to segment-based parameters and are not combined with full-regex routes, which remain a separate catch-all mechanism.
Middleware
You can attach middleware to specific routes to handle authentication, authorization, or other logic for individual endpoints. Route middleware is specified using the middleware() chaining method for file routes, or the middleware property for JSON routes. You may use either a middleware class name or an alias (defined in your config).
Example (file route):
Router::get('/admin', [AdminController::class, 'dashboard'])
->middleware('auth'); // 'auth' is an alias defined in configExample (JSON route):
[
{
"route": "/admin",
"controller": "Application\\Controller\\Admin",
"action": "dashboard",
"method": "GET",
"middleware": "auth"
}
]Aliases are mapped to middleware class names in your configuration:
<?php
return [
'middleware' => [
'aliases' => [
'auth' => 'App\\Middleware\\RequireAuth',
],
],
];For more details, see Middleware: Route Middleware.