跳至主要內容
技術

Laravel 的魔法與紀律:Request Lifecycle、Service Container 與 Middleware

Laravel 的魔法與紀律:Request Lifecycle、Service Container 與 Middleware
PHP/Laravel 完全指南 第 3 / 15 篇

本篇是「PHP/Laravel 完全指南」系列的第 3 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。

用 Laravel 寫程式的時候,你有沒有一種「魔法」的感覺?Controller 的參數會自動注入、Middleware 不知道在哪裡就生效了、Facade 明明是靜態呼叫卻能在測試裡被 mock。這些看起來很酷,但如果你不理解背後的機制,遲早會在 debug 的時候撞牆——因為你不知道東西是從哪裡冒出來的。

這一章我們要拆解 Laravel 最核心的三個概念:Request Lifecycle(一個 HTTP 請求從進來到回去的完整旅程)、Service Container(Laravel 的依賴注入容器,也是整個框架的心臟)、以及 Middleware(請求的過濾器與守門員)。這三樣東西搞懂了,你就從「會用 Laravel」升級成「理解 Laravel」,debug 的時候也比較知道該往哪裡找。

我們也會在揪好買專案裡實際動手:寫一個 Request 記錄 Middleware,讓每個進來的請求都留下足跡;註冊第一個 Service Provider,體會 Container 的運作方式。理論搭配實作,理解才會紮實。

一個 Request 的旅程:從瀏覽器到 Response

上一章我們看過「六步驟概覽」,這次我們用更精細的視角來追蹤。當使用者在瀏覽器輸入 http://localhost:8000/ 按下 Enter,以下是完整的旅程:

瀏覽器 → Nginx/Apache → public/index.php
  → Bootstrap Application (bootstrap/app.php)
  → Service Providers (register + boot)
  → Global Middleware(依序執行)
  → Route Middleware(針對特定路由)
  → Router(比對 URL → Controller)
  → Controller 處理邏輯
  → Response(反向穿越 Middleware)
  → 瀏覽器收到 HTML

起點:public/index.php

整個 Laravel 應用只有一個入口——public/index.php。不管使用者訪問 //products/123 還是 /api/orders所有請求都從這裡進來。這叫做「Front Controller」模式。

// public/index.php(Laravel 11/12/13 實際版本)
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

// 檢查維護模式...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance;
}

// 載入 Composer autoloader...
require __DIR__.'/../vendor/autoload.php';

// 啟動 Laravel 並處理請求...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';

$app->handleRequest(Request::capture());

三件事,從上到下:

  1. 載入 Composer autoloader——讓所有 class 可以自動載入
  2. 建立 Application 實例——這就是 Service Container 本人
  3. handleRequest() 一行搞定全流程——內部仍會走 HTTP Kernel(Middleware → 路由 → Controller),產出 Response 並送回瀏覽器,善後任務也在此完成

Express 開發者的類比: public/index.php 就像你的 server.js 入口。Front Controller 模式等同於 Express 的 app.use() 中介軟體管線——所有請求都通過同一條管線。

bootstrap/app.php:應用程式的組裝說明書

從 Laravel 11 開始,bootstrap/app.php 取代了舊版的 Http Kernel 和 Console Kernel,成為應用程式的唯一組裝設定檔:

// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // 在這裡自訂 Middleware
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // 在這裡自訂例外處理
    })
    ->create();

三個 with 方法,分別設定路由、Middleware 和例外處理。整個 Laravel 的啟動設定就在這一個檔案裡,乾淨又集中。

Service Container:Laravel 的心臟

Service Container 是 Laravel 最重要的概念,沒有之一。如果你只能從這章記住一件事,就記住這個。

什麼是 Service Container?

用最白話的方式說:Service Container 是一個物件工廠。你告訴它「我需要一個 X」,它就幫你把 X 造出來——包括 X 依賴的 Y 和 Z,也一併搞定。

