Docs LATEST

Router

The Router handles HTTP route registration, request dispatching, middleware execution, and URL generation.

HTTP

Namespace: VelvetCMS\Http\Routing\Router


Definition #

class Router
{
    public function __construct(EventDispatcher $events);
    
    // Route registration
    public function get(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    public function post(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    public function put(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    public function delete(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    public function patch(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    public function any(string $path, callable|array $handler, ?string $name = null): RouteDefinition;
    
    // Route groups
    public function group(array $attributes, callable $callback): void;
    
    // Middleware
    public function middleware(string|array|callable $middleware): self;
    public function registerMiddleware(string $name, callable|string $middleware): void;
    public function pushMiddleware(callable|string $middleware): void;
    
    // URL generation
    public function url(string $name, array $params = []): string;
    
    // Dispatching
    public function dispatch(Request $request): Response;
    
    // Caching
    public function getRouteDefinitions(): array;
    public function loadCachedRoutes(array $cachedRoutes): void;
    public function hasCachedRoutes(): bool;
}

Route Registration #

HTTP Method Shortcuts #

Register routes for specific HTTP methods:

$router->get('/posts', [PostController::class, 'index']);
$router->post('/posts', [PostController::class, 'store']);
$router->put('/posts/{id}', [PostController::class, 'update']);
$router->delete('/posts/{id}', [PostController::class, 'delete']);
$router->patch('/posts/{id}', [PostController::class, 'patch']);

any() #

Register a route that responds to any HTTP method:

$router->any('/webhook', [WebhookController::class, 'handle']);

Route Handlers #

Routes accept either a controller/method array or a closure:

// Controller action
$router->get('/posts', [PostController::class, 'index']);

// Closure
$router->get('/health', fn(Request $request) => Response::json(['status' => 'ok']));

// Closure with route parameters
$router->get('/posts/{id}', function(Request $request, string $id) {
    return Response::json(['id' => $id]);
});

Route Parameters #

Required Parameters #

Use {name} for required segments:

$router->get('/posts/{id}', [PostController::class, 'show']);
$router->get('/users/{userId}/posts/{postId}', [PostController::class, 'showUserPost']);

Optional Parameters #

Use {name?} for optional segments:

$router->get('/posts/{category?}', [PostController::class, 'index']);

Wildcard Parameters #

Use {name*} for greedy matching (captures nested paths):

// Matches /docs/getting-started, /docs/api/router, etc.
$router->get('/docs/{path*}', [DocsController::class, 'show']);

Accessing Parameters #

Route parameters are passed to handlers after the Request:

// Controller method
public function show(Request $request, string $id): Response
{
    // $id contains the route parameter value
}

// Closure
$router->get('/posts/{id}', function(Request $request, string $id) {
    return Response::json(['id' => $id]);
});

Named Routes #

Name routes for URL generation:

$router->get('/posts', [PostController::class, 'index'], 'posts.index');
$router->get('/posts/{id}', [PostController::class, 'show'], 'posts.show');
$router->get('/posts/{id}/edit', [PostController::class, 'edit'], 'posts.edit');

Generate URLs using route names:

$url = $router->url('posts.index');           // /posts
$url = $router->url('posts.show', ['id' => 5]); // /posts/5

// Using helper function (respects tenant prefix)
$url = route('posts.show', ['id' => 5]);

Middleware #

Route Middleware #

Attach middleware to specific routes:

$router->get('/admin', [AdminController::class, 'index'])
    ->middleware('auth');

$router->get('/api/data', [ApiController::class, 'data'])
    ->middleware(['auth', 'throttle']);

Registering Middleware Aliases #

Register middleware classes with aliases:

$router->registerMiddleware('auth', AuthMiddleware::class);
$router->registerMiddleware('throttle', ThrottleMiddleware::class);
$router->registerMiddleware('cors', CorsMiddleware::class);

Global Middleware #

Add middleware that runs on all routes:

$router->pushMiddleware(ErrorHandlerMiddleware::class);
$router->pushMiddleware('cors');

Writing Middleware #

Middleware must implement MiddlewareInterface (from VelvetCMS\Contracts) or be a callable:

use VelvetCMS\Contracts\MiddlewareInterface;
use VelvetCMS\Http\Request;
use VelvetCMS\Http\Response;

class AuthMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, callable $next): Response
    {
        if (!session('user_id')) {
            return Response::redirect('/login');
        }
        
        return $next($request);
    }
}

Route Groups #

group() #

Group routes that share a common prefix and/or middleware:

public function group(array $attributes, callable $callback): void

Parameters:

  • $attributes — Associative array with optional prefix (string) and middleware (string or array)
  • $callback — Receives the Router instance; define routes inside

Behavior:

  • Prefixes are concatenated when groups are nested
  • Middleware is merged — outer group middleware runs first
  • Groups are scoped — routes outside the group are unaffected
  • Per-route ->middleware() stacks on top of group middleware
$router->group(['prefix' => '/api/v1', 'middleware' => ['auth', 'throttle']], function (Router $r) {
    $r->get('/users', [UserController::class, 'index']);     // /api/v1/users
    $r->post('/users', [UserController::class, 'store']);    // /api/v1/users
    $r->get('/users/{id}', [UserController::class, 'show']); // /api/v1/users/{id}
});

Request Dispatching #

dispatch() #

Match and execute a route for the given request:

public function dispatch(Request $request): Response

The router:

  1. Matches the request path and method to a route
  2. Supports method spoofing via _method POST field
  3. Executes global middleware, then route middleware
  4. Calls the route handler
  5. Returns a Response (auto-converts strings and arrays)

Return Value Handling #

Handlers can return various types:

// String → HTML response
$router->get('/hello', fn() => '<h1>Hello</h1>');

// Array → JSON response
$router->get('/api/data', fn() => ['status' => 'ok']);

// Response object (preferred)
$router->get('/posts', fn() => Response::json($posts));

Method Spoofing #

HTML forms only support GET and POST. Use _method to spoof other methods:

<form method="POST" action="/posts/5">
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete</button>
</form>

The router detects _method and routes to the appropriate handler.


Route Caching #

For production, cache routes to improve performance:

getRouteDefinitions() #

Export route definitions for caching:

$definitions = $router->getRouteDefinitions();
file_put_contents(
    storage_path('cache/routes.php'),
    '<?php return ' . var_export($definitions, true) . ';'
);

loadCachedRoutes() #

Load cached route definitions:

if ($router->hasCachedRoutes()) {
    $cached = require storage_path('cache/routes.php');
    $router->loadCachedRoutes($cached);
}

Events #

The router dispatches events during dispatch:

Event Payload Description
router.matching ['method', 'path', 'original_method'] Before route matching
router.matched ['route', 'params'] After route matched

Usage Examples #

Basic Application Routes #

// bootstrap/app.php or routes file
$router = app('router');

// Home page
$router->get('/', [HomeController::class, 'index'], 'home');

// Static pages
$router->get('/about', [PageController::class, 'about'], 'about');
$router->get('/contact', [PageController::class, 'contact'], 'contact');
$router->post('/contact', [PageController::class, 'submitContact'], 'contact.submit');

// Blog
$router->get('/blog', [BlogController::class, 'index'], 'blog.index');
$router->get('/blog/{slug}', [BlogController::class, 'show'], 'blog.show');

RESTful API Routes #

// API routes with authentication using groups
$router->group(['prefix' => '/api', 'middleware' => 'auth:api'], function (Router $r) {
    $r->get('/posts', [ApiPostController::class, 'index'], 'api.posts.index');
    $r->post('/posts', [ApiPostController::class, 'store'], 'api.posts.store');
    $r->get('/posts/{id}', [ApiPostController::class, 'show'], 'api.posts.show');
    $r->put('/posts/{id}', [ApiPostController::class, 'update'], 'api.posts.update');
    $r->delete('/posts/{id}', [ApiPostController::class, 'destroy'], 'api.posts.destroy');
});

Route Groups #

// Group with prefix and middleware
$router->group(['prefix' => '/api', 'middleware' => ['auth', 'throttle']], function (Router $r) {
    $r->get('/posts', [ApiPostController::class, 'index'], 'api.posts.index');
    $r->post('/posts', [ApiPostController::class, 'store'], 'api.posts.store');
    $r->delete('/posts/{id}', [ApiPostController::class, 'destroy'], 'api.posts.destroy');
});

// Nested groups — prefixes stack, middleware merges
$router->group(['prefix' => '/admin', 'middleware' => 'auth'], function (Router $r) {
    $r->get('/dashboard', [AdminController::class, 'dashboard'], 'admin.dashboard');

    $r->group(['prefix' => '/users'], function (Router $r) {
        $r->get('/', [AdminUserController::class, 'index'], 'admin.users.index');
        $r->get('/{id}', [AdminUserController::class, 'show'], 'admin.users.show');
    });
});