控制器 - Controller
簡介
比起在路由檔案中使用閉包來定義所有的請求處理邏輯,你可能會想使用「Controller」類別來管理這個行為。Controller 可以將相關的請求處理邏輯放在單一類別內。舉例來說,UserController
類別可以處理所有有關使用者的連入請求,包含顯示、建立、更新與刪除使用者。預設情況下,Controller 存放在 app/Http/Controllers
目錄下。
撰寫 Controller
基礎 Controller
若要快速產生新的 Controller,可以執行 make:controller
Artisan 指令。預設情況下,專案中所有的 Controller 都保存在 app/Http/Controllers
目錄:
1php artisan make:controller UserController
1php artisan make:controller UserController
來看一個基本的 Controller 例子。一個 Controller 可以包含任意數量的 Public 方法,這些 Public 方法會用來回應連入的 HTTP Request:
1<?php23namespace App\Http\Controllers;45use App\Models\User;6use Illuminate\View\View;78class UserController extends Controller9{10 /**11 * Show the profile for a given user.12 */13 public function show(string $id): View14 {15 return view('user.profile', [16 'user' => User::findOrFail($id)17 ]);18 }19}
1<?php23namespace App\Http\Controllers;45use App\Models\User;6use Illuminate\View\View;78class UserController extends Controller9{10 /**11 * Show the profile for a given user.12 */13 public function show(string $id): View14 {15 return view('user.profile', [16 'user' => User::findOrFail($id)17 ]);18 }19}
寫好 Controller 類別與方法後,就可以像這樣定義一個 Route 至該 Controller 方法:
1use App\Http\Controllers\UserController;23Route::get('/user/{id}', [UserController::class, 'show']);
1use App\Http\Controllers\UserController;23Route::get('/user/{id}', [UserController::class, 'show']);
當有連入請求符合這個路由 URI 時,將叫用 App\Http\Controllers\UserController
類別的 show
方法,且 route 參數會被傳入這個方法內。
Controllers are not required to extend a base class. However, it is sometimes convenient to extend a base controller class that contains methods that should be shared across all of your controllers.
單一動作的 Controller
若某個 Controller 動作特別複雜,則可以將這個動作放到獨立的 Controller 類別。為此,可在該 Controller 內定義一個單一的 __invoke
方法:
1<?php23namespace App\Http\Controllers;45class ProvisionServer extends Controller6{7 /**8 * Provision a new web server.9 */10 public function __invoke()11 {12 // ...13 }14}
1<?php23namespace App\Http\Controllers;45class ProvisionServer extends Controller6{7 /**8 * Provision a new web server.9 */10 public function __invoke()11 {12 // ...13 }14}
當為單一動作的 Controller 註冊路由時,不需要指定 Controller 方法。只需要傳入該 Controller 的名稱給 Router 即可:
1use App\Http\Controllers\ProvisionServer;23Route::post('/server', ProvisionServer::class);
1use App\Http\Controllers\ProvisionServer;23Route::post('/server', ProvisionServer::class);
可以通過 make:controller
Artisan 指令的 --invokable
選項來建立可被叫用的 Controller:
1php artisan make:controller ProvisionServer --invokable
1php artisan make:controller ProvisionServer --invokable
Controller 的 Stub 可通過發佈 Stub 來自定。
Controller Middleware
可在路由檔案中指派 Middleware 給 Controller 的路由:
1Route::get('/profile', [UserController::class, 'show'])->middleware('auth');
1Route::get('/profile', [UserController::class, 'show'])->middleware('auth');
Or, you may find it convenient to specify middleware within your controller class. To do so, your controller should implement the HasMiddleware
interface, which dictates that the controller should have a static middleware
method. From this method, you may return an array of middleware that should be applied to the controller's actions:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Routing\Controllers\HasMiddleware;7use Illuminate\Routing\Controllers\Middleware;89class UserController extends Controller implements HasMiddleware10{11 /**12 * Get the middleware that should be assigned to the controller.13 */14 public static function middleware(): array15 {16 return [17 'auth',18 new Middleware('log', only: ['index']),19 new Middleware('subscribed', except: ['store']),20 ];21 }2223 // ...24}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Routing\Controllers\HasMiddleware;7use Illuminate\Routing\Controllers\Middleware;89class UserController extends Controller implements HasMiddleware10{11 /**12 * Get the middleware that should be assigned to the controller.13 */14 public static function middleware(): array15 {16 return [17 'auth',18 new Middleware('log', only: ['index']),19 new Middleware('subscribed', except: ['store']),20 ];21 }2223 // ...24}
You may also define controller middleware as closures, which provides a convenient way to define an inline middleware without writing an entire middleware class:
1use Closure;2use Illuminate\Http\Request;34/**5 * Get the middleware that should be assigned to the controller.6 */7public static function middleware(): array8{9 return [10 function (Request $request, Closure $next) {11 return $next($request);12 },13 ];14}
1use Closure;2use Illuminate\Http\Request;34/**5 * Get the middleware that should be assigned to the controller.6 */7public static function middleware(): array8{9 return [10 function (Request $request, Closure $next) {11 return $next($request);12 },13 ];14}
資源 Controller
若將專案內的各個 Eloquent Model 當作是「資源」,則我們通常會在專案中對各個資源進行同一系列的動作。舉例來說,假設專案中有個 Photo
Model 與 Movie
Model。則使用者應該可以建立 (Create)、檢視 (Read)、更新 (Update)、或刪除 (Delete) 這些資源。
由於這個常見的使用情況,Laravel 資源路由可將常見的建立 (Create)、讀取 (Read)、更新 (Update) 與刪除 (Delete) (即「CRUD」) 通過單行程式碼來指派路由。要開始建立資源路由,可使用 make:controller
Artisan 指令的 --resource
選項來快速建立處理這些動作的 Controller:
1php artisan make:controller PhotoController --resource
1php artisan make:controller PhotoController --resource
這個指令會在 app/Http/Controllers/PhotoController.php
下產生一個 Controller。該 Controller 會包含用於各個可用資源操作的方法。接著,可以註冊一個指向該 Controller 的資源路由:
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class);
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class);
這一個路由定義會建立多個路由來處理對該資源的數種動作。剛才產生的 Controller 已經預先有了用於這幾個動作的方法了。請記得,你可以隨時通過執行 route:list
Artisan 指令來快速檢視專案的路由。
也可以通過傳入陣列給 resources
方法來一次註冊多個資源 Controller:
1Route::resources([2 'photos' => PhotoController::class,3 'posts' => PostController::class,4]);
1Route::resources([2 'photos' => PhotoController::class,3 'posts' => PostController::class,4]);
Actions Handled by Resource Controllers
自訂找不到 Model 的行為
Typically, a 404 HTTP response will be generated if an implicitly bound resource model is not found. However, you may customize this behavior by calling the missing
method when defining your resource route. The missing
method accepts a closure that will be invoked if an implicitly bound model cannot be found for any of the resource's routes:
1use App\Http\Controllers\PhotoController;2use Illuminate\Http\Request;3use Illuminate\Support\Facades\Redirect;45Route::resource('photos', PhotoController::class)6 ->missing(function (Request $request) {7 return Redirect::route('photos.index');8 });
1use App\Http\Controllers\PhotoController;2use Illuminate\Http\Request;3use Illuminate\Support\Facades\Redirect;45Route::resource('photos', PhotoController::class)6 ->missing(function (Request $request) {7 return Redirect::route('photos.index');8 });
軟刪除的 Model
一般來說,隱式型別繫結不會取得被軟刪除的 Model,而會回傳 404 HTTP Response。不過,我們也可以在定義 Route 時呼叫 withTrashed
方法來讓 Laravel 取得這些已刪除的 Model:
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->withTrashed();
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->withTrashed();
在呼叫 withTrashed
時若不提供屬性,則可讓 show
、edit
、與 update
Resource Route 存取軟刪除的 Model。可以傳入一組陣列給 withTrashed
方法來指定只使用這些 Route 中的一部分:
1Route::resource('photos', PhotoController::class)->withTrashed(['show']);
1Route::resource('photos', PhotoController::class)->withTrashed(['show']);
Specifying the Resource Model
若使用了路由 Model 繫結,且想型別提示資源 Controller 的方法,可以在產生 Controller 時使用 --model
選項:
1php artisan make:controller PhotoController --model=Photo --resource
1php artisan make:controller PhotoController --model=Photo --resource
產生 Form Request
可以在產生資源 Controller 時提供 --requests
選項來告訴 Artisan 要產生用於 Controller 中 storage 與 update 方法的 Form Request 類別:
1php artisan make:controller PhotoController --model=Photo --resource --requests
1php artisan make:controller PhotoController --model=Photo --resource --requests
部分資源路由
宣告資源路由時,比起宣告全部的預設動作,也可以只宣告該 Controller 要處理的一部分動作:
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->only([4 'index', 'show'5]);67Route::resource('photos', PhotoController::class)->except([8 'create', 'store', 'update', 'destroy'9]);
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->only([4 'index', 'show'5]);67Route::resource('photos', PhotoController::class)->except([8 'create', 'store', 'update', 'destroy'9]);
API 資源路由
在會被 API 使用的資源路由時,我們通常會想排除用來顯示 HTML 樣板的路由,如 create
與 edit
。為了方便起見,可以使用 apiResource
方法來自動排除這兩個路由:
1use App\Http\Controllers\PhotoController;23Route::apiResource('photos', PhotoController::class);
1use App\Http\Controllers\PhotoController;23Route::apiResource('photos', PhotoController::class);
也可以通過傳入陣列給 apiResources
方法來一次註冊多個 API 資源 Controller:
1use App\Http\Controllers\PhotoController;2use App\Http\Controllers\PostController;34Route::apiResources([5 'photos' => PhotoController::class,6 'posts' => PostController::class,7]);
1use App\Http\Controllers\PhotoController;2use App\Http\Controllers\PostController;34Route::apiResources([5 'photos' => PhotoController::class,6 'posts' => PostController::class,7]);
若要快速建立不包含 create
或 edit
方法的 API 資源路由,請在執行 make:contorller
指令時使用 --api
開關:
1php artisan make:controller PhotoController --api
1php artisan make:controller PhotoController --api
巢狀資源
有時候我們會需要為巢狀資源定義路由。舉例來說,某個照片資源可能會有多個附加到該照片的留言。要巢狀嵌套資源 Controller,我們可以在路由定義上使用「點」標記法:
1use App\Http\Controllers\PhotoCommentController;23Route::resource('photos.comments', PhotoCommentController::class);
1use App\Http\Controllers\PhotoCommentController;23Route::resource('photos.comments', PhotoCommentController::class);
該路由會註冊一個巢狀資源,可使用像這樣的 URI 來存取:
1/photos/{photo}/comments/{comment}
1/photos/{photo}/comments/{comment}
限定範圍的巢狀資源
Laravel 的隱式 Model 繫結功能可自動限制巢狀繫結的範圍,讓要被解析的子 Model 可被限制在屬於其上層 Model。只要在定義巢狀資源時使用 scoped
方法,就可以開啟自動範圍限制,並告訴 Laravel 應使用子資源的哪個欄位來取得。更多有關此的資訊,請參考限制資源路由的範圍的說明文件。
淺層巢狀
通常,在 URI 中並不需要同時擁有上層 Model 與子 Model 的 ID,因為子 ID 已經是唯一的識別子了。若要在使用唯一如自動遞增的主鍵這樣的識別子來在 URI 區段中識別 Model,可使用「淺層巢狀 (Shallow Nesting)」:
1use App\Http\Controllers\CommentController;23Route::resource('photos.comments', CommentController::class)->shallow();
1use App\Http\Controllers\CommentController;23Route::resource('photos.comments', CommentController::class)->shallow();
這個路由定義會定義下列路由:
命名資源路由
預設情況下,所有的資源 Controller 動作都有對應的路由名稱。不過,可以通過將包含欲使用的路由名稱的陣列傳入 names
來複寫這些名稱:
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->names([4 'create' => 'photos.build'5]);
1use App\Http\Controllers\PhotoController;23Route::resource('photos', PhotoController::class)->names([4 'create' => 'photos.build'5]);
命名資源路由參數
預設情況下,Route::resource
會為依照「單數化 (Singularized)」的資源名稱來為資源路由建立路由參數。可以輕鬆地通過 parameters
方法來對個別資源複寫資源名稱。傳入 parameters
的陣列應為一個包含資源名稱與參數名稱的關聯式陣列:
1use App\Http\Controllers\AdminUserController;23Route::resource('users', AdminUserController::class)->parameters([4 'users' => 'admin_user'5]);
1use App\Http\Controllers\AdminUserController;23Route::resource('users', AdminUserController::class)->parameters([4 'users' => 'admin_user'5]);
上述範例會為資源的 show
路由產生下列 URI:
1/users/{admin_user}
1/users/{admin_user}
限制資源路由的範圍
Laravel 的限定範圍的隱式 Model 繫結功能可自動限制巢狀繫結的範圍,讓要被解析的子 Model 可被限制在屬於其上層 Model。只要在定義巢狀資源時使用 scoped
方法,就可以開啟自動範圍限制,並告訴 Laravel 應使用子資源的哪個欄位來取得:
1use App\Http\Controllers\PhotoCommentController;23Route::resource('photos.comments', PhotoCommentController::class)->scoped([4 'comment' => 'slug',5]);
1use App\Http\Controllers\PhotoCommentController;23Route::resource('photos.comments', PhotoCommentController::class)->scoped([4 'comment' => 'slug',5]);
該路由會註冊一個限定範圍的巢狀資源,可使用像這樣的 URI 來存取:
1/photos/{photo}/comments/{comment:slug}
1/photos/{photo}/comments/{comment:slug}
當使用自訂鍵值的隱式繫結作為巢狀路由參數時,Laravel 會自動以慣例推測其上層 Model 上的關聯名稱來將限制巢狀 Model 的查詢範圍。在這個例子中,Laravel 會假設 Photo
Model 有個名為 comments
的關聯 (即路由參數名稱的複數形),該關聯將用於取得 Comment
Model。
本地化資源 URI
By default, Route::resource
will create resource URIs using English verbs and plural rules. If you need to localize the create
and edit
action verbs, you may use the Route::resourceVerbs
method. This may be done at the beginning of the boot
method within your application's App\Providers\AppServiceProvider
:
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Route::resourceVerbs([7 'create' => 'crear',8 'edit' => 'editar',9 ]);10}
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Route::resourceVerbs([7 'create' => 'crear',8 'edit' => 'editar',9 ]);10}
Laravel 的複數化程式 (Pluralizer) 可以按照需求設定支援不同的語言。自訂好動詞與複數化語言後,如 Route::resource('publicacion', PublicacionController::class)
這樣的 Resource Route 就會產生下列 URI:
1/publicacion/crear23/publicacion/{publicaciones}/editar
1/publicacion/crear23/publicacion/{publicaciones}/editar
補充資源 Controller
若有需要為某個資源 Controller 增加除了預設資源路由以外的額外路由,則應在呼叫 Route::resource
方法前先定義這些路由。否則,又 resource
方法定義的路由可能會不可預期地取代所擴充的路由:
1use App\Http\Controller\PhotoController;23Route::get('/photos/popular', [PhotoController::class, 'popular']);4Route::resource('photos', PhotoController::class);
1use App\Http\Controller\PhotoController;23Route::get('/photos/popular', [PhotoController::class, 'popular']);4Route::resource('photos', PhotoController::class);
請記得要保持 Controller 的功能專一。若發現常常需要使用除了一般資源動作以外的方法,請考慮將 Controller 拆分成兩個、更小的 Controller。
單例 Resource Controller
有時候,專案中可能會有只有一個實體的資源。舉例來說,我們可以編輯或更新使用者的「個人檔案 (Profile)」,而每個使用者通常都不會有超過一個的「個人檔案」。類似的,一張圖片可能也只有一個「縮圖」。這些資源就叫做「單例資源」,意思是,這些資源只會有一個實體。在這些情況下,我們可以註冊一個「單例」的 Resource Controller:
1use App\Http\Controllers\ProfileController;2use Illuminate\Support\Facades\Route;34Route::singleton('profile', ProfileController::class);
1use App\Http\Controllers\ProfileController;2use Illuminate\Support\Facades\Route;34Route::singleton('profile', ProfileController::class);
上面的單例 Resource 定義會註冊下列 Route。就像這樣,單例 Resource 不會註冊「建立」Route,而該程式碼註冊的 Route 也不接受識別子 (Identifier),因為這些資源只會有一個實體:
1Route::singleton('photos.thumbnail', ThumbnailController::class);
1Route::singleton('photos.thumbnail', ThumbnailController::class);
In this example, the photos
resource would receive all of the standard resource routes; however, the thumbnail
resource would be a singleton resource with the following routes:
可建立的單例資源
有時候,我們會需要為某個單例資源定義建立與保存的 Route。這時,我們可以在註冊單例資源 Route 時呼叫 creatable
方法:
1Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();
1Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();
在此範例中,會註冊下列 Route。如下所示,在可被建立的單例資源中,也會一併建立 DELETE
Route:
1Route::singleton(...)->destroyable();
1Route::singleton(...)->destroyable();
API 的單例資源
apiSingleton
方法可用來註冊通過 API 操作的單例資源。因此,這些資源不需要 create
與 edit
Route:
1Route::apiSingleton('profile', ProfileController::class);
1Route::apiSingleton('profile', ProfileController::class);
檔案,API 的單例資源也可以被設為 creatable
,也就是可為該資源註冊 store
與 destroy
Route:
1Route::apiSingleton('photos.thumbnail', ProfileController::class)->creatable();
1Route::apiSingleton('photos.thumbnail', ProfileController::class)->creatable();
Dependency Injection and Controllers
建構函式注入
Laravel 的 Service Container 會被用來解析所有的 Laravel Controller。因此,可以在 Controller 的建構函式內型別提示所有 Controller 所需要的依賴。所宣告的依賴會被自動解析並插入到 Controller 實體上:
1<?php23namespace App\Http\Controllers;45use App\Repositories\UserRepository;67class UserController extends Controller8{9 /**10 * Create a new controller instance.11 */12 public function __construct(13 protected UserRepository $users,14 ) {}15}
1<?php23namespace App\Http\Controllers;45use App\Repositories\UserRepository;67class UserController extends Controller8{9 /**10 * Create a new controller instance.11 */12 public function __construct(13 protected UserRepository $users,14 ) {}15}
方法注入
除了注入到建構函式內,也可以在 Controller 的方法上型別提示依賴。常見的使用情況是將 Illuminate\Http\Request
實體注入到 Controller 方法內:
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\RedirectResponse;6use Illuminate\Http\Request;78class UserController extends Controller9{10 /**11 * Store a new user.12 */13 public function store(Request $request): RedirectResponse14 {15 $name = $request->name;1617 // Store the user...1819 return redirect('/users');20 }21}
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\RedirectResponse;6use Illuminate\Http\Request;78class UserController extends Controller9{10 /**11 * Store a new user.12 */13 public function store(Request $request): RedirectResponse14 {15 $name = $request->name;1617 // Store the user...1819 return redirect('/users');20 }21}
若 Controller 方法也預期會從路由參數取得輸入,則請將路由引數放在其他依賴之後。舉例來說,若路由是像這樣定義:
1use App\Http\Controllers\UserController;23Route::put('/user/{id}', [UserController::class, 'update']);
1use App\Http\Controllers\UserController;23Route::put('/user/{id}', [UserController::class, 'update']);
還是可以像這樣定義 Controller 方法來型別提示 Illuminate\Http\Request
並取得 id
參數:
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\RedirectResponse;6use Illuminate\Http\Request;78class UserController extends Controller9{10 /**11 * Update the given user.12 */13 public function update(Request $request, string $id): RedirectResponse14 {15 // Update the user...1617 return redirect('/users');18 }19}
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\RedirectResponse;6use Illuminate\Http\Request;78class UserController extends Controller9{10 /**11 * Update the given user.12 */13 public function update(Request $request, string $id): RedirectResponse14 {15 // Update the user...1617 return redirect('/users');18 }19}