Middleware Laravel

Nội dung bài học này chúng ta sẽ tìm hiểu về một thành phần rất hay sử dụng trong Laravel đó chính là Middleware. Thuật ngữ này đã được mình nhắc đến trong bài vòng đời request của Laravel rồi phải hông nào, trong bài này chúng ta sẽ đào sâu về Middleware trong Laravel hơn để có cái nhìn rõ ràng hơn về nó các bạn nhé!

1. Giới thiệu về Middleware 

Middleware cung cấp cho chúng ta cơ chế dễ dàng để có thể lọc các request HTTP đến ứng dụng. 

Ví dụ khi người dùng click vào trang mua hàng hệ thống sẽ kiểm tra xem đã tồn tại session/cookie user chưa (tức là đã đăng nhập hay chưa). Nếu có đăng nhập thì tiếp tục cho request thực thi các bước mua hàng tiếp theo còn không thì redirect về /login hoặc /register. Công việc kiểm tra trên sẽ do các middleware đảm nhận.

Các core middleware của Laravel và kể các các middleware do bạn tạo ra đều nằm trong thư mục app/Http/Middleware.

2. Khởi tạo middleware (Create middleware)

Để tạo một middleware mới, chúng ta sử dụng lệnh Artisan make:middleware.

php artisan make:middleware CheckAge

Sau khi lệnh thực thi, ta sẽ thấy file CheckAge.php đã được tạo trong thư mục app/Http/Middleware. Bây giờ chúng ta mở file đó lên và quan sát, ta chỉ thấy vỏn vẹn một method handle.

Code trong  app/Http/Middleware/CheckAge.php

<?php
namespace App\Http\Middleware;
use Closure;
class CheckAge
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request);
    }
}

Đầu tiên ta hãy phân tích các tham số trong method handle trước.

  •  Với $request ta có thể lấy được các thuộc tính của request hiện tại như các giá trị tham số trong URI, tên route, phương thức HTTP... 
  • Còn về $next, nó sẽ có dạng là một Closure, bạn chỉ hiểu đơn giản nó dùng để cho phép request HTTP "qua cửa", tức là cho phép request HTTP đi tiếp để xử lý, cú pháp là $next($request).

Để hiểu hơn về cách hoạt động, chúng ta sẽ đi vào một ví dụ cụ thể. Bây giờ chúng mình sẽ tạo một route như sau:

Code trong routes/web.php

Route::get('age/{age}', function ($age) {
    return $age;
});

Route trên thực hiện việc in dữ liệu ra trình duyệt khi ta truyền giá trị cho tham số age. Giả sử giờ chúng ta muốn khi nào age lớn hơn hoặc bằng 18 thì mới được in ra màn hình. Còn nếu chưa đủ thì sẽ báo "Bạn chưa đủ 18 tuổi". Lúc này ta sẽ cần áp dụng middleware CheckAge để xử lý.

Chúng ta có thể thêm middleware cho một route bất kì với phương thức middleware cùng với tham số là tên class middleware đó.

Code trong routes/web.php

use App\Http\Middleware\CheckAge;
Route::get('age/{age}', function ($age) {
    return $age;
})->middleware(CheckAge::class);

Sau đó quay lại file middleware CheckAge để tiến hành code xử lý trong method handle. Để kiểm tra xem age có lớn hơn hoặc bằng 18 hay không thì ta phải lấy được giá trị của age trong middleware. Như đã đề cập ở trên, các giá trị tham số URI sẽ được lấy thông qua $request. Lúc này đoạn code function handle trong CheckAge.php sẽ trở thành 

public function handle($request, Closure $next)
{
    if ($request->age <= 18) {
        return response('Bạn chưa đủ 18 tuổi');
    }
    
    return $next($request)
}

Như vậy là ta đã có thể đáp ứng yêu cầu trên rồi đấy, các bạn có thể nạp server và truy cập route với 2 trường hợp tham số age để test.

Taij sao laij Return hàm response cho rắc rối? Bây giờ các bạn thử bỏ hàm response đi và chỉ return string kia thôi xem sao nhé! Vâng, một lỗi đã xuất hiện:

