-
Notifications
You must be signed in to change notification settings - Fork 2
route
This guide covers everything related to routing in Webrium: defining routes, organizing them into groups, and protecting them with middleware.
Routes are defined using the Route class and are typically placed in app/Routes/web.php, then loaded with Route::source() from your application's entry point.
use Webrium\Route;
Route::get('/', function () {
return 'Hello, World!';
});Webrium supports all the common HTTP verbs:
Route::get($uri, $handler);
Route::post($uri, $handler);
Route::put($uri, $handler);
Route::patch($uri, $handler);
Route::delete($uri, $handler);
Route::any($uri, $handler);any() matches the given URI regardless of HTTP method.
Each verb method also accepts an optional third argument — a route name (see Named Routes).
Plain HTML forms can only submit GET or POST. To target a PUT, PATCH, or DELETE route from a form, submit a POST request with a _method field holding the intended verb:
<form method="POST" action="/users/42">
<input type="hidden" name="_method" value="DELETE">
<!-- ... -->
</form>Webrium reads _method on incoming POST requests and routes the request as the spoofed method. Only PUT, PATCH, and DELETE can be spoofed; any other value is ignored and the real POST method is used.
A route handler can be a closure:
Route::get('/ping', function () {
return 'pong';
});Or a reference to a controller method, using either of two equivalent syntaxes:
// String syntax
Route::get('/users', 'UserController@index');
// Array syntax (IDE-friendly — supports autocomplete and refactoring)
Route::get('/users', [UserController::class, 'index']);The string form resolves the controller class inside App\Controllers (so 'UserController@index' becomes App\Controllers\UserController). The array form uses the fully-qualified class name you pass in. See Controllers for details on how dispatching works, including the boot() and teardown() lifecycle hooks.
Capture segments of the URI using {parameter} syntax:
Route::get('/users/{id}', function ($id) {
return "User #$id";
});
Route::get('/posts/{category}/{slug}', 'PostController@show');namespace App\Controllers;
class PostController
{
public function show($category, $slug)
{
return "Category: $category, Slug: $slug";
}
}Important — parameters are matched by name, not position. Internally, captured values are passed to your handler keyed by their placeholder name (e.g.
category,slug). This means the parameter name in your closure or controller method must match the{placeholder}name in the route — the order of the arguments in your function signature does not matter, but the spelling does.// Works — names match (order is irrelevant) Route::get('/posts/{category}/{slug}', function ($category, $slug) { /* ... */ }); Route::get('/posts/{category}/{slug}', function ($slug, $category) { /* ... */ });The same rule applies to controller methods.
You can constrain a parameter to a built-in pattern with {name:type}. If the captured segment does not match the type, the route is treated as a non-match (and matching continues to the next route, eventually falling through to the 404 handler):
Route::get('/users/{id:int}', 'UserController@show'); // only matches numeric ids
Route::get('/posts/{slug:slug}', 'PostController@show'); // letters, digits, '-' and '_'
Route::get('/tokens/{token:uuid}', 'TokenController@show'); // a v4-style UUIDThe available types are:
| Type | Matches |
|---|---|
int |
An optionally-signed integer (e.g. 42, -7) |
alpha |
Letters only (a–z, A–Z) |
alnum |
Letters and digits |
slug |
Letters, digits, hyphens, and underscores |
uuid |
A canonical 8-4-4-4-12 hexadecimal UUID |
A trailing ? marks a parameter as optional: {name?} or, combined with a type, {name?:type}. An optional parameter may be omitted from the URI; when omitted it is simply absent from the parameters passed to your handler, so give it a default in your signature:
Route::get('/posts/{page?:int}', function ($page = 1) {
return "Page $page";
});Optional parameters are only meaningful at the end of the pattern — once an optional segment is omitted, no later segment may be present.
You can assign a name to a route as a third argument:
Route::get('/dashboard', 'DashboardController@index', 'dashboard');Alternatively, name the most recently registered route fluently with ->name():
Route::get('/dashboard', 'DashboardController@index')->name('dashboard');Route names must be unique; registering the same name twice triggers an error via Debug.
Generate a URL for a named route using the route() helper or Route::route():
$url = route('dashboard'); // /dashboardFor routes with parameters, pass an associative array of values:
Route::get('/users/{id}', 'UserController@show', 'users.show');
$url = route('users.show', ['id' => 42]); // /users/42Parameter values are percent-encoded, so values containing slashes, spaces, or other reserved characters cannot corrupt the resulting URL. A typed placeholder such as {id:int} is substituted the same way — the :type suffix is stripped from the generated URL. If a required parameter is missing, or a named route doesn't exist, an error is triggered via Debug.
Use Route::source() to load one or more route files. By default, files are resolved from the routes directory:
Route::source(['web.php', 'api.php']);Pass a second argument to load route files from a different directory or registered alias — useful for modular applications or packages that ship their own routes:
// Load from a directory alias registered with Directory
Route::source(['shop.php'], 'modules/shop/routes');
// Load from an absolute path
Route::source(['admin.php'], '/var/www/admin/routes');If a file in the list doesn't exist, an error is triggered.
By default, unmatched requests return a 404 with a plain "Page not found" message. You can override this with a custom handler:
Route::setNotFoundHandler(function () {
return 'Sorry, the page you requested could not be found.';
});
// or a controller
Route::setNotFoundHandler('ErrorController@notFound');Note:
view()is not a built-in Webrium helper — Webrium's core package does not ship a templating/view layer. If you've added your ownview()function (or one from a separate package) to render HTML templates, you can return its output from the handler just like any other string:Route::setNotFoundHandler(function () { return view('errors.404'); // only works if you've defined a view() helper yourself });
Route groups let you share a URL prefix and/or middleware across multiple routes without repeating them.
Route::group() accepts either a string (used as a prefix) or an array of options, followed by a closure where the grouped routes are defined.
use Webrium\Route;
Route::group('admin', function () {
Route::get('/dashboard', 'AdminController@dashboard'); // /admin/dashboard
Route::get('/users', 'AdminController@users'); // /admin/users
});Route::group(['prefix' => 'api'], function () {
Route::get('/users', 'UserController@index'); // /api/users
Route::get('/posts', 'PostController@index'); // /api/posts
});Add a middleware key to apply middleware to every route inside the group:
Route::group(['prefix' => 'api', 'middleware' => 'App\Middlewares\AuthMiddleware'], function () {
Route::get('/profile', 'ProfileController@show');
Route::post('/profile', 'ProfileController@update');
});Any of the middleware forms described in Middleware — closures, class names, Class@method, function names, booleans, or arrays of these — can be used here.
The prefix key is optional. You can apply middleware to a group of routes without changing their URLs:
Route::group(['middleware' => 'App\Middlewares\AuthMiddleware'], function () {
Route::get('/dashboard', 'DashboardController@index');
Route::get('/settings', 'SettingsController@index');
});Groups can be nested. Prefixes are concatenated across levels, and middleware accumulates across levels:
Route::group(['prefix' => 'api'], function () {
Route::get('/status', 'ApiController@status'); // /api/status, no middleware
Route::group(['prefix' => 'admin', 'middleware' => 'App\Middlewares\AuthMiddleware@isAdmin'], function () {
Route::get('/users', 'AdminController@users'); // /api/admin/users
});
});Here /api/status has no middleware, while /api/admin/users is prefixed with api/admin and protected by AuthMiddleware@isAdmin.
Middleware stacks across nested groups. When an inner group declares its own
middleware, it is merged with the middleware inherited from every enclosing group — the inner routes run the full chain from the outside in. (This is a change from earlier versions, where an inner group's middleware replaced the outer one.)Route::group(['prefix' => 'api', 'middleware' => 'App\Middlewares\AuthMiddleware'], function () { Route::get('/status', 'ApiController@status'); // protected by AuthMiddleware Route::group(['prefix' => 'admin', 'middleware' => 'App\Middlewares\AdminMiddleware'], function () { // protected by BOTH AuthMiddleware and AdminMiddleware, in that order Route::get('/users', 'AdminController@users'); }); });Inherited middleware runs first, then the inner group's. You can still list several on one group explicitly when you are not relying on nesting:
Route::group(['prefix' => 'admin', 'middleware' => ['App\Middlewares\AuthMiddleware', 'App\Middlewares\AdminMiddleware']], function () { Route::get('/users', 'AdminController@users'); });
Middleware provides a way to run logic before a route's handler executes — typically for authentication, authorization, logging, or rate limiting. If middleware fails, the request is stopped immediately and the route handler never runs.
Middleware in Webrium is applied through route groups — there is no per-route ->middleware() method. Wrap any routes that need protection in a Route::group() call.
Middleware checks run after a route has matched the current request, and before its handler is dispatched. If no route matches, middleware is never evaluated at all — the request goes straight to the 404 handler.
Each middleware returns a value that decides the outcome. The contract is fail-secure: a route advances to its handler only when every middleware returns exactly boolean true.
| Return value | Effect |
|---|---|
true |
Pass — continue to the next middleware, or to the handler |
false |
Deny with the default 403 Forbidden
|
array |
Short-circuit: sent as a JSON response. An optional status key sets the HTTP code (default 403) and is removed from the body |
string or object
|
Short-circuit: sent as the response with status 200 (e.g. a rendered view or redirect body) |
anything else (null, 0, 1, '', ...) |
Treated as a denial → default 403
|
The first middleware that returns anything other than true short-circuits the chain — later middleware and the route handler do not run.
Webrium supports several forms of middleware.
The simplest form — any callable:
Route::group(['middleware' => function () {
return isset($_SESSION['user_id']);
}], function () {
Route::get('/admin', 'AdminController@index');
});For reusable middleware, create a plain class with a handle() method:
namespace App\Middlewares;
class AuthMiddleware
{
public function handle()
{
return isset($_SESSION['user_id']);
}
}Reference it by its fully-qualified class name:
Route::group(['middleware' => 'App\Middlewares\AuthMiddleware'], function () {
Route::get('/dashboard', 'DashboardController@index');
});Middleware classes are resolved with
class_exists()against the exact string you pass, so give the full namespace (e.g.App\Middlewares\AuthMiddleware), not just the short class name.
If you want multiple checks in one class, or a more descriptive method name, use Class@method syntax:
namespace App\Middlewares;
class AuthMiddleware
{
public function handle()
{
return isset($_SESSION['user_id']);
}
public function isAdmin()
{
return isset($_SESSION['user_id']) && $_SESSION['role'] === 'admin';
}
}Route::group(['middleware' => 'App\Middlewares\AuthMiddleware@isAdmin'], function () {
Route::get('/admin', 'AdminController@index');
});A plain function name is also supported:
function isLoggedIn(): bool
{
return isset($_SESSION['user_id']);
}
Route::group(['middleware' => 'isLoggedIn'], function () {
Route::get('/profile', 'ProfileController@index');
});Booleans are accepted directly — useful for feature flags or conditionally enabling a group of routes:
Route::group(['middleware' => env('FEATURE_BETA', false)], function () {
Route::get('/beta', 'BetaController@index');
});Pass an array to apply multiple checks. They run in order, and the first one that does not return true stops the chain immediately — later middleware in the array is not executed:
Route::group(['middleware' => ['App\Middlewares\AuthMiddleware', 'App\Middlewares\RateLimiter@check']], function () {
Route::get('/api/data', 'ApiController@data');
});By default, a middleware that returns false (or any non-true scalar) produces:
{ "error": "Forbidden" }with HTTP status 403. The route handler is never invoked, and no further routes are checked.
A middleware can also return a custom response instead of just passing or failing:
// Custom JSON body and status code
Route::group(['middleware' => function () {
if (!isset($_SESSION['user_id'])) {
return ['error' => 'Unauthenticated', 'status' => 401];
}
return true;
}], function () {
Route::get('/api/me', 'ProfileController@show');
});Returning an array sends it as JSON, honouring an optional status key (default 403). Returning a string or object sends it as the response with status 200.
-
No request/next pipeline. Middleware runs as a simple before-check; it cannot modify the request or pass data forward to the controller. It can, however, short-circuit with a custom response (array, string, or object) as described above — it is not limited to a fixed
403. -
No parameterized middleware. There's no syntax like
auth:admin. For variations of the same check, define separate methods and useClass@methodinstead (e.g.AuthMiddleware@isAdmin). - Middleware stacks across nested groups. As shown in Nested Groups, an inner group's middleware is combined with — not a replacement for — its enclosing groups' middleware.