在請(qǐng)求過(guò)程中,控制器往往是我們?cè)谧鰳I(yè)務(wù)開(kāi)發(fā)時(shí)繞不過(guò)的一環(huán)。從 MVC 理論的成熟到現(xiàn)代化的開(kāi)發(fā)過(guò)程中,控制器一直扮演著重要的角色。可以說(shuō),我們可以不要前端(只做接口),可以不要模型(直接讀取數(shù)據(jù)),但控制器卻是必不可少的。當(dāng)然,在正式的 MVC 模型中,視圖是可以直接和模型交互的,由此,也引申出了 MVP 模型,其中的這個(gè) P 就是強(qiáng)化控制器的作用,讓模型和視圖解耦。其實(shí)我們大部分正規(guī)的開(kāi)發(fā),都是基于這個(gè) MVP 的,很少會(huì)直接讓視圖和模型去交互。
所以說(shuō),只要是遵循 MVC 模式的框架,控制器都是最核心的部分。在傳統(tǒng)的框架中,我們的控制器往往也充當(dāng)路由的功能,比如 TP3.2 系列,定義控制器名稱就是我們要請(qǐng)求的 URL 路徑名稱。之前在講路由的時(shí)候也說(shuō)過(guò)這個(gè)問(wèn)題,但是在 Laravel 中,實(shí)現(xiàn)了路由和控制器的解耦,所以我們的控制器是可以隨意定義并且命名的,直接通過(guò)路由來(lái)進(jìn)行綁定。
我們可以通過(guò)命令行來(lái)創(chuàng)建一個(gè)控制器,當(dāng)然,您也可以直接自己創(chuàng)建一個(gè)控制器類。
php artisan make:controller TestController
如果是自己創(chuàng)建的控制器類,需要繼承 app/Http/Controllers/Controllers 這個(gè)基類。如果不繼承這個(gè)基類,也就無(wú)法使用框架的能力,比如說(shuō)中間件之類的功能。我們這里測(cè)試的是直接通過(guò)命令行創(chuàng)建的,看看它的代碼。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController extends Controller
{
//
}
非常簡(jiǎn)單,也沒(méi)有什么別的特別的地方,接下來(lái),我們就可以在這個(gè)控制器中寫(xiě)需要的控制方法了。
public function test(){
}
最簡(jiǎn)單的一個(gè)控制器就這樣實(shí)現(xiàn)了,接下來(lái)要如何訪問(wèn)它呢?當(dāng)然就是去配下路由就好啦。
Route::get('test/test', 'App\Http\Controllers\TestController@test');
// http://laravel8/test/test
這時(shí)訪問(wèn)的結(jié)果是一個(gè)空白的頁(yè)面,因?yàn)樵谶@個(gè)控制器方法中我們什么都沒(méi)有做,也沒(méi)有任何的返回,所以頁(yè)面上沒(méi)有任何的顯示。但其實(shí),Laravel 中還是為我們做了一些事情。比如返回 HTTP 的頭信息,包括響應(yīng)狀態(tài)碼、基礎(chǔ)的頭信息之類的內(nèi)容。
另外,我們還可以定義一個(gè)單行為控制器,這是什么意思呢?其實(shí)就是一個(gè)控制器里面只有一個(gè)方法,這樣的一個(gè)控制器就不需要在路由中指定控制方法。
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class Test2Controller extends Controller
{
//
public function __invoke()
{
echo 'single action controller';
}
}
注意到單行為控制器中使用的這個(gè) __invoke() 魔術(shù)方法了嗎?不記得的小伙伴可以回到我們最早的文章中 PHP的那些魔術(shù)方法(二)https://mp.weixin.qq.com/s/8WgQ3eVYKjGaEd2CwnB0Ww 復(fù)習(xí)一下,在 Laravel 中,我們會(huì)用到很多之前學(xué)習(xí)過(guò)的基礎(chǔ)知識(shí)。所以說(shuō),框架的學(xué)習(xí)其實(shí)就是一次對(duì)于基礎(chǔ)知識(shí)的全面鞏固復(fù)習(xí),同時(shí)也需要我們對(duì)于 PHP 的基礎(chǔ)知識(shí)有牢固的掌握。
接下來(lái)就是路由和我們的測(cè)試了。
Route::get('test/test2', 'App\Http\Controllers\Test2Controller');
// http://laravel8/test/test2
// single action controller
對(duì)于請(qǐng)求參數(shù)的接收來(lái)說(shuō),在控制器中和在路由的回調(diào)函數(shù)中接收參數(shù)沒(méi)有什么區(qū)別。都可以通過(guò)依賴注入的方式獲取到指定的參數(shù)。
// 控制器
public function test2(Request $request, $id){
var_dump($request === \request()); // bool(true)
return 'test2: ' . $id . ', ' . $request->input('name', '') . ', ' . \request()->input('sex', '');
}
// 路由
Route::get('test/test2/{id}', 'App\Http\Controllers\TestController@test2');
// http://laravel8/test/test2/2?name=Bob&sex=male
在這里,我們使用了兩種接收 Request 的方式。一個(gè)是使用依賴注入的 request 對(duì)象,一個(gè)是使用 request() 方法返回的 Request 對(duì)象。兩種方式在本質(zhì)上沒(méi)有什么區(qū)別,在代碼中我們也打印了這兩種方式的對(duì)象是否是全等的。只不過(guò)一個(gè)是通過(guò)依賴注入到當(dāng)前方法的參數(shù)中,而另一個(gè) request() 方法則是通過(guò)全局的服務(wù)容器來(lái)獲取 Request 對(duì)象的。關(guān)于依賴注入和服務(wù)容器的內(nèi)容都會(huì)在后面核心架構(gòu)相關(guān)的文章中學(xué)習(xí)到。
在上篇路由的文章中就講過(guò),我們可以定制一個(gè)資源型的路由,對(duì)應(yīng)的就是一個(gè)資源型的控制器,這倆貨是相輔相成的。那么什么是資源型呢?其實(shí)就是標(biāo)準(zhǔn)的 RESTful 類型的一套請(qǐng)求鏈接。對(duì)于 REST 有疑問(wèn)的同學(xué)可以自行查閱相關(guān)的文檔,在這里就不多說(shuō)了,畢竟我們的主旨還是在于 Laravel 框架如何實(shí)現(xiàn)這些功能。
我們可以直接使用命令:
php artisan make:controller ResourceTestController --resource
創(chuàng)建一個(gè)資源型的控制器,直接來(lái)看看代碼,這個(gè)控制器已經(jīng)為我們準(zhǔn)備好了一系列的方法。
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ResourceTestController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
return 'get列表';
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
return 'post添加數(shù)據(jù)-顯示表單';
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
return 'post保存數(shù)據(jù)';
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
return 'get單條數(shù)據(jù)';
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
return 'get修改數(shù)據(jù)-顯示表單';
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
return 'put/patch修改數(shù)據(jù)';
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
return 'delete刪除數(shù)據(jù)';
}
}
當(dāng)定義完成資源型控制器之后,就可以在路由上非常方便地配置這個(gè)資源的路由,一行就搞定。
Route::resource('test/resource', 'App\Http\Controllers\ResourceTestController');
剩下的呢?Laravel 框架會(huì)自動(dòng)幫我們配置以下這些路由,大家只要按照規(guī)則訪問(wèn)就好了。
請(qǐng)求方式 | 鏈接 | 說(shuō)明 |
---|---|---|
GET | /test/resource | 索引/列表 |
GET | /test/resource/create | 創(chuàng)建(顯示表單) |
POST | /test/resource/store | 保存你創(chuàng)建的數(shù)據(jù) |
GET | /test/resource/{id} | 顯示對(duì)應(yīng)id的內(nèi)容 |
GET | /test/resource/{id}/edit | 編輯(顯示表單) |
PUT/PATCH | /test/resource/{id} | 保存你編輯的數(shù)據(jù) |
DELETE | /test/resource/{id} | 刪除 |
是不是感覺(jué)很高大上,確實(shí)如此,而且這一套路由也是非常符合 RESTFul 規(guī)范的,并且最主要的是,這一套路由不需要我們?cè)偈謩?dòng)去寫(xiě)了,它直接就幫我們定義好了。在測(cè)試的時(shí)候直接訪問(wèn)它們就可以了。
對(duì)于路由到控制器的調(diào)用,還記得上篇文章中學(xué)習(xí)過(guò)的 laravel/framework/src/Illuminate/Routing/Route.php 這個(gè)文件中的 run() 方法嗎?如果我們定義的路由是指定的控制器,那么它就會(huì)走到 runController() 。在這個(gè) runController() 方法中,會(huì)指定分發(fā)到的控制器,其實(shí)也是從一個(gè)控制器的集合中查找指定的控制器信息。
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
getController() 和 getControllerMethod() 都是獲取的當(dāng)前文件中的 action 里面的 uses 字段里面的內(nèi)容,它保存的就是我們?cè)诼酚芍刑顚?xiě)的控制器信息。
$this->action->uses = "App\Http\Controllers\ResourceTestController@index";
在我們實(shí)例化所有路由時(shí),都會(huì)創(chuàng)建一個(gè) Route 對(duì)象。傳遞過(guò)來(lái)的數(shù)據(jù)就是我們?cè)诼酚晌募卸x的數(shù)據(jù),也就是調(diào) get()/post() 這些方法的時(shí)候添加的數(shù)據(jù)。而第二個(gè)參數(shù),也就是我們指定的回調(diào)或者控制器參數(shù)就會(huì)充當(dāng) action 參數(shù),交給 Route.php 中的 parseAction() 方法進(jìn)行處理,處理之后的結(jié)果就會(huì)保存在當(dāng)前這個(gè) Route 對(duì)象的 action 屬性里面。
整體來(lái)說(shuō),控制器的調(diào)用和回調(diào)路由的調(diào)用本質(zhì)上是沒(méi)有什么區(qū)別的。
在日常的業(yè)務(wù)開(kāi)發(fā)中,出于安全以及數(shù)據(jù)格式驗(yàn)證的考慮,我們通常會(huì)對(duì)接收到的參數(shù)進(jìn)行驗(yàn)證過(guò)濾,一般情況下,都是通過(guò)一個(gè)個(gè)的 if...else 來(lái)進(jìn)行這項(xiàng)工作。既然說(shuō)到這里了,那么在 Laravel 框架中,其實(shí)也是有對(duì)應(yīng)的表單驗(yàn)證的功能的,可以方便地讓我們進(jìn)行表單參數(shù)的驗(yàn)證。
首先我們需要定義一個(gè)頁(yè)面,這個(gè)頁(yè)面用于提交表單,只需要簡(jiǎn)單的定義一個(gè)模板頁(yè)就可以。
// ValidateController
public function create(){
return view("validate.create");
}
// validate/create.blade.php
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<h2>表單驗(yàn)證</h2>
<form method="post" action="http://laravel8/validate/store">
<label>標(biāo)題</label><input name="title"/><br/>
<label>作者</label><input name="author"/><br/>
<label>年齡</label><input name="age"/><br/>
<label>內(nèi)容</label><input name="body"/><br/>
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<button type="submit">提交</button>
</form>
// route/web.php
Route::get('validate/create', 'App\Http\Controllers\ValidateController@create');
這個(gè)就相當(dāng)于是一個(gè)要提交數(shù)據(jù)的靜態(tài)表單頁(yè)面,我們沒(méi)有做別的任何操作。其中在模板文件中,csrf_token() 這個(gè)東西是用于 CSRF 攻擊防御的,這個(gè)在后面如果學(xué)習(xí)到了相關(guān)的內(nèi)容再說(shuō),大家也可以自行查閱一下相關(guān)的資料。如果沒(méi)有這個(gè) _token 的話,那么表單提交之后就會(huì)報(bào) 419 的錯(cuò)誤。
繼續(xù)寫(xiě)我們的這個(gè) store 接收頁(yè)面。來(lái)看看我們?nèi)绾悟?yàn)證這個(gè)表單里面提交的數(shù)據(jù)信息。
// ValidateController
public function store(Request $request){
$validatedData = $request->validate([
'title'=>"required|max:20",
'author'=>['required','min:2', 'max:20'],
'age'=>"numeric",
'body'=>"required"
]);
}
// route/web.php
Route::post('validate/store', 'App\Http\Controllers\ValidateController@store');
接下來(lái)就是去測(cè)試一下,在表單頁(yè)面,我們什么都不填,直接提交,就可以看到頁(yè)面上輸出了如下的錯(cuò)誤提示信息。
這個(gè)錯(cuò)誤信息正是在模板中的
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
這段代碼輸出的。而驗(yàn)證的規(guī)則,則是在 request 的 validate() 方法中配置的這些。從英文可以看出,我們讓 title 這個(gè)字段 required(必填)、max:20(最大不超過(guò)20個(gè)),讓 age 這個(gè)字段的內(nèi)容 numeric(只能是數(shù)字)。當(dāng)然,還有很多可配置的內(nèi)容,在這里就不一一列舉了,大家可以自己查閱相關(guān)的文檔,畢竟這些東西都是文檔中現(xiàn)成的,學(xué)習(xí)這些配置參數(shù)的使用也不是我們這個(gè)系列文章的重點(diǎn)。
從這段功能的測(cè)試代碼中,我們可以看出幾個(gè)問(wèn)題。其一,這個(gè)驗(yàn)證是直接通過(guò)請(qǐng)求對(duì)象實(shí)現(xiàn)的,也就是這個(gè) Request 對(duì)象中的方法,而且我們?cè)诳刂破髦袥](méi)有返回 Response ,也就是說(shuō),這一切框架都自動(dòng)為我們處理了。其二,錯(cuò)誤信息會(huì)直接傳到模板的一個(gè) $errors 變量中,這個(gè)也不是我們控制的,也是框架自動(dòng)處理的,這個(gè)地方也是我們平常在寫(xiě)業(yè)務(wù)代碼的時(shí)候需要注意的,因?yàn)檫@個(gè)變量名是寫(xiě)死在框架內(nèi)部的,不能修改的。其三,沒(méi)有地方設(shè)置錯(cuò)誤信息的內(nèi)容,比如說(shuō)我們要顯示中文的錯(cuò)誤信息。
太智能太自動(dòng)的東西有好處,但也有很多的限制,比如這個(gè)第三點(diǎn),如果需要顯示中文的錯(cuò)誤信息的話,我們需要去下載或者自己配置一個(gè) resource/lang 下的語(yǔ)言包,并且修改框架配置中的 lang 為對(duì)應(yīng)的語(yǔ)言包。不過(guò),我們有別的辦法來(lái)解決,那就是我們自己配置,手動(dòng)驗(yàn)證。
說(shuō)實(shí)話,上面的自動(dòng)表單驗(yàn)證平常還真沒(méi)用過(guò)。平常用得最多的反而是這個(gè)自定義的手動(dòng)驗(yàn)證,說(shuō)是手動(dòng)驗(yàn)證,其實(shí)大部分也是已經(jīng)框架提供好的內(nèi)容,我們只需要簡(jiǎn)單的配置就可以了。
public function store2(Request $request){
$validator = Validator::make($request->all(), [
'title'=>"required|max:20",
'author'=>['required','min:2', 'max:20'],
'age'=>"numeric",
'body'=>"required"
], [
'title.required'=>'請(qǐng)?zhí)顚?xiě)標(biāo)題',
'title.max'=>'標(biāo)題最大不超過(guò)20個(gè)字符',
'author.required'=>'請(qǐng)?zhí)顚?xiě)作者',
'author.min'=>'作者最少填寫(xiě)2個(gè)字符',
'author.max'=>'作者最大不超過(guò)20個(gè)字符',
'age.numeric'=>'年齡必須是數(shù)字',
'body.required'=>'內(nèi)容必填'
]);
if($validator->fails()){
return redirect('validate/create')
->withErrors($validator)
->withInput();
}
}
在這個(gè)控制器中,我們使用的是 Validator 這個(gè)門(mén)面類 make() 出來(lái)的一個(gè)驗(yàn)證器。它的第一個(gè)參數(shù)我們傳遞的是所有的請(qǐng)求數(shù)據(jù),當(dāng)然,也可以自己傳遞一個(gè)數(shù)組進(jìn)來(lái)進(jìn)行驗(yàn)證。第二個(gè)參數(shù)就是和上面一樣的驗(yàn)證配置信息。不同的,它的第三個(gè)參數(shù)是我們可以自定義的驗(yàn)證提示信息。有了這個(gè)參數(shù),返回的提示需要什么樣的內(nèi)容就方便了很多。
最后,還有一處不同的是,這個(gè) Validator 對(duì)象不是用得請(qǐng)求 Request 的方法,所以它不會(huì)自動(dòng)返回,需要自己構(gòu)造 Response ,在這里,我們跳轉(zhuǎn)回了原來(lái)的頁(yè)面,并且將錯(cuò)誤信息通過(guò) withErrors() 添加到了模板的 \$errors 變量中。進(jìn)入 withError() 方法,我們可以看到 $errors 是保存在 session 的 flash() 中,這個(gè)我們后面講 session 的時(shí)候再說(shuō)。
// laravel/framework/src/Illuminate/Http/RedirectResponse.php
public function withErrors($provider, $key = 'default')
{
$value = $this->parseErrors($provider);
$errors = $this->session->get('errors', new ViewErrorBag);
if (! $errors instanceof ViewErrorBag) {
$errors = new ViewErrorBag;
}
$this->session->flash(
'errors', $errors->put($key, $value)
);
return $this;
}
不管是請(qǐng)求對(duì)象的驗(yàn)證函數(shù),還是我們通過(guò)門(mén)面 make() 后獲得的驗(yàn)證對(duì)象,它的核心都是 laravel/framework/src/Illuminate/Validation/Validator.php 這個(gè)文件中的 Validator 對(duì)象。在初始化的時(shí)候,會(huì)將數(shù)據(jù) data 、 規(guī)則 initialRules 、提示消息 customMessages 存放到這個(gè)對(duì)象的相關(guān)變量中,然后通過(guò)對(duì)象里面的 validateAttribute() 方法進(jìn)行參數(shù)和規(guī)則的匹配,并通過(guò) addFailure() 方法匹配對(duì)應(yīng)的提示消息信息,最后將這些信息放在 messages 屬性中。上面 withErrors() 的代碼中的 parseErrors() 最終的調(diào)用其實(shí)就是走到了 Validator 對(duì)象的 validateAttribute() 這個(gè)方法中。
基本上整個(gè)處理過(guò)程都是在這個(gè) Validator 對(duì)象里面,所以這里我也就不貼代碼了,大家自己調(diào)試一下。
這篇文章的內(nèi)容不少吧,我們學(xué)習(xí)了控制器和驗(yàn)證器相關(guān)的內(nèi)容,之所以把這兩個(gè)放在一起,也是因?yàn)轵?yàn)證這個(gè)功能一般都會(huì)在控制器的最開(kāi)始使用。當(dāng)然,我們?cè)谥v數(shù)據(jù)庫(kù)模型的時(shí)候,還有數(shù)據(jù)庫(kù)驗(yàn)證相關(guān)的內(nèi)容,和這邊又不太一樣了,這個(gè)我們等學(xué)習(xí)到的時(shí)候再說(shuō)。
控制器的內(nèi)容其實(shí)并不多,但里面的很多東西我們并沒(méi)有都講解到,畢竟現(xiàn)成的文檔都在,也沒(méi)必要全部再?gòu)?fù)制一遍,還是以調(diào)用路徑的源碼分析分主。下一個(gè)要講的內(nèi)容相信也是很多同學(xué)非常感興趣的,那就是中間件的應(yīng)用以及源碼的分析。
參考文檔:
https://learnku.com/docs/laravel/8.x/controllers/9368
https://learnku.com/docs/laravel/8.x/validation/937
聯(lián)系客服