Thông báo không lan quyên cho lắm nhỉ, ồ no liên quan đấy bạn ạ. Bây giờ bạn thử dump cái hàm response bằng cách thay dòng return 'Bạn chưa đủ 18 tuổi'; thành dd(response('Bạn chưa đủ 18 tuổi')); và quan sát:

Nó sẽ trả về một object chứa thuộc tính headers, đó là lý do vì sao khi bạn chỉ return string thì bị lỗi. Từ đó ta có thể rút ra một lưu ý:

Khi thực hiện một số công việc nào đó trong middleware thì sau khi kết thúc bạn phải return một object chứa thuộc tính headers.

Một số hành động trả về object chứa thuộc tính headers:

Hành động
Cú pháp
Cho phép HTTP request tiếp tục
return $next($request)
Trả về kết quả
return response($data)
Chuyển hướng đến một URI
return redirect($URI)


Ví dụ:

public function handle($request, Closure $next)
{
    if ($condition) {
        // Some jobs
        // ...
        
        return redirect('trangchu'); // http://localhost:8000/trangchu
    }


    // Some jobs
    // ...
    
    return $next($request);
}

Lưu ý: Chúng ta có thể type-hint bất kì service provider nào trong container trong phương thức __construct của middleware, bởi vì các service đã load trước khi middleware được gọi.

Cách hoạt động của middleware trên là chạy trước request, tức là nó được gọi ra trước khi request được gửi tới controller/action. Đôi khi trong thực tiễn, ta cần một chức năng gì đó mà request chạy trước middleware, nghĩa là sau khi request đã được xử lý ở controller/action thì mới gọi middleware ra. Laravel đã cung cấp hai cách thức hoạt động đó là: Before và after middleware.

a. Before middleware

<?php
namespace App\Http\Middleware;
use Closure;
class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // Perform action


        return $next($request);
    }
}

b. After middleware

<?php

namespace App\Http\Middleware;

use Closure;

class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // Perform action

        return $response;
    }
}

3 Đăng ký middleware (Registering middleware)

Laravel cho phép chúng ta đăng ký các middleware ở file app/Http/Kernel.php.

Lưu ý: Phải thêm dấu \ trước namespace mỗi class khi khai báo vì các middleware không cùng cấp với App\Http\Kernel:class.

3.1 Global middleware

Global middleware sẽ được autoload khi có một HTTP request gửi đến mà không cần phải khai báo ở route. Bạn có thể liệt kê danh sách các global middleware ở $middleware.

app/Http/Kernel.php

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];

Trong đây Laravel đã liệt kê sẵn một số middleware cốt lõi như kiểm tra xem có đang ở chế độ bảo trì (CheckForMaintenanceMode), hay trim string các request (TrimStrings), hay chuyển chuỗi trống sang null (ConvertEmptyStringsToNull)...

3.2 Route middleware

Nếu global middleware được load sau mỗi request thì route middleware chỉ được gọi khi request đi vào route tương ứng. Mặc định, các route middleware được liệt kê ở $routeMiddleware. Bạn quan sát sẽ thấy cấu trúc mảng nó có khác biệt so với $middleware.

app/Http/Kernel.php

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

Cú pháp này hơi quen quen đúng không nào, đúng vậy nó khá giống trong mảng aliases ở config/app.php. 

Ta có thể alias các namespace dài này thành nhưng tên ngắn gọn để dễ dàng đăng ký ở route.

Cũng như ở global middleware, Laravel cung cấp cho chúng ta một số route middleware, nhưng mình sẽ nói về các middleware này ở những tập sau.

Lấy ví dụ đoạn code phần trên, thay vì phải đăng ký middleware cho route thế này:

Code trong routes/web.php

use App\Http\Middleware\CheckAge;

