Middleware
Middleware มักจะใช้สำหรับการดักจับคำร้องหรือการตอบสนอง เช่น การตรวจสอบตัวตนของผู้ใช้ก่อนที่จะทำการควบคุม หากผู้ใช้ยังไม่ได้เข้าสู่ระบบ จะถูกเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ หรือเพิ่ม header ในการตอบสนอง เป็นต้น รวมถึงการสถิติอัตราการร้องขอจาก URI ที่กำหนด
Onion Model ของ Middleware
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Reqeust ───────────────────> Controller ── Response ───────────────────────────> Client
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Middleware และการควบคุมรูปแบบ Onion Model แบบคลาสสิก โดย Middleware จะคล้ายกับเปลือกหัวหอมแต่ละชั้น ส่วนการควบคุมจะเป็นเนื้อของหัวหอม เช่นในภาพที่แสดงให้เห็นคำร้องเดินทางผ่าน middleware 1, 2, 3 ไปยัง Controller ซึ่ง Controller จะส่งกลับการตอบสนอง จากนั้นการตอบสนองจะเดินทางกลับผ่าน middleware 3, 2, 1 ก่อนที่จะส่งกลับไปยังลูกค้า กล่าวอีกนัยหนึ่งใน Middleware ทุกตัวเราสามารถเข้าถึงทั้งคำร้องและการตอบสนองได้
การดักจับคำร้อง
บางครั้งเราก็ไม่ต้องการให้คำร้องไปถึง Controller เช่น ถ้าใน middleware 2 ตรวจพบว่าผู้ใช้ยังไม่ได้เข้าสู่ระบบ ก็สามารถดักจับคำร้องและส่งกลับการตอบสนองล็อกอินได้ ดังนั้นไหลเวียนนี้ก็จะแสดงดังนี้
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Reqeust ─────────┐ │ │ Controller │ │ │ │
│ │ Response │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
จากภาพ คำร้องมาถึง middleware 2 และสร้างการตอบสนองล็อกอิน ซึ่งการตอบสนองจะเดินทางกลับผ่าน middleware 1 ก่อนที่จะส่งกลับไปยังลูกค้า
อินเตอร์เฟซของ Middleware
Middleware ต้องดำเนินการ Webman\MiddlewareInterface
อินเตอร์เฟซ
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(Request $request, callable $handler): Response;
}
ซึ่งแสดงว่าต้องมีการดำเนินการ process
เมธอด เมธอด process
ต้องคืนค่าที่เป็น support\Response
วัตถุ โดยวัตถุเริ่มต้นนี้ถูกสร้างโดย $handler($request)
(คำร้องจะเดินทางต่อไปยังเนื้อของหัวหอมต่อไป) นอกจากนี้ยังสามารถเป็นการตอบสนองที่สร้างโดยฟังก์ชันผู้ช่วย เช่น response()
, json()
, xml()
, redirect()
เป็นต้น (ซึ่งจะหยุดการเดินทางต่อไปยังเนื้อของหัวหอม)
การเข้าถึงคำร้องและการตอบสนองใน Middleware
ใน Middleware เราสามารถเข้าถึงคำร้องและการตอบสนองที่เกิดขึ้นหลังจากการควบคุม ดังนั้น Middleware จะแบ่งเป็นสามส่วน
- ขั้นตอนการเดินทางของคำร้อง ซึ่งจะหมายถึงก่อนการจัดการคำร้อง
- ขั้นตอนการจัดการคำร้องโดย Controller
- ขั้นตอนการตอบสนองซึ่งหมายถึงหลังจากการจัดการคำร้อง
สามขั้นตอนใน Middleware จะมีลักษณะดังนี้
<?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 'ที่นี่คือขั้นตอนการเดินทางของคำร้อง หมายถึงก่อนการจัดการคำร้อง';
$response = $handler($request); // เดินทางต่อไปยังเนื้อของหัวหอม จนกว่าจะจัดการ Controller และได้รับการตอบสนอง
echo 'ที่นี่คือขั้นตอนการตอบสนองซึ่งหมายถึงหลังจากการจัดการคำร้อง';
return $response;
}
}
ตัวอย่าง: Middleware การตรวจสอบตัวตน
สร้างไฟล์ app/middleware/AuthCheckTest.php
(หากไม่มีไดเรกทอรีโปรดสร้างเอง) ดังต่อไปนี้:
<?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')) {
// เข้าสู่ระบบแล้ว คำร้องจะเดินทางต่อไปยังเนื้อของหัวหอม
return $handler($request);
}
// ใช้การสะท้อนเพื่อรับข้อมูลว่าฟังก์ชันใดของ Controller ที่ไม่ต้องการการเข้าสู่ระบบ
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
// ฟังก์ชันที่เรียกต้องการการเข้าสู่ระบบ
if (!in_array($request->action, $noNeedLogin)) {
// ดักจับคำร้อง ส่งกลับการตอบสนองเปลี่ยนเส้นทาง หยุดการเดินทางต่อไปยังเนื้อของหัวหอม
return redirect('/user/login');
}
// ไม่ต้องการการเข้าสู่ระบบ คำร้องจะเดินทางต่อไปยังเนื้อของหัวหอม
return $handler($request);
}
}
สร้าง Controller ใหม่ app/controller/UserController.php
<?php
namespace app\controller;
use support\Request;
class UserController
{
/**
* ฟังก์ชันที่ไม่ต้องการการเข้าสู่ระบบ
*/
protected $noNeedLogin = ['login'];
public function login(Request $request)
{
$request->session()->set('user', ['id' => 10, 'name' => 'webman']);
return json(['code' => 0, 'msg' => 'ล็อกอินสำเร็จ']);
}
public function info()
{
return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
}
}
หมายเหตุ
$noNeedLogin
จะบันทึกฟังก์ชันที่สามารถเข้าถึงได้โดยไม่ต้องเข้าสู่ระบบของ Controller ปัจจุบัน
ใน config/middleware.php
ให้เพิ่ม Middleware ทั่วไปดังนี้:
return [
// Middleware ทั่วไป
'' => [
// ... ที่นี่ไม่รวม Middleware อื่น ๆ
app\middleware\AuthCheckTest::class,
]
];
ด้วย Middleware การตรวจสอบตัวตนนี้ เราสามารถเขียนโค้ดธุรกิจในระดับ Controller ได้อย่างสงบ โดยไม่ต้องกังวลเกี่ยวกับว่าผู้ใช้จะเข้าสู่ระบบหรือไม่
ตัวอย่าง: Middleware การร้องขอข้ามโดเมน
สร้างไฟล์ app/middleware/AccessControlTest.php
(หากไม่มีไดเรกทอรีโปรดสร้างเอง) ดังนี้:
<?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
{
// หากเป็นคำร้องแบบ options จะส่งคืนการตอบสนองเปล่า มิฉะนั้นให้เดินทางต่อไปยังเนื้อของหัวหอมและได้รับการตอบสนอง
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// เพิ่ม header ที่เกี่ยวข้องกับการข้ามโดเมนลงในการตอบสนอง
$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;
}
}
คำแนะนำ
คำร้องข้ามโดเมนอาจทำให้เกิดคำร้องแบบ OPTIONS และเราไม่ต้องการให้คำร้องแบบ OPTIONS เข้าสู่ Controller ดังนั้นเราจึงส่งคืนการตอบสนองเปล่า(response('')
) เพื่อทำการดักจับคำร้อง
หาก API ของคุณต้องการตั้งค่าเส้นทาง โปรดใช้Route::any(..)
หรือRoute::add(['POST', 'OPTIONS'], ..)
เพื่อทำการตั้งค่า
ใน config/middleware.php
ให้เพิ่ม Middleware ทั่วไปดังนี้:
return [
// Middleware ทั่วไป
'' => [
// ... ที่นี่ไม่รวม Middleware อื่น ๆ
app\middleware\AccessControlTest::class,
]
];
หมายเหตุ
หากคำร้อง AJAX มี header ที่กำหนดเอง คุณจำเป็นต้องเพิ่มหัวข้อที่กำหนดเองนี้ในฟิลด์Access-Control-Allow-Headers
ใน Middleware มิฉะนั้นจะเกิดข้อผิดพลาดRequest header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.
หมายเหตุ
- Middleware แบ่งออกเป็น Middleware ทั่วไป, Middleware แอปพลิเคชัน (Middleware แอปพลิเคชันใช้ได้เฉพาะในโหมดมัลติแอป) และ Middleware เส้นทาง
- ไฟล์การกำหนดค่าของ Middleware จะอยู่ใน
config/middleware.php
- การกำหนดค่า Middleware ทั่วไปจะอยู่ที่กุญแจ
''
- การกำหนดค่า Middleware แอพพลิเคชั่นจะอยู่ภายใต้ชื่อแอพพลิเคชันเฉพาะ เช่น
return [
// Middleware ทั่วไป
'' => [
app\middleware\AuthCheckTest::class,
app\middleware\AccessControlTest::class,
],
// Middleware แอพพลิเคชั่น API (Middleware แอพพลิเคชั่นใช้ได้เฉพาะในโหมดมัลติแอป)
'api' => [
app\middleware\ApiOnly::class,
]
];
Middleware สำหรับ Controller และวิธีการ Middleware
โดยใช้อนุกรมเราสามารถกำหนด Middleware ให้กับ Controller หรือวิธีการของ Controller ใด ๆ
<?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 เส้นทาง
เราสามารถกำหนด Middleware ให้กับเส้นทางหนึ่งเส้นทางหรือกลุ่มเส้นทางได้
เช่นใน config/route.php
ให้เพิ่มการกำหนดค่าดังต่อไปนี้:
<?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,
]);
การส่งพารามิเตอร์ใน Middleware
ไฟล์การกำหนดค่ารองรับการสร้าง Middleware หรือฟังก์ชันอนุกรมได้โดยตรง ซึ่งจะช่วยให้เราส่งพารามิเตอร์ไปยัง Middleware ผ่านคอนสตรัคเตอร์ได้ง่ายขึ้น
เช่นใน config/middleware.php
ก็สามารถตั้งค่าได้ดังนี้
return [
// Middleware ทั่วไป
'' => [
new app\middleware\AuthCheckTest($param1, $param2, ...),
function(){
return new app\middleware\AccessControlTest($param1, $param2, ...);
},
],
// Middleware แอพพลิเคชั่น API (Middleware แอพพลิเคชั่นใช้ได้เฉพาะในโหมดมัลติแอป)
'api' => [
app\middleware\ApiOnly::class,
]
];
ในทำนองเดียวกัน Middleware เส้นทางก็สามารถส่งพารามิเตอร์เข้าไปได้เช่นกัน เช่นใน 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, ...);
},
]);
การใช้พารามิเตอร์ใน Middleware ตัวอย่าง
<?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);
}
}
ลำดับการดำเนินการของ Middleware
- ลำดับการทำงานของ Middleware จะเป็น
Middleware ทั่วไป
->Middleware แอพพลิเคชั่น
->Middleware ของ Controller
->Middleware เส้นทาง
->Middleware วิธีการ
- เมื่อมี Middleware หลายตัวในระดับเดียวกัน จะดำเนินการตามลำดับการกำหนดจริงในระดับเดียวกัน
- คำร้อง 404 โดยค่าเริ่มต้นจะไม่กระตุ้น Middleware ใด ๆ (แต่ยังสามารถเพิ่ม Middleware โดยใช้
Route::fallback(function(){})->middleware()
)
การส่งพารามิเตอร์ไปยัง Middleware
การตั้งค่าเส้นทางใน config/route.php
<?php
use support\Request;
use Webman\Route;
Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);
Middleware (สมมุติว่าเป็น 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->route จะเป็น null ดังนั้นจะต้องตรวจสอบว่า $request->route เป็นศูนย์
if ($route = $request->route) {
$value = $route->param('some_key');
var_export($value);
}
return $handler($request);
}
}
การส่งพารามิเตอร์จาก Middleware ไปยัง Controller
บางครั้ง Controller อาจต้องการใช้ข้อมูลที่สร้างโดย Middleware ในกรณีนี้เราสามารถใช้การเพิ่มคุณสมบัติไปยังออบเจ็กต์ $request
เพื่อส่งพารามิเตอร์ไปยัง Controller ได้ ตัวอย่างเช่น:
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);
}
}
Controller:
<?php
namespace app\controller;
use support\Request;
class FooController
{
public function index(Request $request)
{
return response($request->data);
}
}
การเข้าถึงข้อมูลเส้นทางคำร้องใน Middleware
เราสามารถใช้ $request->route
เพื่อเข้าถึงออบเจ็กต์เส้นทางโดยการเรียกใช้วิธีที่เกี่ยวข้องเพื่อรับข้อมูลที่เหมาะสม
การตั้งค่าเส้นทาง
<?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;
// หากคำร้องไม่มีเส้นทางใดที่ตรงกับ (ยกเว้นเส้นทางเริ่มต้น) จะไม่มีค่า $request->route
// สมมุติว่าเบราว์เซอร์เข้าถึงที่อยู่ /user/111 จะพิมพ์ข้อมูลดังนี้
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);
}
}
หมายเหตุ
การจัดการข้อยกเว้นใน Middleware
ในระหว่างกระบวนการดำเนินการทางธุรกิจ อาจเกิดข้อยกเว้นได้ เราสามารถใช้ $response->exception()
เพื่อรับข้อยกเว้นใน Middleware
การตั้งค่าเส้นทาง
<?php
use support\Request;
use Webman\Route;
Route::any('/user/{uid}', function (Request $request, $uid) {
throw new \Exception('ข้อยกเว้นการทดสอบ');
});
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 กว้าง
Middleware ทั่วไปในโครงการหลักจะส่งผลต่อเพียงโครงการหลัก ไม่ส่งผลกระทบต่อ แอปพลิเคชันปลั๊กอิน บางครั้งเราต้องการเพิ่ม Middleware ที่มีผลต่อทุกอย่างรวมถึงทุกปลั๊กอิน สามารถใช้งาน Middleware กว้างได้
ใน config/middleware.php
ให้ตั้งค่าดังนี้:
return [
'@' => [ // เพิ่ม Middleware ทั่วไปในโครงการหลักและทุกปลั๊กอิน
app\middleware\MiddlewareGlobl::class,
],
'' => [], // เพิ่ม Middleware ทั่วไปในโครงการหลักเท่านั้น
];
คำแนะนำ
Middleware กว้าง@
ไม่เพียงแค่สามารถกำหนดในโครงการหลัก แต่ยังสามารถกำหนดในปลั๊กอินใด ๆ เช่น หากกำหนด Middleware กว้างในplugin/ai/config/middleware.php
ก็จะส่งผลต่อโครงการหลักและปลั๊กอินทั้งหมด
การเพิ่ม Middleware ให้กับปลั๊กอิน
บางครั้งเราอาจต้องการเพิ่ม Middleware ให้กับ แอปพลิเคชันปลั๊กอิน โดยไม่ต้องแก้ไขโค้ดของปลั๊กอิน (เพราะการอัปเกรดจะถูกเขียนทับ) เราจึงสามารถกำหนด Middleware ในโครงการหลักให้กับปลั๊กอินนั้นได้
ใน config/middleware.php
ให้ตั้งค่าดังนี้:
return [
'plugin.ai' => [], // เพิ่ม Middleware ให้กับปลั๊กอิน ai
'plugin.ai.admin' => [], // เพิ่ม Middleware ให้กับโมดูล admin ของปลั๊กอิน ai (ไดเรกทอรี plugin\ai\app\admin)
];
คำแนะนำ
สามารถเพิ่มการกำหนดค่าที่คล้ายคลึงกันในปลั๊กอินบางตัวเพื่อส่งผลต่อปลั๊กอินอื่น เช่น ในplugin/foo/config/middleware.php
ใส่การกำหนดค่าดังกล่าวจะส่งผลต่อปลั๊กอิน ai