>[danger] 官方已經在前不久發布了ThinkPHP`5.1.7`版本,`5.1`版本相較于`5.0`版本而言,本身更加嚴謹和規范,更接近主流設計思想。近半年來,`5.1`版本更新頻繁,此次最新版本更是帶來了很多的新特性。正在或者打算使用`5.1`版本的朋友可以關注下了,因為經過此次更新后,`5.1`版本也進入穩定階段了,基本上不太會有大的調整了。
最新版本(`V5.1.7`)的主要新特性主要包含:
[TOC=2,2]
## 引入中間件支持
關于中間件想必很多開發者已經在其它的主流框架中了解到了,ThinkPHP`5.1`版本最開始沒有引入中間件的原因是考慮到用戶需要了解新的知識概念和用法,同時容易和原來的行為用法相混淆。
>[info] 為了更規范開發和最大程度的公用某些組件,新版正式引入中間件支持。
原來你可能需要在控制器的初始化方法中添加相類似的代碼,現在你可以把這些代碼從控制器中獨立出來,方便更多的模塊調用和重用。如果你原來使用的是行為來進行處理的話,那么是否決定也改為中間件完全取決于你自己,因為仍然可以兼容運行(至少在`5.1`版本中不會變化)。
中間件的基本用法我大概介紹下:
### 定義中間件
可以通過命令行指令快速生成中間件類
~~~
php think make:middleware Check
~~~
這個指令會 `application/http/middleware`目錄下面生成一個`Check`中間件,也就是說默認生成的中間件的命名空間都是`app\http\middleware`,這個必須注意。
~~~
namespace app\http\middleware;
class Check
{
public function handle($request, \Closure $next)
{
}
}
~~~
中間件的入口執行方法必須是`handle`方法,而且第一個參數是`Request`對象,第二個參數是一個閉包。
>[danger] 中間件`handle`方法的返回值必須是一個`Response`對象。
### 前置/后置中間件
中間件是在請求具體的操作之前還是之后執行,完全取決于中間件的定義本身。
下面是一個前置行為的中間件
~~~
namespace app\http\middleware;
class Before
{
public function handle($request, \Closure $next)
{
// 添加中間件執行代碼
return $next($request);
}
}
~~~
下面是一個后置行為的中間件
~~~
namespace app\http\middleware;
class After
{
public function handle($request, \Closure $next)
{
$response = $next($request);
// 添加中間件執行代碼
return $response;
}
}
~~~
### 注冊中間件
第一種方式:注冊全局中間件
你可以在應用目錄下面定義`middleware.php`文件,使用下面的方式:
~~~
return [
\app\http\middleware\Auth::class,
'Check',
'Hello',
];
~~~
中間件的注冊應該使用完整的類名,如果沒有指定命名空間則使用`app\http\middleware`作為命名空間。
全局中間件的執行順序就是定義順序。可以在定義全局中間件的時候傳入中間件參數,支持兩種方式傳入。
~~~
return [
[\app\http\middleware\Auth::class, 'admin'],
'Check',
'Hello:thinkphp',
];
~~~
上面的定義表示 給`Auth`中間件傳入`admin`參數,給`Hello`中間件傳入`thinkphp`參數。
### 路由中間件
最常用的中間件注冊方式是注冊路由中間件
~~~
Route::rule('hello/:name', 'hello')
->middleware('Auth');
~~~
或者使用完整的中間件類名
~~~
Route::rule('hello/:name', 'hello')
->middleware(app\http\middleware\Auth::class);
~~~
支持對路由分組注冊中間件
~~~
Route::group('hello', function(){
Route::rule('hello/:name', 'hello');
})->middleware('Auth');
~~~
如果需要傳入額外參數給中間件,可以使用
~~~
Route::rule('hello/:name', 'hello')
->middleware('Auth:admin');
~~~
如果使用的是常量方式定義,可以在第二個參數傳入中間件參數。
~~~
Route::rule('hello/:name', 'hello')
->middleware(Auth::class, 'admin');
~~~
>[danger] 中間件方法參數只能有一個,但可以支持任意類型,在`handle`方法的第三個參數傳入即可。
如果你不希望每次在路由中注冊完整的類名,還可以在應用配置文件`middleware.php`中進行中間件的預定義,指定中間件的別名。
~~~
return [
'auth' =>'app\http\middleware\Auth',
'check'=>[
'user'=> app\common\middleware\CheckUser::class
],
];
~~~
然后,在路由中使用別名注冊中間件。
~~~
Route::rule('hello/:name', 'hello')
->middleware(['auth', 'check.user']);
~~~
好了,更多的中間件用法還需要你親自實踐。
## 路由改進和提速
新版本的路由改進是最多的(幾乎整個過年都在調整路由組件^_^),我們知道路由無非是兩大要點,路由定義和路由檢測。
路由定義涉及到一個規范化的問題,新版路由在保持原來的用法前提下,內部做了一些架構和細節的調整,使得路由的用法更加對象化。如果你看源碼的話,就會發現核心類庫的`route`目錄下面新增了幾個類。
### 路由規則的變量用法改進
路由定義方面主要對域名路由和路由分組的功能進行了強化,以及路由規則的更靈活定義。
之前的路由規則中變量分兩種,普通變量和組合變量定義,新版把這兩種合二為一了(也就是說兩種變量沒有任何區別,僅僅是表現方式不同,而且是出于兼容考慮)。你現在完全可以在路由規則中以任何方式定義路由變量。
例如你可以使用下面的路由規則而不用管目前的URL分隔符是什么:
~~~
Route::rule('item/:name_:id', 'order/index');
Route::rule('product-:name', 'product/item');
~~~
另外,新版路由定義兩種變量方式`:name`和` `可以混合使用,但建議統一使用``,在性能上略有優勢(實際上,最終解析的時候系統會統一解析成后者)。
### 路由匹配算法改進
路由檢測其實是最耗性能的,尤其是路由匹配這塊,因為基本上都是采用的正則匹配。在`5.1.6`版本之前,如果定義了100個路由,那么最后的那個路由規則可能需要遍歷100次才能正確匹配到路由,而且每個路由規則中的路由變量都是單獨匹配的,所以這個路由匹配的性能開銷是隨著路由定義的數量指數上升的。
`5.1.6`版本開始,系統對路由的匹配這塊進行了算法優化,靈感來自于`fastRoute`。基本思想是分兩個步驟優化(思想其實很容易懂,但技術層面比較復雜,所以想了解怎么實現的話還是仔細看代碼吧~本文只闡述思想)。
第一個步驟是對單個路由規則的匹配算法進行調整,不按照變量進行多次匹配,而是把路由變量的正則合并到一起,然后整個路由規則只匹配一次(如果這個路由規則都是靜態的,那么基本上不需要正則匹配,采用的是更快的非正則檢測方式)。這個是新版默認就開啟的,相比較之前的版本,性能已經提升明顯。
但路由的性能提速遠非如此簡單,第二個步驟的優化策略是如果當前匹配的路由分組下面有多個路由規則(確切的說是滿足當前請求類型的),則把這些滿足條件的路由規則合并成一個正則表達式進行路由匹配。如果你的路由分組下面有100個滿足條件的路由規則,如果要訪問最后的路由規則,之前的方式可能需要遍歷100次,而新版只需要進行一次匹配。就算加上路由規則的合并開銷,仍然是值得的。
如果需要使用分組路由規則的全局合并檢測,需要開啟下面的設置(另外一個方式是單獨對某個路由分組使用`mergeRuleRegex`方法,這種需求應該不多):
~~~
// 合并分組路由規則
'route_rule_merge' => true,
~~~
那么到這是不是就結束了呢?不要忘了ThinkPHP`5.1`的路由有一個很創新的地方就是延遲解析,主要體現在路由分組或者域名路由。通常的路由定義是需要把定義的路由進行一次解析處理,然后保存成我們方便檢測的一個數據結構或者對象,但由于WEB請求的特殊性,每次請求都需要重復這種路由定義的解析過程,為了降低路由解析的開銷,ThinkPHP只會在路由分組匹配之后才會實際去進行該路由分組下面的路由解析過程,然后再進行后續的路由檢測,如果你的分組下面還有子分組,那么繼續按照這個方式解析。
不過路由的延遲解析功能默認是關閉的,可以在應用配置中開啟:
~~~
// 開啟路由延遲解析
'url_lazy_route' => true,
~~~
通過這三個步驟的優化,我相信現在你應該明白如何利用路由規則的合并以及路由的延遲解析機制來提升你的路由性能了吧。
如果你繼續深入路由的使用,還會發現一些細節的用法。
## 查詢安全性改進
接下來要講的是新版對于查詢安全性的一個改進,引入了一個新的`Expression`類,對于這種類型的數據,在查詢和寫入操作的時候會保持原樣`SQL`(比較適合于使用SQL函數的情況),并且同時支持參數綁定功能。
為了方便,`Query`類增加了`raw`方法用于實例化一個`Expression`對象,我們可以這樣使用:
~~~
Db::name('user')
->field(Db::raw('id, name'))
->where(Db::raw('id > :id AND status = 1'), ['id' => 1])
->order(Db::raw('id desc'))
->select();
~~~
系統在解析的時候,支持對`field/where/order`方法的`Expression`對象的解析。
同時為了方便使用,提供了一些快捷方法,例如上面的例子可以改為:
~~~
Db::name('user')
->fieldRaw('id, name')
->whereRaw('id > :id AND status = 1', ['id' => 1])
->orderRaw('id desc')
->select();
~~~
> 當然,大部分情況下,系統會自動判斷是否需要使用`Expression`表達式對象,從而避免數據出現安全隱患。例如,當你使用字符串條件,并且包含空格和函數用法的話,會自動解析為`Expression`對象執行查詢。
在數據寫入方面一樣可以利用表達式方式來杜絕可能的安全隱患,例如:
~~~
Db::name('user')
->save($data);
~~~
當你的`data`數據直接來自于表單提交數據的時候,會導致SQL注入的可能,新版會檢查數組數據的安全性,對`exp`表達式寫入進行更嚴格的類型檢查。
原來的`exp`數組用法將不可用,而必須改為:
~~~
Db::name('user')
->where('id', 1)
->save([
'score'=>['exp', Db::raw('score+1')],
]);
~~~
或者直接使用`exp`方法更新數據
~~~
Db::name('user')
->where('id', 1)
->exp('score', 'score+1')
->save();
~~~
這些查詢的安全特性同樣對模型適用,比如你需要使用SQL更新數據的時候,可以使用
~~~
$user = User::get(1);
$user->name = Db::raw('UPPER("thinkphp")');
$user->score = Db::raw('score+1');
$user->save();
~~~
新版的特性暫時就介紹這么多,還有一些細節等待各位去挖掘和分享。
> 本文使用了看云文檔的單頁文檔功能,有興趣的可以多了解下,并且多使用看云進行一些技術的分享和電子出版。