// 你不用這樣寫(手動建立所有依賴)
$logger = new FileLogger('/var/log/app.log');
$mailer = new SmtpMailer('smtp.gmail.com', 587, $credentials);
$notifier = new OrderNotifier($mailer, $logger);
$controller = new OrderController($notifier);

// Container 幫你搞定一切——你只需要「要」
$controller = app(OrderController::class);
// Container 自動解析所有依賴,遞迴建立

為什麼需要它?

想像你在蓋一棟房子。沒有 Container 的世界裡,你得自己去買磚、拌水泥、叫水電工、找油漆師傅——每蓋一棟都從頭來。有 Container 就像有個包工頭:你說「我要一棟三房兩廳」,他幫你搞定所有上下游。

實際好處:

  1. 解耦合——你的程式碼不需要知道「怎麼建立」依賴,只需要宣告「我需要什麼」
  2. 可測試——測試時可以替換假的依賴(mock),不用動到真正的資料庫或郵件服務
  3. 靈活性——想換 Logger 實作?改一處設定,全站生效

跨語言對照

概念LaravelSpring (Java)NestJS (Node.js)Python
IoC ContainerService ContainerApplicationContextModule + providers通常不內建
註冊服務bind() / singleton()@Bean@Injectable()
自動注入Type-hint in constructor@AutowiredConstructor injection

JS/Python 開發者注意: JavaScript 和 Python 社群通常不使用 IoC Container(因為語言的動態性質讓手動 DI 比較容易)。但在 PHP 和 Java 的世界裡,Container 是框架的基石。如果這個概念對你來說很新,不用擔心——跟著往下看,很快就會上手。

基本操作

// 1. 綁定:告訴 Container「當有人要 X 的時候,這樣造」
app()->bind(PaymentGateway::class, function () {
    return new StripeGateway(config('services.stripe.secret'));
});

// 2. 解析:「我需要一個 PaymentGateway」
$gateway = app(PaymentGateway::class);
// Container 執行上面的 closure,回傳 StripeGateway 實例

// 3. 單例模式:整個 request 只建立一次
app()->singleton(ShoppingCart::class, function () {
    return new ShoppingCart();
});
// 不管你 resolve 幾次,拿到的都是同一個實例

介面綁定:最強大的用法

// 綁定介面到實作
app()->bind(
    PaymentGatewayInterface::class,
    StripeGateway::class
);

// 之後不管哪裡需要 PaymentGatewayInterface
// Container 都會自動給你 StripeGateway

// 想換成 ECPay?改這一行就好
app()->bind(
    PaymentGatewayInterface::class,
    EcPayGateway::class
);

這就是「面向介面編程」的威力——你的 Controller 不知道也不關心後面是 Stripe 還是 ECPay,它只跟介面說話。

依賴注入:不用自己 new 物件

依賴注入(Dependency Injection, DI)是 Service Container 最常被使用的方式。簡單說就是:不要自己建立依賴,讓框架幫你注入

Constructor Injection

最常見也最推薦的注入方式——在 constructor 裡用 type-hint 宣告你需要什麼:

class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService,
        private PaymentGatewayInterface $gateway,
    ) {}

    public function store(Request $request)
    {
        $order = $this->orderService->create($request->all());
        $this->gateway->charge($order->total);

        return redirect('/orders/' . $order->id);
    }
}

你完全沒有寫過 new OrderService()new StripeGateway()——Container 看到 constructor 的 type-hint,自動幫你建立並注入。

Method Injection

在 Controller 的方法裡也可以注入:

class ProductController extends Controller
{
    // Request 自動注入、Product 自動透過 Route Model Binding 注入
    public function show(Request $request, Product $product)
    {
        return view('products.show', compact('product'));
    }
}

Request 是 Laravel 的 HTTP 請求物件,Product 是透過 URL 參數自動查到的 Eloquent Model(這叫 Route Model Binding,第四章會詳細介紹)。兩個都是 Container 自動注入的。

