Okay, you’ve used Laravel Breeze and got a basic app with users, authentication, etc. Woah, that was fast!
Okay, now you have a few routes that are for admin users and a bunch that are for normal users. If you’re building an app like so many that we’ve seen before, you have a bunch of stuff in your web.php routes file. It’s starting to get messy, and maybe you’ve added a few different route groups to try and organize this stuff. Something like this:
<?php
// anybody routes
Route::get("/", [PublicController::class, "home"]);
Route::get("/about", [PublicController::class, "about"]);
// ...
// admin only routes, middleware provided by the package
Route::group(['middleware' => ['auth','role:admin']], function () {
Route::get("/admin/dashboard", [AdminDashboardController::class, "index"]);
Route::get("/admin/users", [AdminUsersController::class, "index"]);
// ...
});
// other auth routes intended for non admins
Route::group(['middleware' => ['auth']], function () {
Route::get("/dashboard", [UserController::class, "dashboard"]);
Route::get("/profile", [UserController::class, "profile"]);
// ...
});This is fine, and works, but man oh man it could be a lot better. First, when your routes are defined like this, often times we see routes floating outside of the groups that protect them. You end up with routes that you intend to have only authed users use, but you forgot to put them in the right group so they could get hit by non-authed users. Second, after your routes file gets large enough, you have multiple groups that are almost the same, but have one slight different requirement for the middleware. Fast forward 6 months and you no longer remember which group the new route you need to add should go into…
Custom route groups to the rescue!
Okay, so how does laravel get these routes in the first place? Let’s go diving and checkout a few different provider files. First, let’s look at the file app/Providers/RouteServiceProvider.php. In the boot method, out of the box modern laravel apps come with two groups, api and web.
class RouteServiceProvider extends ServiceProvider
{
// ...
public function boot()
{
// ...
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php')); // <- api routes go in this file
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php')); // <- web routes go in this file
});
}
// ...
}Not that surprisingly, they correlate to the routes files routes/api.php and routes/web.php. In the example above, we ended up putting routes of all type into the web.php file and grouping them into route groups with code, which makes us have to hunt around in that file whenever we need to add more functionality to the application. But laravel rules, so it’s really easy for us to add additional route groups and files, so we don’t need to do that anymore.
The middleware(NAME) method on these Route declarations set up all the middleware methods that are used on each route, and they differ depending on which file you put your route in. So, where are these names defined? Let’s go check out app/Http/Kernel.php:
class Kernel extends HttpKernel
{
// ...
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// ...
}Awesome! This is just a PHP class property that is an associtive array. So why don’t we just make some custom groups with different keys, and pull those groups into their own routes files? Let’s make two new groups, one for routes for authenticated users (routes/protected.php), and one for authenticated users that are also admin users (routes/admin.php). So, let’s just copy the web group and add some additional items to those arrays:
class Kernel extends HttpKernel
{
// ...
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// NEW!!!
'protected' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'auth', // added this, it's the same name that was used in the web.php route group
],
// NEW!!!
'admin' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'auth', // added these, it's the same names that were used in the web.php route group
'role:admin',
],
];
// ...
}Ok, now we have new route groups defined for our entire app, so let’s create two new files in the routes folder protected.php and admin.php. You can name these files whatever you want, but make sure they match the filenames that you use in the next step. We’re now going to add those new routes files to our app, so back to RouteServiceProvider.php:
class RouteServiceProvider extends ServiceProvider
{
// ...
public function boot()
{
// ...
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
Route::middleware('protected') // <- the name of our new group in the Kernel
->namespace($this->namespace)
->group(base_path('routes/protected.php')); // <- authed routes can now go here
Route::middleware('admin')
->prefix('admin') // <- make it so that all our routes will get this prefix automatically
->namespace($this->namespace)
->group(base_path('routes/admin.php')); // <- admin only routes go here
});
}
// ...
}Awesome! We can now get rid of the groups in our routes/web.php file entirely and just put the routes into their appropriate file. No more needing to make sure that we put the route into the right spot, as long as it’s in the right file, the correct middleware will be applied automatically.
web.php
// anybody routes
Route::get("/", [PublicController::class, "home"]);
Route::get("/about", [PublicController::class, "about"]);
// now only public facing routes should go here ...
protected.php
// got rid of the inline group, only authed user routes go in this file ...
Route::get("/dashboard", [UserController::class, "dashboard"]);
Route::get("/profile", [UserController::class, "profile"]);admin.php
// got rid of the inline group AND the prefix on each route,
// as it's included in the group.
//
// only authed admin routes go in this file ...
Route::get("/dashboard", [AdminDashboardController::class, "index"]);
Route::get("/users", [AdminUsersController::class, "index"]);
// ...
So much cleaner, clearer, and easier to maintain 💪
Apr 18, 2024
Categories: