Understanding Laravel Middleware
Saturday, October 19, 2024
// 5 min read
Table of Contents
Introduction
Middleware in Laravel can be used to filter HTTP requests or perform tasks before or after each request. For instance, you can use middleware to verify if a user is authenticated, which lets you control access to specific routes and redirect those who aren't authenticated to the login page. You can also alter request data before they reach your routes.
Middleware Types
Laravel comes with several built-in middleware. If you're curious, you can view the list of available middleware in the Illuminate\Foundation\Configuration\Middleware
class.
Global Middleware
Global middleware is applied to every HTTP request that the application receives. To see the default global middleware, check the getGlobalMiddleware
method.
Some examples include:
\Illuminate\Http\Middleware\HandleCors
: Handles Cross-Origin Resource Sharing (CORS) requests, which manage how resources can be shared across different domains.\Illuminate\Foundation\Http\Middleware\TrimStrings
: Trims whitespace from request data.\Illuminate\Http\Middleware\ValidatePostSize
: Limits the size of incoming requests based on thepost_max_size
configuration inphp.ini
.
Route Middleware
Route middleware can be applied to specific routes or groups of routes. Laravel includes several built-in route middleware that you can use with selected routes. To see the list of available route middleware, check the defaultAliases
method.
Some commonly used built-in route middleware include:
auth
: Verifies that the user is authenticated. If the user is not authenticated, it redirects them to the login page.guest
: Redirects authenticated users away from routes intended for guests, such as login or registration pages.verified
: Ensures that the user's email has been verified.
Middleware groups
Middleware groups consist of multiple middleware bundled together. Laravel provides two middleware groups by default: web
and api
.
The
web
middleware group is applied to routes that require session state, CSRF protection, and other web-related features.The
api
middleware group is intended for routes used by API requests, offering features like rate limiting.
Create Middleware
Let’s look at an example. We will create a middleware class to check if the authenticated user is an admin. If the user is an admin, the request will proceed further into the application. Otherwise, the system will respond with an unauthorized exception.
To create the middleware class, you can use this Artisan command:
php artisan make:middleware IsAdmin
Within the handle
method of the generated class, you can specify the conditions for this check:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Check if the user is unauthenticated, redirect to login
if (! $request->user()) {
return redirect()->route('login');
}
// If the user is not an admin, abort with a 403 error
if (! $request->user()->isAdmin()) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}
Since this middleware is intended for specific routes, we need to apply it to the appropriate routes.
I've created a simple controller called PostController
which has different methods to list, show, create and edit posts. And I want to restrict certain routes so that only an admin user can create and edit posts.
In my routes/web.php
file, I can add the middleware method to the selected routes:
Route::resource('posts', PostController::class)->middleware(IsAdmin::class);
This approach restricts all routes under the posts
prefix. However, if I want to restrict only the admin CRUD operations, I can define middleware for a specific group of routes:
Route::middleware([IsAdmin::class])->group(function () {
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
Route::patch('/posts/{post}', [PostController::class, 'update'])->name('posts.update');
Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
});
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
If I navigate to the posts/create
route as a non-admin user, I’ll see an unauthorized message.
Now I log in with the admin user.
I have the ability now to create, edit, or delete posts. When I visit the posts/create
route again, I can see the create form:
Middleware parameters
You can pass parameters to a middleware from a route definition.
I would like to refactor the IsAdmin
middleware in a way that I can check for any user role by passing a role name as a parameter.
You can add the new parameter to the handle
method. Here is the modified class:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Role
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param string $role
*/
public function handle(Request $request, Closure $next, string $role): Response
{
// Check if the user is unauthenticated, redirect to login
if (! $request->user()) {
return redirect()->route('login');
}
// If the user does not have the required role, abort with a 403 error
if (! $request->user()->role === $role) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}
You can use a custom method in your user model to check the role that fits your role implementation. In this example, I’ve added a simple attribute check since a user can only have one type of role.
Next, you need to include the role parameter in your route definition.
Route::middleware([Role::class . ':admin'])
Middleware alias
You can also create an alias if you don’t want to include the full class name each time.
To do this, add the alias in your bootstrap/app.php
file using the withMiddleware
method, like this:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => Role::class
]);
})
Now, you can simply use 'role:admin'
:
Route::middleware(['role:admin'])
Register Global Middleware
What if you want to create a middleware that you can use in every route? For example, I would like to log the time it takes to process each request.
Let’s create the middleware class:
php artisan make:middleware LogRequestDuration
After creating the middleware, you can modify the file to calculate the duration of each request. But to achieve this, better to use the terminate
method, which is executed after the response has been sent to the client. This method is useful for tasks like logging, resource cleanup, or any additional code that doesn't need to change the response itself.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class LogRequestDuration
{
protected $startTime;
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$this->startTime = microtime(true); // Start the timer
// Return the response
return $next($request);
}
/**
* Terminate the request.
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
*/
public function terminate(Request $request, Response $response)
{
$duration = microtime(true) - $this->startTime; // Calculate the duration
// Log the request duration
Log::info('Request Duration', [
'url' => $request->fullUrl(),
'duration' => $duration . ' seconds',
]);
}
}
To make this middleware available for all requests, add it to the global middleware list inside your bootstrap/app.php
file using the withMiddleware
method:
$middleware->append([LogRequestDuration::class]);
Now that the middleware is registered globally, the duration of all incoming requests will be logged. You can view the storage/logs/laravel.log
file to see the outputs of these logs. It should look something like this:
Append middleware to groups
To apply a middleware to a specific group of routes, you can use the appendToGroup
method in your application's bootstrap/app.php
file.
I've created a simple middleware that sets the application locale based on a locale
request parameter. I want to apply this middleware to every request that uses my application's API.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocaleFromRequest
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->has('locale')) {
app()->setLocale($request->locale);
}
return $next($request);
}
}
I can append it to the api
middleware group like this:
$middleware->appendToGroup('api', [SetLocaleFromRequest::class]);
You can also create a new group with selected middleware:
$middleware->appendToGroup('group-name', [
First::class,
Second::class,
]);
Now you have the basics of using middleware in Laravel. For more detailed information, visit the official Laravel documentation.