## **簡介**
與其他 Web 應用框架(經典MVC設計模式)一樣,我們可以通過控制器來定義請求路由。控制器的主要職責就是獲取 HTTP 請求,進行一些簡單處理(如驗證)后將其傳遞給真正處理業務邏輯的職能部門,如 Service。
## **控制器入門**
我們可以通過[Artisan 命令](https://xueyuanjun.com/post/9562.html)快速創建一個控制器:
~~~
php artisan make:controller TaskController
~~~
該命令會在`app/Http/Controllers`目錄下創建一個新的名為`TaskController.php`的文件,并為該控制器添加一個簡單的`home()`動作方法:
~~~
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function home()
{
return 'Hello, World!';
}
}
~~~
然后我們來定義一個指向該控制器動作的路由,這樣,我們訪問`/task`就能看到「Hello, World!」了:
~~~
Route::get('/task', 'TaskController@home');
~~~
> 注:這里需要注意的是控制器`TaskController`的完整命名空間是`App\Http\Controllers\TaskController`,默認情況下,如果沒有指定完整的命名空間,那么路由文件`web.php`中所有控制器都位于`App\Http\Controllers`命名空間下,所以在定義控制器路由的時候可以省略這個命名空間前綴。
## **獲取用戶輸入**
實際開發中,很少有返回字符串的場景,最常見的就是在控制器中獲取用戶輸入并進行處理,下面我們來看兩個例子:
~~~
Route::get('task/create', 'TaskController@create');
Route::post('task', 'TaskController@store');
~~~
我們通過`create()`方法來渲染一個任務提交表單, 然后通過`store()`方法來存儲提交的任務數據。關于表單渲染我們放到后面去討論,現在我們直接跳到表單數據處理的`store()`方法:
~~~
public function store(Request $request)
{
$task = new Task();
$task->title = $request->input('title');
$task->description = $request->input('description');
$task->save();
return redirect('task'); // 重定向到 GET task 路由
}
~~~
這里我們通過`$request`對象來獲取用戶輸入,并將提交數據收集保存到`Task`模型類,然后將用戶重定向到顯示所有任務的頁面。此外還可以通過`Input`[門面](https://xueyuanjun.com/post/9536.html)來獲取用戶輸入:
~~~
$task->title = Input::get('title');
~~~
> 注:使用這種方式需要引入`Input`門面:`use Illuminate\Support\facades\Input`。其實門面僅僅是靜態代理,底層調用的還是`$request->input`方法,語法糖而已,建議大家還是用`$request`來獲取。
使用上述獲取方式可以獲取用戶提供的任何輸入數據,不管是查詢字符串還是表單字段。
>注:需要注意的是,如果發起 POST 請求提交 JSON 格式請求數據時,請求頭沒有設置為`application/json`的話,`$request->input()`方法將不會以 JSON 格式解析數據。這個時候,我們需要顯式地通過`$request->json()`來獲取 JSON 格式數據。
### **依賴注入**
正如前面介紹的`Input`門面一樣,Laravel 中的門面為 Laravel 代碼庫中的大部分類提供了簡單的接口調用,通過門面你可以輕松從當前獲取各種請求數據,比如用戶輸入、Session、Cookie 等,但不是所有的類都有對應的門面(當前的映射關系可以查看[門面列表](https://xueyuanjun.com/post/9536.html#toc_6)),對于這些類提供的方法我們可以通過更底層的依賴注入來調用,本質上來看,門面僅僅是一種設計模式,是對底層復雜 API 的上層靜態代理,主要目的在于簡化代碼調用,所以可以用門面調用的方法肯定可以用依賴注入來實現,而可以通過依賴注入實現的功能不一定可以通過門面來調用,除非你自定義實現這個門面。
> 在日常開發中,推薦大家使用依賴注入而非門面來獲取用戶輸入數據,除此之外,還可以通過`$request`對象獲取 Session、Cookie 數據。
提到依賴注入,就繞不開[服務容器](https://xueyuanjun.com/post/9534.html),關于服務容器后面我們會單獨講解,而現在你只需了解**服務容器是一個綁定多個接口與具體實現類的容器,而依賴注入則是在代碼編寫時以接口(或者叫類型提示)方式作為參數,不必傳入具體實現類,在代碼運行時會根據配置從服務容器獲取接口對應的實現類執行具體的接口方法,從而極大提高了代碼的可維護和可擴展性**。
在Laravel中所有的控制器方法(包括構造函數)都會在服務容器中進行解析,這意味著所有方法中傳入的可以被容器解析的接口/類型提示對應服務實現都會被自動注入,我們將這個過程稱之為依賴注入。我們上面演示的通過`$request`對象獲取用戶請求數據就是采用依賴注入的方式。
## **驗證請求字段**
作為一個靈活的框架,Laravel 提供了多種方式對表單請求進行驗證,你可以在控制器中通過`$this->validate()`方法驗證用戶請求,也可以通過單獨的表單驗證類定義驗證規則,再將其注入到相應的控制器方法,我們由簡入繁,先從`validate()`方法說起。
### **通過validate方法進行驗證**
通過`php artisan make:controller`生成的所有控制器默認都繼承自基類`App\Http\Controllers\Controller`,因此所有這些控制器都使用了`ValidatesRequests`Trait,進而可以使用該 Trait 中提供的`validate()`方法對請求字段進行驗證。簡單示例如下:
~~~
public function form(Request $request, $id)
{
$this->validate($request, [
'title' => 'bail|required|string|between:2,32',
'url' => 'sometimes|url|max:200',
'picture' => 'nullable|string'
], [
'title.required' => '標題字段不能為空',
'title.string' => '標題字段僅支持字符串',
'title.between' => '標題長度必須介于2-32之間',
'url.url' => 'URL格式不正確,請輸入有效的URL',
'url.max' => 'URL長度不能超過200',
]);
return response('表單驗證通過');
}
~~~
在該validate方法中,第一個參數是用戶請求實例,第二個參數是以數組形式定義的請求字段驗證規則,關于所有字段驗證規則及其說明你可以在[驗證規則文檔](https://xueyuanjun.com/post/9547.html#toc_17)中查看,這里我們定義`title`字段是必填的,格式是字符串,且長度介于2~32之間,并且通過`bail`指定任何一個驗證規則不通過則立即退出,不再做后續校驗;`url`字段通過`sometimes`指定為存在時驗證,如果填寫了的話格式必須是 URL,且長度不能超過 200,沒填寫的話則不驗證;最后圖片路徑允許為空。不同的驗證規則之間通過`|`分隔。
如果表單驗證通過,則繼續向下執行,如果表單驗證不通過,會拋出`ValidationException`異常,具體怎么處理這個異常要看請求方式,如果是 Ajax 請求的話,將會返回包含錯誤信息的 JSON 響應(錯誤碼為`422`),如果是正常的 POST 表單請求的話,會重定向到表單提交頁,并包含所有用戶輸入和錯誤信息,以便重新渲染已填寫表單并顯示錯誤信息。
### **通過Validator::make方法進行驗證**
如果你使用過 Laravel 自帶腳手架代碼實現登錄認證的話,你可能會留意到`RegisterController`中對用戶注冊請求進行驗證的時候,使用的是這樣的驗證代碼:
~~~
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
}
~~~
這其實是通過`Validator`門面實現的驗證,原理和上面通過`$this->validate()`一樣,這是形式不同,這樣做的一個好處是在非控制器類中也可以對字段進行驗證,因為`validate`畢竟是`ValidatesRequests`中的方法,沒有使用這個 Trait 的話就不能在代碼中這么調用。
除了第一個參數和最后要手動調動`validate()`方法外,其它參數都是一模一樣的,底層的處理方式也是一樣,所以其它地方的代碼都不需要做任何更改。如果是在控制器中進行請求驗證都可以,具體使用哪種方式,看你個人偏好了,如果是在其它地方比如服務類,可能`Validator::make`更合適些。
### **通過表單請求類實現請求字段驗證和錯誤提示**
對于大量請求字段,或者復雜的請求驗證,都寫到控制器方法中顯然會導致控制器的代碼變得臃腫,從單一職責原則來說需要將表單請求驗證拆分出去,然后通過類型提示的方式注入到控制器方法。
首先,我們需要通過 Artisan 命令來創建一個表單請求類`php artisan make:request SubmitFormRequest`,該命令會在`app/Http/Requests`目錄下新增一個`SubmitFormRequest.php`文件,并且初始化代碼如下:
~~~
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SubmitFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}
~~~
`authorize()`方法用于檢查用戶權限,如果返回`false`則表示用戶無權提交表單,會拋出權限異常中止請求,現在我們將其調整為返回`true`即可,然后我們在`rules()`方法中定義請求字段驗證規則,比如我們可以將上例中的字段驗證規則移到該方法中:
~~~
public function rules()
{
return [
'title' => 'bail|required|string|between:2,32',
'url' => 'sometimes|url|max:200',
'picture' => 'nullable|string'
];
}
~~~
然后你可能要問那自定義錯誤提示消息在哪里定義呢?既然是在類中,自然可以通過方法來實現,我們只需重寫父類的`messages()`方法即可:
~~~
public function messages()
{
return [
'title.required' => '標題字段不能為空',
'title.string' => '標題字段僅支持字符串',
'title.between' => '標題長度必須介于2-32之間',
'url.url' => 'URL格式不正確,請輸入有效的URL',
'url.max' => 'URL長度不能超過200',
];
}
~~~
這樣,我們就將控制器方法中的表單請求字段驗證邏輯全部遷移過來了。
接下來,問題又來了,這段表單請求字段驗證邏輯放在哪里執行呢?答案是**將其以類型提示的方式注入到請求路由對應的控制器方法即可**,在本例中,就是`RequestController`的`form`方法:
~~~
public function form(SubmitFormRequest $request)
{
return response('表單驗證通過');
}
~~~
Laravel底層在解析這個控制器方法的參數時,如果發現這個請求是一個表單請求類,則會自動執行其中定義的字段驗證規則對請求字段進行驗證,如果驗證成功則繼續執行控制器中的方法,否則會拋出驗證失敗異常,和我們上一篇在控制器方法中實現驗證邏輯的處理一樣。由于該表單請求類也是`Illuminate\Http\Request`的子類,所以后續獲取請求字段值也可以通過`$request`來獲取,將表單請求驗證和請求實例參數合二為一,非常方便。
### **通過匿名函數和驗證規則類自定義字段驗證規則**
Laravel從5.5版本開始支持自定義字段驗證規則,我們可以通過匿名函數和驗證規則類兩種方式來自定義驗證規則。
#### **通過匿名函數實現自定義規則**
我們先演示下如何在控制器方法中調用`$this->validate()`時自定義驗證規則,以`title`字段為例,除了系統提供的字段驗證規則之外,有時候我們還會禁止用戶輸入包含敏感詞的字段,這就需要自定義驗證規則了:
~~~
$this->validate($request, [
'title' => [
'bail',
'required',
'string',
'between:2,32',
function($attribute, $value, $fail) {
if (strpos($value, '敏感詞') !== false) {
return $fail('標題包含了系統禁用的敏感詞');
}
},
],
'url' => 'sometimes|url|max:200',
'picture' => 'nullable|string'
], [
'title.required' => '標題字段不能為空',
'title.string' => '標題字段僅支持字符串',
'title.between' => '標題長度必須介于2-32之間',
'url.url' => 'URL格式不正確,請輸入有效的URL',
'url.max' => 'URL長度不能超過200',
]);
~~~
為某個字段自定義驗證規則,原來通過`|`分隔多個規則的組合規則字符串已經實現不了了,需要將其改成數組的方式,然后將自定義規則以匿名函數的方式添加到數組最后,如上面的代碼所示,該匿名函數第一個參數是字段名,第二個參數是字段值,第三個參數是校驗失敗用于返回的函數名。如果檢查到輸入標題包含敏感詞,則認為驗證不通過,返回錯誤信息。
#### **通過創建規則類自定義驗證規則**
首先,我們需要通過 Artisan 命令來創建一個規則類來實現驗證規則的自定義`php artisan make:rule SensitiveWordRule`,該命令會在`app`目錄下創建一個`Rules`子目錄,并在這個子目錄下新增`SensitiveWordRule.php`文件,我們可以將驗證通過條件定義到該類的`passes`方法中:
~~~
public function passes($attribute, $value)
{
return strpos($value, '敏感詞') === false;
}
~~~
如果輸入值中包含敏感詞,則認為驗證失敗,然后在`message`方法中修改驗證失敗的錯誤消息,由于我們這個規則類是通用的,所以將字段名通過`:attribute`動態注入:
~~~
public function message()
{
return ':attribute輸入字段中包含敏感詞';
}
~~~
最后,將自定義驗證規則的匿名函數修改為實例化自定義規則類即可:
~~~
public function rules()
{
return [
'title' => [
'bail',
'required',
'string',
'between:2,32',
new SensitiveWordRule()
],
'url' => 'sometimes|url|max:200',
'picture' => 'nullable|string',
];
}
~~~
此外,我們還可以在表單請求類中通過重寫父類`attributes()`方法自定義字段名:
~~~
public function attributes()
{
return [
'title' => '標題',
'url' => 'URL',
'picture' => '圖片'
];
}
~~~
這樣,在驗證規則類`SensitiveWordRule`驗證失敗時返回錯誤提示時,就可以將`:attribute`替換為`標題`,而不是默認的`title`了。
## **資源控制器**
Laravel 為常見的 REST/CRUD 控制器(在 Laravel 中稱之為「資源控制器」)提供了一套約定規則,并為此提供了相應的 Artisan 生成器和路由定義方法,從而方便我們一次為所有控制器方法定義路由。
我們可以使用這個 Artisan 生成器來生成一個資源控制器(在之前命名后加上`--resource`選項):
~~~
php artisan make:controller PostController --resource
~~~
現在打開`app/Http/Controllers/PostController.php`文件,即可看到`PostController`代碼:
~~~
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
~~~
以上`PostController`控制器的每個方法都有對應的請求方式、路由命名、URL、方法名和業務邏輯約定。
| HTTP請求方式 | URL | 控制器方法 | 路由命名 | 業務邏輯描述 |
| --- | --- | --- | --- | --- |
| GET | post | index() | post.index | 展示所有文章 |
| GET | post/create | create() | post.create | 發布文章表單頁面 |
| POST | post | store() | post.store | 獲取表單提交數據并保存新文章 |
| GET | post/{post} | show() | post.show | 展示單個文章 |
| GET | post/{id}/edit | edit() | post.edit | 編輯文章表單頁面 |
| PUT | post/{id} | update() | post.update | 獲取編輯表單輸入并更新文章 |
| DELETE | post/{id} | destroy() | post.desc | 刪除單個文章 |
此外,Laravel 還為我們提供了一個`Route::resource`方法用于一次注冊包含上面列出的所有路由,并且遵循上述所有約定:
~~~
Route::resource('post', 'PostController');
~~~
你還可以通過 Artisan 命令`php artisan route:list`查看應用的所有路由。