Routing

Basic Routing

The most basic Slenix routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior without complicated routing configuration files:

php
use Slenix\Http\Routing\Router;

Router::get('/greeting', function ($request, $response) {
    return 'Hello World';
});

The Default Route File

All Slenix routes are defined in your route files, which are located in the routes/ directory. The routes/web.php file is loaded automatically by the framework's kernel. The routes defined in routes/web.php may be accessed by entering the defined route's URL in your browser:

php
// routes/web.php

use Slenix\Http\Routing\Router;
use App\Controllers\HomeController;

Router::get('/', [HomeController::class, 'index']);

Available Router Methods

The router allows you to register routes that respond to any HTTP verb:

php
Router::get($uri, $callback);
Router::post($uri, $callback);
Router::put($uri, $callback);
Router::patch($uri, $callback);
Router::delete($uri, $callback);
Router::options($uri, $callback);

Sometimes you may need to register a route that responds to multiple HTTP verbs. You may do so using the match method. Or, you may even register a route that responds to all HTTP verbs using the any method:

php
Router::match(['GET', 'POST'], '/contact', function ($request, $response) {
    // handles both GET and POST
});

Router::any('/endpoint', function ($request, $response) {
    // handles any HTTP verb
});

Route Parameters

Required Parameters

Sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. You may do so by defining route parameters:

php
Router::get('/users/{id}', function ($request, $response, $params) {
    return 'User ' . $params['id'];
});

You may define as many route parameters as required by your route:

php
Router::get('/posts/{post}/comments/{comment}', function ($request, $response, $params) {
    $postId    = $params['post'];
    $commentId = $params['comment'];
});

Route parameters are injected into route callbacks and controllers via the third argument $params, which is an associative array keyed by parameter name.

Optional Parameters

Occasionally you may need to specify a route parameter that may not always be present in the URI. You may do so by placing a ? mark after the parameter name:

php
Router::get('/users/{id?}', function ($request, $response, $params) {
    $id = $params['id'] ?? null;

    if (!$id) {
        return 'Listing all users';
    }

    return 'User ' . $id;
});

Parameter Constraints

You may constrain the format of your route parameters using the whereNumber, whereAlpha, and whereAlphaNumeric methods on a route instance:

php
Router::get('/users/{id}', [UserController::class, 'show'])
    ->whereNumber('id');

Router::get('/categories/{slug}', [CategoryController::class, 'show'])
    ->whereAlpha('slug');

Router::get('/posts/{slug}', [PostController::class, 'show'])
    ->whereAlphaNumeric('slug');
MethodAllowed CharactersExample
whereNumberDigits only42, 100
whereAlphaLetters onlynews, about
whereAlphaNumericLetters and digitspost1, abc123

Named Routes

Named routes allow the convenient generation of URLs or redirects for specific routes. You may specify a name for a route by chaining the name method onto the route definition:

php
Router::get('/dashboard', [DashboardController::class, 'index'])
    ->name('dashboard');

You may also specify route names for controller actions:

php
Router::get('/users/{id}', [UserController::class, 'show'])
    ->name('users.show');

Router::post('/users', [UserController::class, 'store'])
    ->name('users.store');

Generating URLs to Named Routes

Once you have assigned a name to a given route, you may use the route's name when generating URLs via Slenix's route helper function or Router::route method:

php
// Generating a URL
$url = route('dashboard');

// Generating a URL with parameters
$url = route('users.show', ['id' => 42]);
// Result: /users/42

In your Luna templates:

html
<a href="{{ route('dashboard') }}">Dashboard</a>

<a href="{{ route('users.show', ['id' => $user['id']]) }}">View Profile</a>

If the named route defines parameters and those parameters are not provided, a RuntimeException will be thrown:

php
// This will throw a RuntimeException — 'id' is required
$url = route('users.show');

Route Groups

Route groups allow you to share route attributes, such as middleware or prefixes, across a large number of routes without needing to define those attributes on each individual route.

Middleware

To assign middleware to all routes within a group, pass a middleware key in the group configuration array. Middleware are executed in the order they are listed:

