--- tags: laravel --- # Controller 控制器 ![](https://i.imgur.com/zuOncaT.jpg) ## 簡介 為了取代在路由檔案中以 Closure 形式定義所有的請求處理邏輯,你可能想要使用控制器類別來組織這些行為 控制器能將相關的請求處理邏輯統整成一個單獨的類別。比如 UserController 類別能處理所有關於使用者的請求,包含了顯示. 創建. 更新以及刪除使用者。預設情況下,控制器被存放在 app/Http/Controllers 資料夾中 ## 撰寫控制器 ### 基本控制器 下面是一個基礎控制器類別的範例。需注意該控制器繼承了 Laravel 的基礎控制器 App\Http\Controllers\Controller。該控制器類別提供了一些便利的方法,如 middleware(),可為控制器行為添加中介層: ``` <?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\User; class UserController extends Controller { /** * Show the profile for a given user. * * @param int $id * @return \Illuminate\View\View */ public function show($id) { return view('user.profile', [ 'user' => User::findOrFail($id) ]); } } ``` 你能夠定義一個路由指向到控制器的方法,像這樣: ``` //routes/web.php use App\Http\Controllers\UserController; Route::get('/user/{id}', [UserController::class, 'show']); ``` 當一個請求與指定路由的 URI 相符時, UserController中的 show() 將會執行,路由參數也將會被傳遞進去 >技巧: > >控制器並非必須繼承基礎類別,但如果控制器沒有繼承基礎類別,你將無法使用一些好用的功能,比如 middleware,validate,和 dispatch 等方法。結論:沒事別作死! ### 單一方法控制器 假如一個控制器的方法過於複雜,你也許會想讓整個控制器只放一個方法。 為此,你可以在控制器中放置一個 __invoke() : ``` //app\Http\Controllers\ProvisionServer.php <?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\User; class ProvisionServer extends Controller { /** * Provision a new web server. * * @param int $id * @return \Illuminate\Http\Response */ public function __invoke() { // ... } } ``` 當為只有一個方法的控制器去註冊路由時,不需要指名控制器的方法。相對的,你只需要傳入控制器的名稱到路由: ``` \\routes\web.php use App\Http\Controllers\ProvisionServer; Route::post('/server', ProvisionServer::class); ``` 你可以通過 Artisan 命令裡的 make:controller 命令中的 --invokable 選項來生成一個只有單個方法的控制器 `php artisan make:controller ProvisionServer --invokable` Controller stubs may be customized using stub publishing >技巧: > >可以使用 [stub 自定義](/0PhDYO0mTvasl23E-Udaiw) 來修改控制器模板 ## 控制器中介層 中介層可以在路由檔案中分配給控制器的路由: ``` //routes\web.php Route::get('profile', [UserController::class, 'show'])->middleware('auth'); ``` 不過,在控制器的建構子中指定中介層更方便。使用控制器建構子中的 middleware() ,可以輕鬆地將中介層分配給控制器。你甚至可以將中介層設定為只針對控制器中的某些方法有效: ``` //app\Http\Controllers\UserController.php class UserController extends Controller { /** * Instantiate a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); $this->middleware('log')->only('index'); $this->middleware('subscribed')->except('store'); } } ``` 同時,控制器還允許你直接使用一個 Closure 來註冊中介層。這為不定義整個中介層類別的情況下(通常是因為這個中介層只有這裡會用到)為單個控制器定義中介層提供了一種便捷的方法: ``` $this->middleware(function ($request, $next) { return $next($request); }); ``` ## 資源型控制器 假如你把每一個 Eloquent 模型視為你應用的資源,那麼它們普遍就像是資源一樣被執行相同的操作。例如,想像一下你的應用有 Photo 和 Movie 這兩個模型,那麼很可能使用者就能夠去新增.閱讀.變更或刪除這些資源 Laravel 的資源路由通過單行代碼即可將典型的「CURD (增刪改查)」路由分配給控制器。例如,你希望創建一個控制器來處理保存 「照片」 應用的所有 HTTP 請求。使用 Artisan 命令 make:controller 可以快速創建這樣一個控制器: 因為這種常見的需求, Laravel 的資源路由通過單行程式碼即可將典型的「CRUD (增查改刪)」等多個路由分配給控制器。我們可以使用 make:controller 命令並加上 --resource 選項來快速生成一個控制器,裡頭包含了處理 CRUD 的7個方法 `php artisan make:controller PhotoController --resource` 這個命令將生成 app/Http/Controllers/PhotoController.php,而這個控制器裡的每個方法都將對應一個資源操作。 下一步,你需要註冊一個資源路由來指定到這個控制器: ``` //routes\web.php use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class); ``` 這個單一的路由聲明創建了多個路由來處理資源上的各種行為。而剛生成的控制器也為每種行為保留了對應方法。記住,你總是可以透過執行 route:list 命令來瀏覽當前應用的所有路由規則 你也可以通過將陣列傳入到 resources() 的方式來一次性的註冊多個資源控制器: ``` //routes\web.php Route::resources([ 'photos' => PhotoController::class, 'posts' => PostController::class, ]); ``` #### 資源控制器操作處理 |請求動詞 |URI |方法名稱Action |路由名稱| |---|---|---|---| |GET |/photos |index |photos.index| |GET |/photos/create |create |photos.create| |POST |/photos |store |photos.store| |GET |/photos/{photo} |show |photos.show| |GET |/photos/{photo}/edit |edit |photos.edit| |PUT/PATCH |/photos/{photo} |update |photos.update| |DELETE |/photos/{photo} |destroy |photos.destroy| #### 指定模型資源 如果你使用了路由模型綁定,並且想在資源控制器的方法中使用型別提示,你可以在生成控制器的時候加入 --model 選項,如下例: `php artisan make:controller PhotoController --resource --model=Photo` ### 部分資源路由 當註冊資源路由時,你可以指定控制器只處理部分行為,而不是預設的所有行為: ``` //routes\web.php use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->only([ 'index', 'show' ]); Route::resource('photos', PhotoController::class)->except([ 'create', 'store', 'update', 'destroy' ]); ``` #### API 資源路由 當註冊用於 APIs 的資源路由時,通常需要排除顯示 HTML 畫面的路由(如 create 和 edit )。為方便起見,你可以使用 apiResource() 自動排除這兩個路由: ``` //routes\api.php use App\Http\Controllers\PhotoController; Route::apiResource('photos', PhotoController::class); ``` 和剛才一樣,你也可以傳遞一個陣列給 apiResources() 來同時註冊多個 API 資源控制器: ``` //routes\api.php use App\Http\Controllers\PhotoController; use App\Http\Controllers\PostController; Route::apiResources([ 'photos' => PhotoController::class, 'posts' => PostController::class, ]); ``` 要快速生成不包含 create 或 edit 方法的用於開發介面的資源控制器,請在執行 make:controller 命令以生成控制器時加入 --api 選項: `php artisan make:controller API/PhotoController --api` ### 多層資源 有時你可能需要定義一個多層的資源型路由。例如,每張照片資源可以被添加了多個評論。那麼可以在路由中使用點語法來註冊資源型控制器: ``` use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class); ``` 該路由會註冊一個多層資源,可使用如下 URI 訪問: `/photos/{photo}/comments/{comment}` #### 多層資源範圍限定 Laravel 的 隱式模型綁定 特性可以自動限定多層綁定的範圍,因此已解析的子模型會自動屬於父模型 ,比如剛才的例子所抓取的評論都會是該張照片的。定義多層資源路由時,使用 scoped() 可以開啟自動範圍限定,也可以指定 Laravel 應該要用哪個欄位去查詢子模型資源 #### 淺層多層 通常並不完全需要在 URI 中同時擁有父 ID 和子 ID ,因為子 ID 已經是唯一的辨識符。當使用辨識符(如自動遞增的主鍵)來指定 URI 中的模型時,可以選擇使用「淺層多層」的方式定義路由: ``` use App\Http\Controllers\CommentController; Route::resource('photos.comments', CommentController::class)->shallow(); ``` 上面的路由定義方式會定義以下路由: |請求動詞 |URI| 方法| 路由名稱| |---|---|---|---| |GET |/photos/{photo}/comments| index| photos.comments.index| |GET |/photos/{photo}/comments/create| create| photos.comments.create| |POST |/photos/{photo}/comments| store| photos.comments.store| |GET |/comments/{comment}| show| comments.show| |GET |/comments/{comment}/edit| edit| comments.edit| |PUT/PATCH |/comments/{comment}| update| comments.update| |DELETE |/comments/{comment}| destroy| comments.destroy| ### 命名資源路由 預設所有的資源控制器行為都有一個路由名稱,但你可以傳入 names 陣列來複寫這些名稱: ``` use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->names([ 'create' => 'photos.build' ]); ``` ### 命名資源路由參數 預設 Route::resource() 會根據資源名稱的「單數」形式去創建資源路由的路由參數。你可以在選項陣列中傳入 parameters 參數來輕鬆地覆寫每個資源。parameters 陣列應該是資源名稱和參數名稱的的鍵值對陣列: ``` use App\Http\Controllers\AdminUserController; Route::resource('users', AdminUserController::class)->parameters([ 'users' => 'admin_user' ]); ``` 上面這個例子將會將資源的 show 路由轉成以下的 URI : `/users/{admin_user}` ### 限定欄位的多層資源路由 Laravel的範圍隱式模型綁定功能能夠自動限縮多層綁定讓被解析出來的子模型必定屬於父模型。透過在定義多層資源時使用 scoped() ,就能啟動自動限定欄位並告知 Laravel 該用哪個欄位來查找子資源 ``` use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class)->scoped([ 'comment' => 'slug', ]); ``` 這個路由將會註冊一個限定欄位的多層資源路由,可透過以下的 URIs 來進行訪問 `/photos/{photo}/comments/{comment:slug}` 當使用一個自定義鍵的隱式綁定作為多層路由參數時,Laravel 會自動限定查詢範圍,按照預設的命名方式去父類別中查找關聯方法名稱,然後檢索到對應的多層模型。在這種情況下將假定 Photo 模型有一個叫 comments(路由參數名的複數)的關聯方法,通過這個方法可以查找到 Comment 模型 ### 資源 URIs 本地化 預設 Route::resource() 將會用英文常用動詞來創建資源 URI。如果希望能自定義 create 和 edit 行為的方法名稱,可以在 app/Providers/AppServiceProvider.php 的 boot() 中使用 Route::resourceVerbs() 來修改。但因為無法改成中文,對我們基本沒用 /** * Define your route model bindings, pattern filters, etc. * * @return void */ public function boot() { Route::resourceVerbs([ 'create' => 'crear', 'edit' => 'editar', ]); // ... } 一旦動詞完成自定義,當像這樣 Route::resource('fotos', 'PhotoController') 的資源路由被註冊時就會生成以下的 URIs: ``` /fotos/crear /fotos/{foto}/editar ``` ### 補充資源控制器 如果需要增加額外的路由到預設的資源路由組合之中,需要在呼叫 Route::resource() 前先定義它們;否則 resource() 定義的路由可能會優先於你定義的路由。 > 懶人包 > > 自定義的路由一律寫在 resource() 之前 ``` use App\Http\Controller\PhotoController; Route::get('/photos/popular', [PhotoController::class, 'popular']); Route::resource('photos', PhotoController::class); ``` > 技巧 > > 記得保持控制器的最小化。如果您需要典型的資源操作以外的方法,請考慮將控制器分割為兩個更小的控制器 ## 架構子依賴注入 Laravel服務容器被用於解析所有的 Laravel 控制器。因此你可以在控制器的建構子中使用型別提示需要的依賴。聲明的依賴將會被自動解析並注入到控制器實例中: ``` <?php namespace App\Http\Controllers; use App\Repositories\UserRepository; class UserController extends Controller { /** * The user repository instance. */ protected $users; /** * Create a new controller instance. * * @param \App\Repositories\UserRepository $users * @return void */ public function __construct(UserRepository $users) { $this->users = $users; } } ``` ### 方法注入 除了建構子注入以外,你也可以在控制器的方法中做型別提示依賴。最常見的用法便是注入 Illuminate\Http\Request 到控制器方法中: ``` <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class UserController extends Controller { /** * Store a new user. * * @param Request $request * @return Response */ public function store(Request $request) { $name = $request->name; // } } ``` 如果你的控制器方法要取得路由參數,請在依賴之後再列出路由參數。例如像這樣定義路由: ``` //routes\web.php use App\Http\Controllers\UserController; Route::put('/user/{id}', [UserController::class, 'update']); ``` 你就能夠像以下的寫法來用依賴注入解析 Illuminate\Http\Request 並取得路由參數: ``` <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class UserController extends Controller { /** * Update the given user. * * @param \Illuminate\Http\Request $request * @param string $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { // } } ```