Laravel 的魔法與紀律:Request Lifecycle、Service Container 與 Middleware
本篇是「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());
三件事,從上到下:
- 載入 Composer autoloader——讓所有 class 可以自動載入
- 建立 Application 實例——這就是 Service Container 本人
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 就像有個包工頭:你說「我要一棟三房兩廳」,他幫你搞定所有上下游。
實際好處:
- 解耦合——你的程式碼不需要知道「怎麼建立」依賴,只需要宣告「我需要什麼」
- 可測試——測試時可以替換假的依賴(mock),不用動到真正的資料庫或郵件服務
- 靈活性——想換 Logger 實作?改一處設定,全站生效
跨語言對照
| 概念 | Laravel | Spring (Java) | NestJS (Node.js) | Python |
|---|---|---|---|---|
| IoC Container | Service Container | ApplicationContext | Module + providers | 通常不內建 |
| 註冊服務 | bind() / singleton() | @Bean | @Injectable() | — |
| 自動注入 | Type-hint in constructor | @Autowired | Constructor 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 怎麼知道要給你什麼?它的邏輯很簡單:
- 看 constructor 的 type-hint
- 如果是具體類別(如
OrderService)→ 直接new出來(遞迴解析它的依賴) - 如果是介面(如
PaymentGatewayInterface)→ 查綁定表,找到對應的實作 - 如果找不到 → 丟出
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 的結構永遠是這樣:
$next($request)之前——處理 Request(前置作業)$next($request)——把 Request 傳遞下去$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 | 加密 Cookie | web |
VerifyCsrfToken | 驗證 CSRF Token | web |
StartSession | 啟動 Session | web |
ThrottleRequests | API 頻率限制 | 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,無法注入 |
| 快速 prototype | Facade | 先求有,重構時再換 |
| 測試 | 都行 | 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。我們也會開始設計揪好買的資料表:使用者、團購、商品、參團紀錄。