Route groups — merging attributes (prefix, middleware)
Advanced5 min read·fw-03-005
laravel-src
Concept
Route groups allow applying shared attributes — prefix, middleware, namespace, name prefix, domain — to multiple routes without repeating them on each route definition.
What groups merge:
- Prefix: Group prefix is prepended to each route URI. Nested groups concatenate:
/api+/users→/api/users. - Middleware: Group middleware is merged with route-level middleware. Inner + outer, not replacing.
- Name prefix: Prepended to route names:
api.+users.show→api.users.show. - Namespace: Controller class namespace prefix (less common in modern PHP with fully-qualified names).
Implementation via a stack: The router maintains an attribute stack. group() pushes the group attributes onto the stack, runs the closure, then pops. When a route is registered inside the closure, the router merges the current stack into the route.
Merging rules:
- Strings (prefix, name prefix): concatenate.
- Arrays (middleware): array_merge.
- Last wins: for conflicts on scalar values (domain, namespace).
Nesting: Groups can be nested. Inner group attributes merge with outer group attributes when the inner group is processed.
Convenience: Without groups, every API route would repeat middleware(['auth:api', 'throttle:60']) and prefix /api/v1. With a group, you write it once.
Code Example
php
<?php
namespace Framework\Routing;
class Router
{
private RouteCollection $routes;
private array $groupStack = [];
public function __construct()
{
$this->routes = new RouteCollection();
}
public function group(array $attributes, \Closure $routes): void
{
// Merge with parent group attributes
$merged = $this->mergeGroupAttributes(
$this->getCurrentGroupAttributes(),
$attributes
);
$this->groupStack[] = $merged;
$routes($this);
array_pop($this->groupStack);
}
public function get(string $uri, mixed $action): Route
{
return $this->addRoute('GET', $uri, $action);
}
public function post(string $uri, mixed $action): Route
{
return $this->addRoute('POST', $uri, $action);
}
private function addRoute(string $method, string $uri, mixed $action): Route
{
$group = $this->getCurrentGroupAttributes();
// Merge prefix
$fullUri = ($group['prefix'] ?? '') . '/' . ltrim($uri, '/');
$fullUri = '/' . ltrim($fullUri, '/');
// Merge middleware
$middleware = array_merge($group['middleware'] ?? [], []);
$route = new Route($method, $fullUri, $action, $middleware);
$this->routes->add($route);
return $route;
}
private function getCurrentGroupAttributes(): array
{
return empty($this->groupStack) ? [] : end($this->groupStack);
}
private function mergeGroupAttributes(array $parent, array $child): array
{
return [
'prefix' => ($parent['prefix'] ?? '') . '/' . ltrim($child['prefix'] ?? '', '/'),
'middleware' => array_merge($parent['middleware'] ?? [], $child['middleware'] ?? []),
'name' => ($parent['name'] ?? '') . ($child['name'] ?? ''),
];
}
}
// Usage
$router = new Router();
$router->group(['prefix' => '/api', 'middleware' => ['auth']], function(Router $router) {
$router->group(['prefix' => '/v1', 'middleware' => ['throttle:60']], function(Router $router) {
$router->get('/users', [UserController::class, 'index']);
// Route: GET /api/v1/users with middleware: ['auth', 'throttle:60']
$router->post('/users', [UserController::class, 'store']);
// Route: POST /api/v1/users with middleware: ['auth', 'throttle:60']
});
});