Route::get('age/{age}', function ($age) {
    return $age;
})->middleware(CheckAge::class');

Ta có thể viết thành thế này

Route::get('age/{age}', function ($age) {
    return $age;
})->middleware('CheckAge');

Sau đó chỉ cần liệt kê middleware CheckAge vào trong $routeMiddleware như thế này là được:

app/Http/Kernel.php

protected $routeMiddleware = [
    // ..    
    'CheckAge' => \App\Http\Middleware\CheckAge::class,
];

3.3 Nhóm middleware

Nếu như các Route có thể nhóm lại thì middleware cũng thế. Chúng ta có thể gom các middleware một nhóm dưới dạng key chung để dễ dàng đăng ký cho route. Tất cả việc bạn cần làm là khai báo chúng trong $middlewareGroups.

Code trong app/Http/Kernel.php

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        \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,
    ],


    'api' => [
        'throttle:60,1',
        'auth:api',
    ],
];

Như đã thấy, Laravel cung cấp sẵn cho chúng ta 2 nhóm middleware đó là web và api.

  • web: giao diện người dùng
  • api: REST API

Về cú pháp đăng ký nhóm middleware cho route cũng tương tự như đăng ký một middleware riêng lẻ.

Route::get('/', function () {
    //
})->middleware('web');


Route::group(['middleware' => ['web']], function () {
    //
});

Lưu ý: Mặc định RouteServiceProvider đã đăng ký middleware web cho tất cả các route trong routes/web.php.

3.4 Sắp xếp middleware (Sorting middleware)

Đôi khi các middleware của bạn cần được gọi theo thứ tự, nhưng khi đăng ký trong route lại không có tác vụ này. Chính vì điều đó, Laravel cung cấp cho chúng ta $middlewarePriority để sort các middleware theo thứ tự ưu tiên xử lý từ trên xuống.

Code trong app/Http/Kernel.php

/**
 * The priority-sorted list of middleware.
 *
 * This forces non-global middleware to always be in the given order.
 *
 * @var array
 */
protected $middlewarePriority = [
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \App\Http\Middleware\Authenticate::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];

4. Tham số middleware

Middleware có thể nhận các tham số tùy chọn. Chẳng hạn trước khi xử lý request /post thì phải kiểm tra xem user có phải là tác giả bài viết không, nếu có thì mới xuất ra màn hình.

Đầu tiên ta khởi tạo một middle Role bằng lệnh Artisan:

php artisan make:middleware Role

Sau đó đăng ký middleware vừa tạo tại $routeMiddleware:

Code trong app/Http/Kernel.php

protected $routeMiddleware = [
    // ...    
    'role' => \App\Http\Middleware\Role::class,
];

Bước tiếp theo ta sẽ định nghĩa route như sau:

Code trong route/web.php

Route::get('/post', function () {
    return 'Body post';
})->middleware('role:editor');

Ở đây chúng ta sử dụng cú pháp : để truyền tham số vào middleware Role. Việc cần làm cuối cùng là lấy giá trị editor đó ở middleware và code xử lý thôi. Dưới đây là đoạn xử lý ở middleware Role các bạn có thể tham khảo.

public function handle($request, Closure $next, $role)
{
    if ($role != 'editor') {
        return response('Bạn không đủ quyền truy cập');
    }

    return $next($request);
}

Tại method handle, ta sẽ khai báo thêm $role để nhận giá trị tham số được truyền từ route. Việc còn lại đơn giản rồi, ta chỉ code xử lý logic là xong.

5. Terminable middleware

Đôi khi một middleware cần thực hiện công việc gì đó sau khi HTTP response được trả về. Ví dụ như middleware session trong Laravel lưu dữ liệu session sau khi response được trả về trình duyệt.

Nếu bạn định nghĩa phương thức terminate trong middleware và server web có sử dụng FastCGI thì terminate sẽ tự động được gọi sau khi response được trả về.

<?php
namespace Illuminate\Session\Middleware;
use Closure;
class StartSession
{
    public function handle($request, Closure $next)
    {
        return $next($request);
    }
    public function terminate($request, $response)
    {
        // Store the session data...
    }
}

Phương thức terminate này sẽ nhận cả request response, khi bạn đã khởi tạo một terminable middleware bạn nên đăng ký nó ở route hoặc global middleware. Khi gọi method terminate trong middleware, framework sẽ resolve một middleware instance mới từ service container. Nếu bạn muốn sử dụng một middleware instance khi các method handleterminate được gọi thì bạn nên đăng ký middleware với container thông qua method singleton.

Bình luận