Middleware
Middleware thường được sử dụng để chặn yêu cầu hoặc phản hồi. Ví dụ, thực hiện xác minh danh tính người dùng trước khi gọi bộ điều khiển, như chuyển hướng đến trang đăng nhập nếu người dùng chưa đăng nhập, hoặc thêm một tiêu đề nào đó vào phản hồi. Ví dụ, thống kê tỷ lệ yêu cầu cho một URI cụ thể,v.v.
Mô hình hành tây của Middleware
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ───────────────────> Controller ── Response ───────────────────────────> Client
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Middleware và bộ điều khiển tạo thành một mô hình hành tây cổ điển, middleware giống như các lớp vỏ hành tây, trong khi bộ điều khiển là lõi hành tây. Như hình trên, yêu cầu giống như một mũi tên xuyên qua các middleware 1, 2, 3 để đến bộ điều khiển, bộ điều khiển trả lại một phản hồi, sau đó phản hồi lại xuyên qua middleware 3, 2, 1 trước khi trở về với khách hàng. Nói cách khác, trong mỗi middleware, ta vừa có thể nhận được yêu cầu, vừa có thể nhận được phản hồi.
Ngăn chặn yêu cầu
Đôi khi ta không muốn một yêu cầu nào đến được lớp điều khiển, ví dụ, tại middleware2, nếu phát hiện rằng người dùng hiện tại chưa đăng nhập, ta có thể chặn yêu cầu và trả về một phản hồi đăng nhập. Quá trình này giống như hình dưới đây.
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ─────────┐ │ │ Controller │ │ │ │
│ │ Response │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
Như hình trên, yêu cầu đến middleware2 và tạo ra một phản hồi đăng nhập, phản hồi đi từ middleware2 ra middleware1 rồi trả về cho khách hàng.
Giao diện Middleware
Middleware phải thực hiện giao diện Webman\MiddlewareInterface
.
interface MiddlewareInterface
{
/**
* Xử lý một yêu cầu máy chủ đến.
*
* Xử lý một yêu cầu máy chủ đến nhằm tạo ra một phản hồi.
* Nếu không thể tạo ra phản hồi tự nó, nó có thể ủy quyền cho
* bộ xử lý yêu cầu được cung cấp để thực hiện.
*/
public function process(Request $request, callable $handler): Response;
}
Điều này có nghĩa là phải thực hiện phương thức process
, phương thức process
phải trả về một đối tượng support\Response
, mặc định đối tượng này được tạo ra bởi $handler($request)
(yêu cầu sẽ tiếp tục xuyên vào lõi hành tây), cũng có thể là phản hồi từ các hàm trợ giúp như response()
, json()
, xml()
, redirect()
, v.v. (yêu cầu sẽ dừng lại không tiếp tục vào lõi hành tây).
Lấy yêu cầu và phản hồi trong Middleware
Trong middleware, ta có thể lấy yêu cầu và cũng có thể lấy phản hồi sau khi thực hiện bộ điều khiển, vì vậy middleware nội bộ sẽ được chia thành ba phần.
- Giai đoạn xuyên qua yêu cầu, tức là giai đoạn trước khi xử lý yêu cầu
- Giai đoạn bộ điều khiển xử lý yêu cầu, tức là giai đoạn xử lý yêu cầu
- Giai đoạn phản hồi đi ra, tức là giai đoạn sau khi xử lý yêu cầu
Ba giai đoạn này sẽ được thể hiện trong middleware như sau:
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Test implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
echo 'Đây là giai đoạn xuyên qua yêu cầu, tức là trước khi xử lý yêu cầu';
$response = $handler($request); // tiếp tục xuyên qua lõi hành tây, cho đến khi gọi bộ điều khiển nhận được phản hồi
echo 'Đây là giai đoạn phản hồi đi ra, tức là sau khi xử lý yêu cầu';
return $response;
}
}
Ví dụ: Middleware xác thực
Tạo file app/middleware/AuthCheckTest.php
(nếu thư mục chưa tồn tại, hãy tự tạo) như sau:
<?php
namespace app\middleware;
use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class AuthCheckTest implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
if (session('user')) {
// Đã đăng nhập, yêu cầu tiếp tục xuyên qua lõi hành tây
return $handler($request);
}
// Sử dụng phản xạ để xác định các phương thức nào của bộ điều khiển không cần đăng nhập
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
// Phương thức được truy cập cần đăng nhập
if (!in_array($request->action, $noNeedLogin)) {
// Chặn yêu cầu, trả về một phản hồi chuyển hướng, yêu cầu dừng lại không tiếp tục xuyên qua lõi hành tây
return redirect('/user/login');
}
// Không cần đăng nhập, yêu cầu tiếp tục xuyên qua lõi hành tây
return $handler($request);
}
}
Tạo mới bộ điều khiển app/controller/UserController.php
<?php
namespace app\controller;
use support\Request;
class UserController
{
/**
* Phương thức không cần đăng nhập
*/
protected $noNeedLogin = ['login'];
public function login(Request $request)
{
$request->session()->set('user', ['id' => 10, 'name' => 'webman']);
return json(['code' => 0, 'msg' => 'đăng nhập ok']);
}
public function info()
{
return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
}
}
Lưu ý
$noNeedLogin
chứa các phương thức của bộ điều khiển hiện tại có thể truy cập mà không cần đăng nhập.
Trong config/middleware.php
, thêm middleware toàn cầu như sau:
return [
// Middleware toàn cầu
'' => [
// ... Ở đây bỏ qua các middleware khác
app\middleware\AuthCheckTest::class,
]
];
Với middleware xác thực, ta có thể tập trung viết mã nghiệp vụ trong lớp điều khiển mà không cần lo lắng về việc người dùng có đăng nhập hay không.
Ví dụ: Middleware yêu cầu đa nguồn
Tạo file app/middleware/AccessControlTest.php
(nếu thư mục chưa tồn tại, hãy tự tạo) như sau:
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class AccessControlTest implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// Nếu là yêu cầu OPTIONS thì trả về một phản hồi trống, nếu không tiếp tục xuyên qua lõi hành tây và nhận được một phản hồi
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// Thêm các tiêu đề liên quan đến cors vào phản hồi
$response->withHeaders([
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
]);
return $response;
}
}
Nhắc nhở
Kết nối đa nguồn có thể gửi yêu cầu OPTIONS, ta không muốn yêu cầu OPTIONS vào bộ điều khiển, vì vậy ta trả về một phản hồi rỗng trực tiếp cho yêu cầu OPTIONS (response('')
) để thực hiện chặn yêu cầu.
Nếu API của bạn cần thiết lập định tuyến, hãy sử dụngRoute::any(..)
hoặcRoute::add(['POST', 'OPTIONS'], ..)
để thiết lập.
Trong config/middleware.php
, thêm middleware toàn cầu như sau:
return [
// Middleware toàn cầu
'' => [
// ... Ở đây bỏ qua các middleware khác
app\middleware\AccessControlTest::class,
]
];
Lưu ý
Nếu yêu cầu ajax tự định nghĩa tiêu đề, cần thêm tiêu đề tùy chỉnh vào trườngAccess-Control-Allow-Headers
trong middleware, nếu không sẽ báoRequest header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.
Giải thích
- Middleware được chia thành middleware toàn cầu, middleware ứng dụng (middleware ứng dụng chỉ có hiệu lực trong chế độ đa ứng dụng, xem đa ứng dụng), và middleware định tuyến.
- Vị trí tệp cấu hình middleware là
config/middleware.php
- Middleware toàn cầu được cấu hình dưới khóa
''
- Middleware ứng dụng được cấu hình dưới tên ứng dụng cụ thể, ví dụ:
return [
// Middleware toàn cầu
'' => [
app\middleware\AuthCheckTest::class,
app\middleware\AccessControlTest::class,
],
// Middleware ứng dụng api (middleware ứng dụng chỉ có hiệu lực trong chế độ đa ứng dụng)
'api' => [
app\middleware\ApiOnly::class,
]
];
Middleware trong bộ điều khiển và phương thức middleware
Sử dụng chú thích, ta có thể thiết lập middleware cho một bộ điều khiển hoặc một phương thức cụ thể trong bộ điều khiển.
<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;
#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
#[Middleware(Method1Middleware::class, Method2Middleware::class)]
public function index(Request $request): string
{
return 'hello';
}
}
Middleware định tuyến
Ta có thể thiết lập middleware cho một hoặc một nhóm các định tuyến.
Ví dụ trong config/route.php
, thêm cấu hình như sau:
<?php
use support\Request;
use Webman\Route;
Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
app\middleware\MiddlewareA::class,
app\middleware\MiddlewareB::class,
]);
Route::group('/blog', function () {
Route::any('/create', function () {return response('create');});
Route::any('/edit', function () {return response('edit');});
Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
app\middleware\MiddlewareA::class,
app\middleware\MiddlewareB::class,
]);
Truyền tham số cho Middleware qua hàm khởi tạo
Tệp cấu hình hỗ trợ khởi tạo trực tiếp middleware hoặc hàm ẩn danh, điều này giúp dễ dàng truyền tham số cho middleware qua hàm khởi tạo.
Ví dụ trong config/middleware.php
cũng có thể cấu hình như sau:
return [
// Middleware toàn cầu
'' => [
new app\middleware\AuthCheckTest($param1, $param2, ...),
function(){
return new app\middleware\AccessControlTest($param1, $param2, ...);
},
],
// Middleware ứng dụng api (middleware ứng dụng chỉ có hiệu lực trong chế độ đa ứng dụng)
'api' => [
app\middleware\ApiOnly::class,
]
];
Tương tự, middleware định tuyến cũng có thể truyền tham số cho middleware qua hàm khởi tạo, ví dụ trong config/route.php
:
Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
new app\middleware\MiddlewareA($param1, $param2, ...),
function(){
return new app\middleware\MiddlewareB($param1, $param2, ...);
},
]);
Sử dụng tham số trong middleware ví dụ
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class MiddlewareA implements MiddlewareInterface
{
protected $param1;
protected $param2;
public function __construct($param1, $param2)
{
$this->param1 = $param1;
$this->param2 = $param2;
}
public function process(Request $request, callable $handler) : Response
{
var_dump($this->param1, $this->param2);
return $handler($request);
}
}
Thứ tự thực thi Middleware
- Thứ tự thực hiện middleware là
middleware toàn cầu
->middleware ứng dụng
->middleware bộ điều khiển
->middleware định tuyến
->middleware phương thức
. - Khi một lớp có nhiều middleware, sẽ thực hiện theo thứ tự thực tế cấu hình của các middleware cùng lớp.
- Các yêu cầu 404 sẽ không kích hoạt bất kỳ middleware nào (tuy nhiên vẫn có thể thêm middleware thông qua
Route::fallback(function(){})->middleware()
).
Truyền tham số đến middleware từ định tuyến (route->setParams)
Cấu hình định tuyến config/route.php
<?php
use support\Request;
use Webman\Route;
Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);
Middleware (giả sử là middleware toàn cầu)
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// Mặc định, $request->route là null, vì vậy cần kiểm tra xem $request->route có phải là null không
if ($route = $request->route) {
$value = $route->param('some_key');
var_export($value);
}
return $handler($request);
}
}
Truyền tham số đến bộ điều khiển từ middleware
Đôi khi bộ điều khiển cần sử dụng dữ liệu được tạo ra trong middleware, lúc này ta có thể thêm thuộc tính vào đối tượng $request
để truyền tham số đến bộ điều khiển. Ví dụ:
Middleware
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$request->data = 'some value';
return $handler($request);
}
}
Bộ điều khiển:
<?php
namespace app\controller;
use support\Request;
class FooController
{
public function index(Request $request)
{
return response($request->data);
}
}
Lấy thông tin định tuyến của yêu cầu hiện tại trong middleware
Ta có thể sử dụng $request->route
để lấy đối tượng định tuyến, thông qua việc gọi các phương thức liên quan để lấy thông tin tương ứng.
Cấu hình định tuyến
<?php
use support\Request;
use Webman\Route;
Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);
Middleware
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$route = $request->route;
// Nếu yêu cầu không khớp với bất kỳ định tuyến nào (ngoại trừ định tuyến mặc định), thì $request->route là null
// Giả sử trình duyệt truy cập địa chỉ /user/111, thì sẽ in thông tin như sau
if ($route) {
var_export($route->getPath()); // /user/{uid}
var_export($route->getMethods()); // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
var_export($route->getName()); // user_view
var_export($route->getMiddleware()); // []
var_export($route->getCallback()); // ['app\\controller\\UserController', 'view']
var_export($route->param()); // ['uid'=>111]
var_export($route->param('uid')); // 111
}
return $handler($request);
}
}
Lưu ý
Lấy ngoại lệ trong Middleware
Trong quá trình xử lý nghiệp vụ, có thể xảy ra ngoại lệ, trong middleware có thể sử dụng $response->exception()
để lấy ngoại lệ.
Cấu hình định tuyến
<?php
use support\Request;
use Webman\Route;
Route::any('/user/{uid}', function (Request $request, $uid) {
throw new \Exception('exception test');
});
Middleware:
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Hello implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$response = $handler($request);
$exception = $response->exception();
if ($exception) {
echo $exception->getMessage();
}
return $response;
}
}
Middleware siêu toàn cầu
Middleware toàn cầu của dự án chính chỉ ảnh hưởng đến dự án chính, không ảnh hưởng đến plugin ứng dụng. Đôi khi ta muốn thêm một middleware ảnh hưởng đến toàn cầu bao gồm cả tất cả các plugin, ta có thể sử dụng middleware siêu toàn cầu.
Trong config/middleware.php
cấu hình như sau:
return [
'@' => [ // Thêm middleware toàn cầu cho dự án chính và tất cả các plugin
app\middleware\MiddlewareGlobl::class,
],
'' => [], // Chỉ thêm middleware toàn cầu cho dự án chính
];
Nhắc nhở
Middleware siêu toàn cầu@
không chỉ có thể được cấu hình trong dự án chính mà cũng có thể được cấu hình trong bất kỳ plugin nào, ví dụ cấu hình middleware siêu toàn cầu@
trongplugin/ai/config/middleware.php
cũng sẽ ảnh hưởng đến dự án chính và tất cả các plugin.
Thêm middleware cho một plugin nào đó
Đôi khi ta muốn thêm một middleware cho một plugin ứng dụng mà không muốn thay đổi mã của plugin đó (bởi vì việc cập nhật sẽ bị ghi đè), lúc này ta có thể cấu hình middleware trong dự án chính.
Trong config/middleware.php
cấu hình như sau:
return [
'plugin.ai' => [], // Thêm middleware cho plugin ai
'plugin.ai.admin' => [], // Thêm middleware cho module admin của plugin ai (thư mục plugin\ai\app\admin)
];
Nhắc nhở
Tất nhiên, cũng có thể thêm cấu hình tương tự trong một plugin nào đó để ảnh hưởng đến các plugin khác, ví dụ trongplugin/foo/config/middleware.php
thêm cấu hình như trên thì cũng sẽ ảnh hưởng đến plugin ai.