# 控制器
>[success]為了替代在路由文件中以閉包的形式定義所有的請求處理邏輯,你也許想使用控制器類來組織這些行為。控制器能將相關的請求處理邏輯組成一個單獨的類,控制器被存放在`app/Http/Controllers`目錄下。
## 基礎控制器
### 定義控制器
下面是一個基礎控制器的例子。需要注意的是,該控制器繼承了一個 Laravel 內置的基礎控制器類。該基礎控制器類提供了一些便利的方法,比如`middleware`方法,該方法可以為控制器行為添加中間件。
~~~php
<?php
namespace App\Http\Controllers;
use App\User;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
/**
* 顯示給定用戶的概要文件
*
* @param int $id
* @return Response
*/
public function show($id)
{
return view('user.profile', ['user' => User::findOrFail($id)]);
}
}
~~~
你可以這樣定義一個指向控制器行為的路由:
~~~php
Route::get('user/{id}', 'UserController@show');
~~~
現在,當一個請求與指定的路由 URI 相匹配時,`UserController`控制器的`show`方法就會被執行。當然,路由參數也將被傳遞給該方法。
> {tip} 控制器并**不是強制要求**繼承基礎類。但是,如果控制器沒有繼承基礎類,你將無法使用一些便捷的功能,比如`middleware`,`validate`和`dispatch`方法。
### 控制器和命名空間
需要著重指出的是,在定義控制器路由時我們不需要指定完整的控制器命名空間。因為`RouteServiceProvider`會在一個包含命名空間的路由器組中加載路由文件,所以我們只需指定類名中`App\Http\Controllers`命名空間之后的部分就可以了。
如果你選擇將控制器放置在`App\Http\Controllers`目錄下更深層次的目錄中,則要使用相對于`App\Http\Controllers`作為根命名空間的指定類名。因此,如果完整的控制器類名為`App\Http\Controllers\Photos\AdminController`,你在路由中應當采用如下的形式注冊:
~~~php
Route::get('foo', 'Photos\AdminController@method');
~~~
### 單個行為控制器
如果你想定義一個只處理單個行為的控制器,你可以在這個控制器中放置一個`__invoke`方法:
~~~php
<?php
namespace App\Http\Controllers;
use App\User;
use App\Http\Controllers\Controller;
class ShowProfile extends Controller
{
/**
* 展示給定用戶的資料
*
* @param int $id
* @return Response
*/
public function __invoke($id)
{
return view('user.profile', ['user' => User::findOrFail($id)]);
}
}
~~~
注冊單個行為控制器的路由時,無需指明方法:
~~~php
Route::get('user/{id}', 'ShowProfile');
~~~
你可以通過 Artisan 命令工具里的`make:controller`命令中的`--invokable`選項來生成一個可調用的控制器:
~~~php
php artisan make:controller ShowProfile --invokable
~~~
## 控制器中間件
[Middleware](https://laravel-china.org/docs/laravel/5.7/middleware)可以在路由文件中被分配給控制器路由:
~~~php
Route::get('profile', 'UserController@show')->middleware('auth');
~~~
但是,在控制器的構造方法中指定中間件會更方便。使用控制器構造函數中的`middleware`方法, 你可以很容易地將中間件分配給控制器的行為。你甚至可以約束中間件只對控制器類中的某些特定方法生效:
~~~php
class UserController extends Controller
{
/**
* 實例化一個控制器實例
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('log')->only('index');
$this->middleware('subscribed')->except('store');
}
}
~~~
還能使用閉包來為控制器注冊中間件。閉包的方便之處在于,你無需特地創建一個中間件類來為某一個特殊的控制器注冊中間件:
~~~php
$this->middleware(function ($request, $next) {
// ...
return $next($request);
});
~~~
> {tip} 你可以將中間件分配給控制器的部分行為上;然而這樣可能意味著你的控制器正在變得很大。這里建議你將控制器拆分成多個更小的控制器。
## 資源控制器
Laravel 資源路由將典型的「CRUD」路由分配給具有單行代碼的控制器。比如,你希望創建一個控制器來處理應用保存的「照片」的所有 HTTP 請求。使用 Artisan 命令`make:controller`,我們可以快速創建這樣一個控制器:
~~~php
php artisan make:controller PhotoController --resource
~~~
這個命令會生成一個控制器`app/Http/Controllers/PhotoController.php`。 其中包含了每個可用資源操作的方法。
接下來,你可以給控制器注冊一個資源路由:
~~~php
Route::resource('photos', 'PhotoController');
~~~
這個單一路由聲明創建多個路由來處理資源上的各種行為。生成的控制器為每個行為保留了方法,包括了關于處理 HTTP 動詞和 URIs 的聲明注釋。
你可以通過將數組傳參到`resources`方法中的方式來一次性的創建多個資源控制器:
~~~php
Route::resources([
'photos' => 'PhotoController',
'posts' => 'PostController'
]);
~~~
#### 資源控制器操作處理
| 動作 | URI | 行為 | 路由名稱 |
| --- | --- | --- | --- |
| 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
php artisan make:controller PhotoController --resource --model=Photo
~~~
#### 偽造表單方法
因為 HTML 表單不能生成`PUT`,`PATCH`和`DELETE`請求,所以你需要添加一個隱藏的`_method`字段來偽造這些 HTTP 動作。 這個Blade 指令 @method 可以為你創建這個字段:
~~~php
<form action="/foo/bar" method="POST">
@method('PUT')
</form>
~~~
### 部分資源路由
聲明資源路由時,你可以指定控制器應該處理的部分行為,而不是所有默認的行為:
~~~php
Route::resource('photos', 'PhotoController')->only([
'index', 'show'
]);
Route::resource('photos', 'PhotoController')->except([
'create', 'store', 'update', 'destroy'
]);
~~~
#### API 資源路由
當聲明用于 APIs 的資源路由時,通常需要排除顯示 HTML 模板的路由, 如`create`和`edit`。 為了方便起見,你可以使用`apiResource`方法自動排除這兩個路由:
~~~php
Route::apiResource('photos', 'PhotoController');
~~~
你可以通過傳遞一個數組給`apiResources`方法的方式來一次性注冊多個API資源控制器:
~~~php
Route::apiResources([
'photos' => 'PhotoController',
'posts' => 'PostController'
]);
~~~
為了快速生成一個不包含`create`和`edit`方法的 API 資源控制器,可以在執行`make:controller`命令時加上`--api`選項:
~~~php
php artisan make:controller API/PhotoController --api
~~~
### 命名資源路由
默認情況下,所有的資源控制器行為都有一個路由名稱。 但是,你可以傳入一個`names`數組來覆蓋這些名稱:
~~~php
Route::resource('photos', 'PhotoController')->names([
'create' => 'photos.build'
]);
~~~
### 命名資源路由參數
默認情況下,`Route::resource`會根據資源名稱的「單數」形式創建資源路由的路由參數。 你可以在選項數組中傳入 parameters參數來輕松地覆蓋每個資源。 parameters 數組應當是一個資源名稱和參數名稱的關聯數組:
~~~php
Route::resource('users', 'AdminUserController')->parameters([
'users' => 'admin_user'
]);
~~~
上例將會為資源的`show`路由生成如下的 URI :
~~~php
/users/{admin_user}
~~~
### 本地化資源 URIs
默認情況下,`Route::resource`將會使用英文動詞來創建資源 URI。如果你需要本地化`create`和`edit`行為動作名,你可以在`AppServiceProvider`的`boot`方法中使用`Route::resourceVerbs`方法實現 :
~~~php
use Illuminate\Support\Facades\Route;
/**
* 初始化任何應用服務
*
* @return void
*/
public function boot()
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
}
~~~
一旦動作被自定義后,像`Route::resource('fotos', 'PhotoController')`這樣注冊的資源路由將會產生如下的 URI:
~~~php
/fotos/crear
/fotos/{foto}/editar
~~~
### 補充資源控制器
如果你需要為資源控制器添加默認路由之外的額外路由,你應該在調用`Route::resource`之前定義這些路由;否則, 由`resource`方法定義的路由可能會無意中優先于你補充的路由 :
~~~php
Route::get('photos/popular', 'PhotoController@method');
Route::resource('photos', 'PhotoController');
~~~
> {tip} 記住保持控制器的專一性。如果你發現自己經常需要典型資源操作之外的方法,請考慮將控制器拆分為兩個較小的控制器。
## 依賴注入 & 控制器
#### 構造函數注入
Laravel使用[服務容器](https://laravel-china.org/docs/laravel/5.7/container)解析所有的控制器。因此,你可以在控制器的構造函數中使用類型提示可能需要的依賴項。依賴聲明會被自動解析并注入到控制器實例:
~~~php
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
/**
* 用戶 repository 實例
*/
protected $users;
/**
* 創建一個新的控制器實例
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
~~~
當然,你可以輸入任何的[Laravel 契約](https://laravel-china.org/docs/laravel/5.7/contracts)類型提示。 只要容器可以解析它。根據你的應用,注入你的類型提示到控制器會提供更好可測試性。
#### 方法注入
除了構造函數注入, 你還可以在控制器方法中輸入類型來提示依賴項。 方法注入最常見的用例是在控制器方法中注入`Illuminate\Http\Request`的實例 :
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 保存新用戶
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$name = $request->name;
//
}
}
~~~
如果你的控制器還需要獲取路由參數中的輸入,把路由參數放在這些依賴項的后面。例如,你的路由定義像這樣:
~~~php
Route::put('user/{id}', 'UserController@update');
~~~
你仍然可以輸入`Illuminate\Http\Request`類型提示,并通過在你的控制器方法中使用下面的定義來訪問`id`參數::
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 更新給定的用戶
*
* @param Request $request
* @param string $id
* @return Response
*/
public function update(Request $request, $id)
{
//
}
}
~~~
## 路由緩存
> {note} 基于閉包的路由無法被緩存。要使用路由緩存,你需要將任何閉包路由轉換成控制器路由。
如果你的應用只使用了基于控制器的路由,那么你應該利用路由緩存。 使用路由緩存將極大地減少注冊所有應用路由所需的時間。某些情況下,路由注冊的速度甚至會快100倍。要生成路由緩存, 只需要執行 Artisan 命令`route:cache`:
~~~php
php artisan route:cache
~~~
運行這個命令之后,每次請求的時候都將會加載緩存的路由文件。記住,如果你添加了任何一個新的路由,你將需要重新生成新的路由緩存。 因此,你應該只在項目進行部署時運行`route:cache`命令。
你可以使用`route:clear`命令清除路由緩存:
~~~php
php artisan route:clear
~~~