php
Router::group(['middleware' => ['auth']], function () {
    Router::get('/dashboard', [DashboardController::class, 'index']);
    Router::get('/profile',   [ProfileController::class, 'index']);
});

You may also use the Router::middleware shorthand when you only need to assign middleware without a prefix:

php
Router::middleware('auth', function () {
    Router::get('/profile',  [ProfileController::class, 'index']);
    Router::get('/settings', [ProfileController::class, 'settings']);
});

Prefixes

The prefix key may be used to prefix each route in the group with a given URI. For example, you may want to prefix all route URIs within the group with admin:

php
Router::group(['prefix' => 'admin', 'middleware' => ['auth']], function () {
    Router::get('/dashboard', [AdminController::class, 'dashboard']);
    Router::get('/users',     [AdminController::class, 'users']);
    Router::post('/users',    [AdminController::class, 'store']);
});

The routes above are accessible at:

plaintext
GET  /cl-admin/dashboard
GET  /cl-admin/users
POST /cl-admin/users

Combining Prefix and Middleware

You may combine both prefix and middleware keys in the same group definition. This is a common pattern for organizing protected areas of your application:

php
Router::group(['prefix' => 'api/v1', 'middleware' => ['auth', 'throttle']], function () {
    Router::get('/users',         [Api\UserController::class, 'index']);
    Router::get('/users/{id}',    [Api\UserController::class, 'show']);
    Router::post('/users',        [Api\UserController::class, 'store']);
    Router::put('/users/{id}',    [Api\UserController::class, 'update']);
    Router::delete('/users/{id}', [Api\UserController::class, 'destroy']);
});

Middleware

Assigning Middleware to Routes

Middleware may be assigned to individual routes by chaining the middleware method onto the route definition:

php
Router::get('/profile', [ProfileController::class, 'index'])
    ->middleware('auth');

You may assign multiple middleware to a route by passing an array:

php
Router::get('/cl-admin', [AdminController::class, 'index'])
    ->middleware(['auth', 'throttle']);

The middlewares method is an alias for middleware and works identically:

php
Router::get('/cl-admin', [AdminController::class, 'index'])
    ->middlewares(['auth', 'throttle']);

Middleware Aliases

Slenix resolves the following short aliases to their full class names automatically:

AliasClass
authApp\Middlewares\AuthMiddleware
guestApp\Middlewares\GuestMiddleware
corsApp\Middlewares\CorsMiddleware
jwtApp\Middlewares\JwtMiddleware
throttleApp\Middlewares\ThrottleMiddleware

You may always use the full class name instead of an alias:

php
Router::get('/profile', [ProfileController::class, 'index'])
    ->middleware(\App\Middlewares\AuthMiddleware::class);

Global Middleware

If you want a middleware to run during every HTTP request to your application, you may register it as a global middleware using Router::globalMiddleware. Global middleware is prepended to every route automatically:

php
// routes/web.php or Kernel bootstrap

Router::globalMiddleware([
    \App\Middlewares\CorsMiddleware::class,
    \App\Middlewares\ThrottleMiddleware::class,
]);

Writing Middleware

All middleware must implement the Slenix\Http\Middlewares\Middleware interface and define a handle method:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class AuthMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        if (!session()->has('user_id')) {
            redirect('/')->with('error', 'Please sign in to continue.');
        }

        return $next($request, $response);
    }
}

Returning $next($request, $response) passes the request deeper into the application. To halt the request — for example, when authentication fails — simply redirect or abort without calling $next.

Middleware Pipeline

Slenix executes middleware in reverse declaration order, forming a pipeline identical to how Laravel handles it. The last declared middleware wraps the outermost layer of the pipeline.

plaintext
Request
  → ThrottleMiddleware
    → AuthMiddleware
      → Controller action
    ← AuthMiddleware
  ← ThrottleMiddleware
Response

CSRF Protection

Slenix automatically validates CSRF tokens for all state-mutating HTTP requests. Any route that receives a POST, PUT, PATCH, or DELETE request must include a valid CSRF token, or the request will be rejected with a 419 status code.

