0

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.showapi.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']
    });
});