自動解析的魔法

Container 怎麼知道要給你什麼?它的邏輯很簡單:

  1. 看 constructor 的 type-hint
  2. 如果是具體類別(如 OrderService)→ 直接 new 出來(遞迴解析它的依賴)
  3. 如果是介面(如 PaymentGatewayInterface)→ 查綁定表,找到對應的實作
  4. 如果找不到 → 丟出 BindingResolutionException

大多數時候你不需要手動 bind()。只要你的類別 constructor 裡用的是具體類別,Container 會自動解析,完全不需要設定。只有當你用介面的時候,才需要告訴 Container「這個介面對應哪個實作」。

Service Provider:應用程式的啟動清單

Service Provider 是你告訴 Container「要綁定哪些東西」的地方。每個 Laravel 應用在啟動時都會跑過一系列的 Service Provider,把需要的服務註冊到 Container 裡。

結構

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 註冊階段:綁定服務到 Container
     * 這裡只做 bind/singleton,不要存取其他服務
     */
    public function register(): void
    {
        $this->app->bind(
            PaymentGatewayInterface::class,
            StripeGateway::class
        );
    }

    /**
     * 啟動階段:所有 Provider 都 register 完了,可以安全地使用任何服務
     * 適合放 Event Listener、Route Model Binding、View Composer 等
     */
    public function boot(): void
    {
        // 例如:全站共用的 View 變數
        view()->share('appName', config('app.name'));
    }
}

register() vs boot()

這兩個方法的執行順序很重要:

Application 啟動

所有 Provider 的 register() 依序執行
  ↓ (此時所有服務都已註冊到 Container)
所有 Provider 的 boot() 依序執行

Application 準備好接收 Request
方法用途能做什麼不該做什麼
register()註冊綁定bind()singleton()使用其他服務(可能還沒被註冊)
boot()啟動設定用任何已註冊的服務不該再做綁定(太晚了)

類比: 想像一場派對。register() 是「大家把食物帶來放桌上」,boot() 是「所有食物都到了,開始擺盤和布置」。你不能在擺盤的時候才發現主菜還沒到。

建立自訂 Provider

php artisan make:provider PaymentServiceProvider
<?php

namespace App\Providers;

use App\Services\Payment\EcPayGateway;
use App\Services\Payment\PaymentGatewayInterface;
use App\Services\Payment\StripeGateway;
use Illuminate\Support\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $gateway = match (config('payment.default')) {
            'ecpay' => EcPayGateway::class,
            default => StripeGateway::class,
        };

        $this->app->singleton(PaymentGatewayInterface::class, $gateway);
    }
}

make:provider 建立時,Laravel 11 起會自動把新 Provider 寫入 bootstrap/providers.php,你不需要手動編輯,只要打開確認它已經在陣列裡即可:

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\PaymentServiceProvider::class, // make:provider 已自動加入
];

注意: 只有當你手動建立 Provider 檔案(沒用 make:provider)時,才需要自己把它加進這個陣列。用指令產生卻又手動再加一次,會造成重複註冊。

Middleware:Request 的過濾器

Middleware 是一個「洋蔥」——每一層 Middleware 包裹著下一層,Request 從外層進去、穿過 Controller、Response 再從內層出來。

Request → [Middleware A → [Middleware B → [Controller] → Middleware B] → Middleware A] → Response

Middleware 做什麼?

常見用途:

  • 認證檢查——沒登入?打回登入頁
  • CSRF 防護——POST 請求必須帶 token
  • Rate Limiting——限制 API 呼叫頻率
  • CORS 處理——設定跨域 headers
  • 日誌記錄——記錄每個 request 的資訊

建立自訂 Middleware

