## **简介**
与其他 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`查看应用的所有路由。