Including the Token in Forms

When defining HTML forms in your application, you should include the @csrf Luna directive or the csrf_field() helper so that the CSRF protection middleware can validate the request:

html
<form action="{{ route('login') }}" method="post">
    @csrf
    <!-- fields -->
</form>

The @csrf directive renders a hidden input field:

html
<input type="hidden" name="_csrf_token" value="a1b2c3...">

CSRF Tokens via JavaScript

When making requests via JavaScript (fetch, Axios, etc.), include the CSRF token as a request header. First, store the token in a meta tag:

html
<head>
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>

Then read it in your JavaScript:

js
fetch('/profile', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
    },
    body: JSON.stringify({ name: 'Cláudio' }),
});

Redirects

Router::redirect provides a convenient shortcut for defining a route that redirects to another URI. By default, a 302 status code is returned:

php
Router::redirect('/here', '/there');

If you need a permanent redirect, use a 301 status:

php
Router::redirect('/old-blog', '/blog', 301);

View Routes

If your route only needs to return a view, you may use the Router::view method. Like the redirect method, this method provides a simple shortcut so that you do not have to define a full route or controller:

php
Router::view('/about', 'about');

You may pass an array of data to the view as the third argument:

php
Router::view('/about', 'about', [
    'title'   => 'About Us',
    'company' => 'Slenix',
]);

Accessing the Current Route

You may inspect all registered routes at any time by calling Router::getRoutes(). This returns the raw array of route definitions and is useful for debugging:

php
$routes = Router::getRoutes();

Fallback Routes and 404 Handling

When no registered route matches the incoming request, Slenix will automatically respond with a 404 Not Found status. It searches for a custom error page in the following locations, in order:

plaintext
views/errors/404.php
views/error/404.php
src/Core/Exceptions/errors/404.php

If none of these files exist, a RuntimeException is thrown with the message 404 - Page not found.

To display a custom 404 page, create one of the above files. For example:

html
<!-- views/errors/404.php -->
<!DOCTYPE html>
<html>
<head><title>404 — Not Found</title></head>
<body>
    <h1>Page not found.</h1>
    <a href="/">Return home</a>
</body>
</html>

Clearing Routes

When writing automated tests, you may want to reset all registered routes between test cases. The clearRoutes method resets the route table, prefix stack, and all middleware registrations:

php
Router::clearRoutes();

This method clears global middleware, group middleware, and prefix state in addition to the route list. It is intended exclusively for use in test environments.


Route Method Reference

Router Static Methods

MethodDescription
Router::get($uri, $handler)Register a GET route
Router::post($uri, $handler)Register a POST route
Router::put($uri, $handler)Register a PUT route
Router::patch($uri, $handler)Register a PATCH route
Router::delete($uri, $handler)Register a DELETE route
Router::options($uri, $handler)Register an OPTIONS route
Router::any($uri, $handler)Register a route for the current HTTP verb
Router::match($methods, $uri, $handler)Register a route for multiple HTTP verbs
Router::redirect($from, $to, $status)Register a redirect route
Router::view($uri, $view, $data)Register a route that returns a view
Router::group($config, $callback)Register a group of routes
Router::middleware($middleware, $callback)Register a middleware-only group
Router::globalMiddleware($middlewares)Register global middleware
Router::route($name, $params)Generate a URL for a named route
Router::getRoutes()Return all registered routes
Router::clearRoutes()Clear all routes (testing only)
Router::dispatch()Dispatch the current HTTP request

Route Fluent Methods

All route registration methods return a Route instance, which supports the following fluent methods:

MethodDescription
->name(string $name)Assign a name to the route
`->middleware(string\array $middleware)`Attach middleware to the route
`->middlewares(string\array $middleware)`Alias for middleware()
->whereNumber(string $param)Constrain parameter to digits only
->whereAlpha(string $param)Constrain parameter to letters only
->whereAlphaNumeric(string $param)Constrain parameter to letters and digits