php artisan make:middleware LogRequest
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequest
{
    public function handle(Request $request, Closure $next): Response
    {
        // ① 在 Request 進入 Controller 之前做事
        $start = microtime(true);

        // ② 把 Request 傳給下一層(最終到 Controller)
        $response = $next($request);

        // ③ Response 出來之後做事
        $duration = round((microtime(true) - $start) * 1000, 2);

        Log::info('Request processed', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'status' => $response->getStatusCode(),
            'duration_ms' => $duration,
            'ip' => $request->ip(),
        ]);

        return $response;
    }
}

Middleware 的結構永遠是這樣:

  1. $next($request) 之前——處理 Request(前置作業)
  2. $next($request)——把 Request 傳遞下去
  3. $next($request) 之後——處理 Response(後置作業)

Express 開發者對照: 這就是 Express 的 (req, res, next) => { ... next(); ... }。概念完全一樣,只是 Laravel 把 $next($request) 的回傳值當作 Response。

註冊 Middleware

bootstrap/app.php 裡設定:

->withMiddleware(function (Middleware $middleware) {
    // 全域 Middleware——每個 Request 都會經過
    $middleware->append(LogRequest::class);

    // 路由群組 Middleware
    $middleware->appendToGroup('api', [
        // API 專用 Middleware
    ]);

    // 路由別名——在個別路由上使用
    $middleware->alias([
        'admin' => EnsureUserIsAdmin::class,
    ]);
})

在路由上使用 Middleware

// 用在單一路由
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])
    ->middleware('admin');

// 用在路由群組
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
    Route::get('/admin/users', [AdminController::class, 'users']);
});

Laravel 內建的重要 Middleware

Middleware用途對應的路由群組
EncryptCookies加密 Cookieweb
VerifyCsrfToken驗證 CSRF Tokenweb
StartSession啟動 Sessionweb
ThrottleRequestsAPI 頻率限制api
auth驗證使用者已登入自訂使用
guest驗證使用者未登入自訂使用

web 群組預設套用在 routes/web.php 的所有路由上,你不用手動加——這也是「魔法」之一,但現在你知道它是怎麼運作的了。

Facade vs 依賴注入:該用哪個?

Laravel 的 Facade 讓你可以用靜態語法呼叫服務:

// Facade 寫法
use Illuminate\Support\Facades\Cache;

$value = Cache::get('key');

// 依賴注入寫法
use Illuminate\Contracts\Cache\Repository;

class MyService
{
    public function __construct(
        private Repository $cache,
    ) {}

    public function getValue()
    {
        return $this->cache->get('key');
    }
}

兩種寫法在底層做的事情完全一樣——Facade 只是一個語法糖,它在背後去 Container 拿服務。

什麼時候用哪個?

場景建議原因
Controller依賴注入明確宣告依賴,好測試
Service 類別依賴注入同上
Blade 模板Facade 或 Helper@auth 就是 Facade 的語法糖
設定檔Helper (config(), env())沒有 class,無法注入
快速 prototypeFacade先求有,重構時再換
測試都行Facade 有 ::fake(),DI 有 mock

實務上的建議

如果你剛從 JS/Python 轉過來,先用 Facade 沒問題。它的學習曲線低,而且 Laravel 的 Facade 設計得很好——不會造成全域狀態的問題(每個 request 結束後都會清除)。

等你對框架更熟了,自然會開始在 Service 層用依賴注入。不用一開始就追求「完美的架構」,那只會讓你寫不出東西。

// 這樣寫完全沒問題,很多 Laravel 專案都這樣
class OrderController extends Controller
{
    public function index()
    {
        $orders = Order::where('user_id', Auth::id())
            ->latest()
            ->paginate(20);

        return view('orders.index', compact('orders'));
    }
}

// 想要更 testable?重構成 DI 也不難
class OrderController extends Controller
{
    public function __construct(
        private OrderService $orders,
    ) {}

    public function index(Request $request)
    {
        $orders = $this->orders->listForUser($request->user());

        return view('orders.index', compact('orders'));
    }
}

