Route Groups
The examples configure a collector. Build a router after configuration to resolve requests. Treat the patterns below as independent examples.
use Solo\Router\RouteCollector;
$routes = new RouteCollector();Groups share prefixes, middleware and attributes across multiple routes.
Basic Groups
$routes->group('/admin', function(RouteCollector $routes) {
$routes->get('/dashboard', [AdminController::class, 'dashboard']);
$routes->get('/users', [AdminController::class, 'users']);
$routes->get('/settings', [AdminController::class, 'settings']);
});
// Registers:
// GET /admin/dashboard
// GET /admin/users
// GET /admin/settingsGroups with Middleware
Pass middleware as the third parameter:
$routes->group('/admin', function(RouteCollector $routes) {
$routes->get('/dashboard', [AdminController::class, 'dashboard']);
$routes->get('/users', [AdminController::class, 'users']);
}, [AuthMiddleware::class, AdminMiddleware::class]);
// All routes have AuthMiddleware and AdminMiddlewareGroups with Attributes
Pass attributes as the fourth parameter, or by name:
$routes->group('/billing', function(RouteCollector $routes) {
$routes->get('/invoices', [InvoiceController::class, 'index']);
$routes->get('/plans', [PlanController::class, 'index'])
->attribute('permission', 'billing.view');
}, attributes: ['permission' => 'billing.manage']);
// /billing/invoices has ['permission' => 'billing.manage']
// /billing/plans has ['permission' => 'billing.view']Unlike middleware, attributes don't accumulate: an inner group or the route itself replaces the value for the same key. See Inheritance Rules.
Nested Groups
Groups can be nested to any depth:
$routes->group('/api', function(RouteCollector $routes) {
$routes->group('/v1', function(RouteCollector $routes) {
$routes->get('/users', [ApiV1UserController::class, 'index']);
$routes->get('/posts', [ApiV1PostController::class, 'index']);
});
$routes->group('/v2', function(RouteCollector $routes) {
$routes->get('/users', [ApiV2UserController::class, 'index']);
$routes->get('/posts', [ApiV2PostController::class, 'index']);
});
});
// Registers:
// GET /api/v1/users
// GET /api/v1/posts
// GET /api/v2/users
// GET /api/v2/postsNested Middleware
Middleware is inherited and merged:
$routes->group('/api', function(RouteCollector $routes) {
$routes->group('/admin', function(RouteCollector $routes) {
$routes->get('/stats', [AdminController::class, 'stats']);
}, [AdminMiddleware::class]);
}, [ApiMiddleware::class, RateLimitMiddleware::class]);
// /api/admin/stats has, in execution order (outermost first):
// - ApiMiddleware
// - RateLimitMiddleware
// - AdminMiddlewareResource-style Groups
Organize CRUD routes:
$routes->group('/users', function(RouteCollector $routes) {
$routes->get('/', [UserController::class, 'index'])->name('users.index');
$routes->get('/create', [UserController::class, 'create'])->name('users.create');
$routes->post('/', [UserController::class, 'store'])->name('users.store');
$routes->get('/{id}', [UserController::class, 'show'])->name('users.show');
$routes->get('/{id}/edit', [UserController::class, 'edit'])->name('users.edit');
$routes->put('/{id}', [UserController::class, 'update'])->name('users.update');
$routes->delete('/{id}', [UserController::class, 'destroy'])->name('users.destroy');
});API Versioning
$routes->group('/api', function(RouteCollector $routes) {
// Version 1
$routes->group('/v1', function(RouteCollector $routes) {
$routes->get('/users', [V1\UserController::class, 'index']);
}, [V1DeprecationMiddleware::class]);
// Version 2 (current)
$routes->group('/v2', function(RouteCollector $routes) {
$routes->get('/users', [V2\UserController::class, 'index']);
});
}, [ApiAuthMiddleware::class, JsonMiddleware::class]);Optional Group Prefix
Use optional segments in group prefix:
$routes->group('[/{lang:[a-z]{2}}]', function(RouteCollector $routes) {
$routes->get('/', [HomeController::class, 'index']);
$routes->get('/about', [PageController::class, 'about']);
$routes->get('/contact', [PageController::class, 'contact']);
});
// Matches both:
// /, /about, /contact
// /en/, /en/about, /en/contactDomain-style Groups
Organize by feature area:
// Public routes
$routes->group('', function(RouteCollector $routes) {
$routes->get('/', [HomeController::class, 'index']);
$routes->get('/about', [PageController::class, 'about']);
});
// Auth routes
$routes->group('/auth', function(RouteCollector $routes) {
$routes->get('/login', [AuthController::class, 'showLogin']);
$routes->post('/login', [AuthController::class, 'login']);
$routes->post('/logout', [AuthController::class, 'logout']);
}, [GuestMiddleware::class]);
// Dashboard routes
$routes->group('/dashboard', function(RouteCollector $routes) {
$routes->get('/', [DashboardController::class, 'index']);
$routes->get('/profile', [ProfileController::class, 'show']);
$routes->put('/profile', [ProfileController::class, 'update']);
}, [AuthMiddleware::class]);
// Admin routes
$routes->group('/admin', function(RouteCollector $routes) {
$routes->get('/', [AdminController::class, 'index']);
$routes->get('/users', [AdminUserController::class, 'index']);
}, [AuthMiddleware::class, AdminMiddleware::class]);Best Practices
Middleware Order
Group middleware runs before route-specific middleware:
$routes->group('/api', function($routes) {
$routes->get('/users', [UserController::class, 'index'])
->middleware(CacheMiddleware::class);
}, [AuthMiddleware::class]);
// Middleware order: AuthMiddleware → CacheMiddlewareKeep Groups Focused
Each group should have a clear purpose (API version, feature area, auth state).