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 จะแบ่งเป็นสามส่วน

  1. ขั้นตอนการเดินทางของคำร้อง ซึ่งจะหมายถึงก่อนการจัดการคำร้อง
  2. ขั้นตอนการจัดการคำร้องโดย Controller
  3. ขั้นตอนการตอบสนองซึ่งหมายถึงหลังจากการจัดการคำร้อง

สามขั้นตอนใน 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