實作:為揪好買加入 Request 記錄 Middleware

理論講完了,讓我們在揪好買專案裡動手做。

Step 1:建立 Middleware

php artisan make:middleware LogRequest

編輯 app/Http/Middleware/LogRequest.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequest
{
    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);

        $response = $next($request);

        $duration = round((microtime(true) - $start) * 1000, 2);

        Log::channel('single')->info('HTTP Request', [
            'method' => $request->method(),
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'duration_ms' => $duration,
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
        ]);

        return $response;
    }
}

Step 2:全域註冊

編輯 bootstrap/app.php

use App\Http\Middleware\LogRequest;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(LogRequest::class);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })
    ->create();

Step 3:測試

php artisan serve
# 在另一個終端視窗
curl http://localhost:8000/

查看 storage/logs/laravel.log

[2026-06-15 10:30:00] local.INFO: HTTP Request {
    "method": "GET",
    "path": "/",
    "status": 200,
    "duration_ms": 42.35,
    "ip": "127.0.0.1",
    "user_agent": "curl/8.20.0"
}

每一個進來的 request 都被記錄了。這個 Middleware 在開發階段幫你觀察請求流量,到了 production 也能用來做效能監控。

Step 4:加入一個簡單的 Service Provider(bonus)

讓我們建立一個 Service Provider,在每個頁面的 View 裡共享揪好買的設定:

php artisan make:provider JiuHaoMaiServiceProvider
<?php

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class JiuHaoMaiServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // 註冊應用程式設定
        $this->app->singleton('jiuhaomai.config', function () {
            return [
                'name' => '揪好買 JiuHaoMai',
                'slogan' => '找好物、揪好友、一起買更划算',
                'version' => '0.1.0',
                'min_participants' => 3,
            ];
        });
    }

    public function boot(): void
    {
        // 讓所有 View 都能用 $jhm 變數
        View::share('jhm', app('jiuhaomai.config'));
    }
}

由於是用 make:provider 產生的,Laravel 已自動把它寫進 bootstrap/providers.php,打開確認一下即可(不需要再手動加):

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\JiuHaoMaiServiceProvider::class, // make:provider 已自動加入
];

現在你可以在任何 Blade 模板裡直接用 $jhm

<footer>
  <p>{{ $jhm['name'] }} v{{ $jhm['version'] }} — {{ $jhm['slogan'] }}</p>
</footer>

這就是 Service Provider 的威力:一處設定,全站生效

小結:理解魔法,掌握紀律

這一章的資訊量很大,讓我們整理一下核心概念:

Request Lifecycle:

  • 所有請求從 public/index.php 進入
  • bootstrap/app.php 設定路由、Middleware、例外處理
  • Request 穿過 Middleware 洋蔥 → Router → Controller → Response 反向穿回

Service Container:

  • 一個智慧物件工廠,自動解析依賴
  • bind() 註冊一對一對應,singleton() 保證只建立一次
  • 介面綁定讓你能輕鬆切換實作

依賴注入:

  • 在 constructor 或方法裡 type-hint,Container 自動注入
  • 具體類別不用設定,介面需要 bind() 綁定

Service Provider:

  • register() 註冊綁定,boot() 設定啟動邏輯
  • 所有 register() 跑完才開始跑 boot()

Middleware:

  • Request 的前置/後置過濾器
  • $next($request) 前後分別處理 request 和 response
  • bootstrap/app.php 設定全域、群組或路由別名

Facade vs DI:

  • 兩種方式底層一樣,都從 Container 取服務
  • 新手先用 Facade 沒問題,熟了再用 DI 提升可測試性

下一章我們進入 Laravel 最迷人的部分——Eloquent ORM。你會學到如何用優雅的 PHP 程式碼操作資料庫,完全不用寫 SQL。我們也會開始設計揪好買的資料表:使用者、團購、商品、參團紀錄。

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →