4 ways to clean up your Laravel routes files
March 11, 2020 ‐ 7 min read
As your Laravel application grows, your routes file grows with it. And most often it doesn't just grow with it, it becomes messy and hard to read. So cleaning it up once every while can be time well spend.
1. Use resource routes
The first route definition you see in your web.php
routes file after starting a new Laravel project is:
<?php
Route::get('/', function () {
return view('welcome');
});
If you're running PHP 7.4 or higher, locally and on your server, you can clean this one up a little with an arrow function. The following is equivalent to the route definition above, but using an arrow function.
<?php
Route::get('/', fn() => view('welcome'));
But if you'd ask me routes using closures cause quite some visual noise. So I rather use controllers over closures unless it really makes no sense to make a controller for a view.
Another pro for using controllers for your routes is that you can leverage resource routes. When you define a resource route you get routes for all the CRUD actions you'll probably need.
<?php
Route::resource('users', 'UsersController');
Just that one line generated the following routes for you.
$ php artisan route:list --compact --name user
+-----------+-------------------+----------------------------------------------+
| Method | URI | Action |
+-----------+-------------------+----------------------------------------------+
| GET|HEAD | users | App\Http\Controllers\UsersController@index |
| POST | users | App\Http\Controllers\UsersController@store |
| GET|HEAD | users/create | App\Http\Controllers\UsersController@create |
| GET|HEAD | users/{user} | App\Http\Controllers\UsersController@show |
| PUT|PATCH | users/{user} | App\Http\Controllers\UsersController@update |
| DELETE | users/{user} | App\Http\Controllers\UsersController@destroy |
| GET|HEAD | users/{user}/edit | App\Http\Controllers\UsersController@edit |
+-----------+-------------------+----------------------------------------------+
Which is super nice ofcourse, but it also means you need a controller with those same actions. That's a low effort task luckily. By running the following artisan command you get a new controller with all seven actions.
$ php make:controller UsersController --resource
There is a chance however that you don't need all of the actions. If that's the case you can chain the route definition with ->only([...])
or ->except([...])
to get a subset of all the resource routes.
<?php
Route::resource('users', 'UsersController')->only(['index', 'show']);
This will limit the user routes to only an index and a show action. Even if you just have a couple of actions this quickly saves you lines of code.
$ php artisan route:list --compact --name user
+----------+--------------+--------------------------------------------+
| Method | URI | Action |
+----------+--------------+--------------------------------------------+
| GET|HEAD | users | App\Http\Controllers\UsersController@index |
| GET|HEAD | users/{user} | App\Http\Controllers\UsersController@show |
+----------+--------------+--------------------------------------------+
2. Register routes in tuples
Since Laravel 5.6 we can use arrays instead of strings to define the controller and action of a route. This tuple syntax looks like the following.
<?php
Route::get('users', [\App\Http\Controllers\UsersController::class, 'index']);
You can clean this up by adding a use
statement for the UsersController
. Check if your editor can hide these, because these will start to add up pretty quick when your application grows.
<?php
use App\Http\Controllers\UsersController;
Route::get('users', [UsersController::class, 'index']);
Or you can set the namespace of the routes file to the same namespace of your controllers. What falls in the category hacky if you'd ask me, but being a little creative can be fun sometimes :-).
<?php
namespace App\Http\Controllers;
Route::get('users', [UsersController::class, 'index']);
Using the tuple syntax might allow you to use the "Go to Definition" and "Refactor" functions of your IDE. Which are nice benefits in (at least) my workflow compared to the string syntax which feels a bit magical.
One thing that is a bit of a bummer is that it doesn't work with resource routes at the moment.
For more information about tuple routes, Freek.dev has a great opinion about them.
3. Split up the routes file
Larger applications have easily dozens of route definitions divided in multipe route groups. Lets say your application has a public section, a section that requires authentication and a section that requires admin access. In that case the routes file has a structure similar to something like this:
<?php
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::group(['middleware' => 'auth', 'prefix' => 'app'], function () {
Route::resource('users', 'UsersController')->only(['index', 'show']);
Route::group(['middleware' => 'admin'], function () {
Route::get('dashboard', 'DashboardController@index');
});
});
I personally find it hard to follow what is happening with all the groupings and the level of indent. Imagine managing an application with hundreds of routes. We can solve this by splitting the routes file up in three files: routes/web.php
, routes/app.php
and routes/admin.php
.
So we end up with the following three files:
<?php
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
<?php
// routes/app.php
use Illuminate\Support\Facades\Route;
Route::resource('users', 'UsersController')->only(['index', 'show']);
<?php
// routes/admin.php
use Illuminate\Support\Facades\Route;
Route::get('dashboard', 'DashboardController@index');
But this doesn't work correctly just like that. If we check our available routes now we that only the one from routes/web.php
is still available.
$ php artisan route:list
+--------+----------+----------+------+---------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+----------+------+---------+--------------+
| | GET|HEAD | / | | Closure | web |
+--------+----------+----------+------+---------+--------------+
Also notice that we got rid of all the middleware in the routes/app.php
and routes/admin.php
files. We fix that later on ;-)
For this to work we need to add the newly created routes files to the RouteServiceProvider. And in the RouteServiceProvider class you want to look for the map()
function. Which, if you haven't touched it before, should look like this:
<?php
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}
We will add calls mapAppRoutes
and mapAdminRoutes
to this function in which we will register the other two routes files.
<?php
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapAppRoutes();
$this->mapAdminRoutes();
}
So now we need to write those two new functions. Both should be very similar to the mapWebRoutes
function that did already exist. But we need to change the file path and add back the middleware.
<?php
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
protected function mapAppRoutes()
{
Route::middleware('web')
->middleware('auth')
->namespace($this->namespace)
->group(base_path('routes/app.php'));
}
protected function mapAdminRoutes()
{
Route::middleware('web')
->middleware('auth')
->middleware('admin')
->namespace($this->namespace)
->group(base_path('routes/admin.php'));
}
And voila, our route definitions are back as we had them before. Only now in clean separated files.
$ php artisan route:list
+--------+----------+--------------+-------------+------------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+--------------+-------------+------------------------------------------------+--------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | dashboard | | App\Http\Controllers\DashboardController@index | web |
| | GET|HEAD | users | users.index | App\Http\Controllers\UsersController@index | auth |
| | GET|HEAD | users/{user} | users.show | App\Http\Controllers\UsersController@show | auth |
+--------+----------+--------------+-------------+------------------------------------------------+--------------+
4. Routes macros
The Marcroable trait is something you might have seen before. With macros you can basically "add" functions to the interface of a class. This is useful when you want to extend one of Laravels core classes with a function that's really specific to your application for example.
Luckily for us, the Laravel router implements the Macroable
trait. So what this allows us to do is to bundle a set of routes in a macro and register them at once in our routes file.
Lets say your application implements a service that expects data to be pushed to it from another service. An incoming webhook from GitHub for example. In that case you can bundle the route definitions with your service, and define them using a single line in your routes file.
You can define the route macro in the service provider of your GitHub service. It requires a name and an closure. And within the closure you can add route definitions as you would normally do in your routes file. Just make sure you add a use
statement for the Route facade.
<?php
public function boot()
{
Route::macro('githubServiceRoutes', function () {
Route::group([ 'middleware' => 'validate_github_secret'], function () {
Route::post('github/webhook', 'GithubController@handle');
});
});
}
In your routes file you can now use the name you gave the macro as a static function for the Route facade.
<?php
// routes/admin.php
use Illuminate\Support\Facades\Route;
Route::githubServiceRoutes();
Lets double check with the artisan routes command. And yup, the route that we registered in the Github ServiceProvider is now available.
$ php artisan route:list
+--------+----------+----------------+------+----------------------------------------------+----------------------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+----------------+------+----------------------------------------------+----------------------------+
| | GET|HEAD | / | | Closure | web |
| | POST | github/webhook | | App\Http\Controllers\GithubController@handle | web,validate_github_secret |
+--------+----------+----------------+------+----------------------------------------------+----------------------------+