市面上常見的php框架有很多,最近因為有技術需求,所以對常見的php框架的中間件進行了一些了解。各個框架盡管在目標上對php框架的定義大同小異,但是在實現方式上卻各有不同,且看下文:
定義
首先什么是php的中間件?
根據zend-framework中的定義:
所謂中間件是指提供在請求和響應之間的,能夠截獲請求,並在其基礎上進行邏輯處理,與此同時能夠完成請求的響應或傳遞到下一個中間件的代碼。
這一介紹十分的簡潔,但卻略顯抽象,接下來我們通過例子來一個個看。
處在原始時代的CI
首先來看CI框架,php star數 12830.
作為一款非常簡潔的框架,CI被吐槽的不少,但是也有很多人喜歡。首先來看它官方給出的一張請求時序圖:

根據上文中對中間件的定義,那么對於CI框架來說,唯一稱得上是內置中間件的:Security模塊
Security模塊是在請求進入controller之前實現的邏輯:
- 請求在完成路由之后,進入controller之前;
- CI框架支持通過配置的方式,決定是否啟用包括“URI安全、XSS過濾、CSRF保護”在內的功能模塊;
- 一旦框架初始化時探測到模塊啟用,那么優先進行模塊邏輯;
- 觸發安全模塊,請求即告終止。
乍看起來,CI框架的中間件十分的局限,但是其實它卻提供了無限的可能性。。因為CI中還提供了一個叫做Hooks的功能。即鈎子。
下面來看兩個個hooks的例子:
定義一個在controller邏輯之前的鈎子,並指定鈎子的參數、類名或函數名信息:
$hook['pre_controller'] = array( 'class' => 'MyClass', 'function' => 'Myfunction', 'filename' => 'Myclass.php', 'filepath' => 'hooks', 'params' => array('beer', 'wine', 'snacks') );
定義一個在controller邏輯之后的鈎子,並直接給出其實現:
$hook['post_controller'] = function() { /* do something here */ };
為什么說CI沒提供什么像樣的中間件但是又很靈活呢,就是因為它可以在如下的多個階段進行掛鈎子的操作。細數過來有7種之多。
從后文中可以看出,很多其他的框架可能也就會涵蓋兩三種階段,因此,從這個角度上來說,CI的鈎子組合而成的中間件的確很靈活。
- pre_system階段: 在系統執行的早期調用,這個時候只有 基准測試類 和 鈎子類 被加載了, 還沒有執行到路由或其他的流程;
- pre_controller階段: 在你的控制器調用之前執行,所有的基礎類都已加載,路由和安全檢查也已經完成;
- post_controller_constructor階段: 在你的控制器實例化之后立即執行,控制器的任何方法都還尚未調用;
- post_controller階段: 在你的控制器完全運行結束時執行;
- display_override階段: 覆蓋 _display() 方法,該方法用於在系統執行結束時向瀏覽器發送最終的頁面結果; 這可以讓你有自己的顯示頁面的方法。注意你可能需要使用
$this->CI =& get_instance()
方法來獲取 CI 超級對象,以及使用$this->CI->output->get_output()
方法來 獲取最終的顯示數據; - cache_override階段: 使用你自己的方法來替代 輸出類 中的 _display_cache() 方法,這讓你有自己的緩存顯示機制。
- post_system 在最終的頁面發送到瀏覽器之后、在系統的最后期被調用。
總結來看,CI中的中間件:
- 有很大的自由度
- 同時支持在多個階段對請求進行嵌入(對比下來是最全面的)
- 鈎子函數的使用成本高;
- 支持各種diy:
- 請求來時http校驗、權限校驗、額外的安全策略
- 請求去時上報數據
大紅大紫的Laravel
github star 24997
作為最近兩年大紅大紫的Laravel,的確也是有必要對其中間件機制進行了解:
首先Laravel提供了一個很好的中間件自動生成工具:php artisan make:middleware OldMiddleware
由Laravel的命令行完成,這種看似簡單的命令行工具其實可以對框架的擴展起到非常重要的作用。
再來看一個Laravel中典型的請求過濾器:
<?php namespace App\Http\Middleware; use Closure; class OldMiddleware { /** * 運行請求過濾器。 * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($request->input('age') <= 200) { return redirect('home'); } return $next($request); } }
過濾器,filter,是中間件中使用最廣泛的一種,很多框架里甚至filter就等同於中間件。意如其名,即是對請求Request進行某種過濾,這個過濾可以是參數上的限制、安全策略的限制、http協議的限制,只要是請求中帶來的屬性,都可以據此進行過濾。
同時這里也可以看到,Laravel使用閉包的方式進行請求的傳遞,真正踐行的優雅的中間件串聯的方式,只需要調用next函數,請求即可被按照預先定義的規則傳遞到下一個中間件中。
Laravel支持全局的中間件和根據具體路由規定的中間件兩種,同時優先級又以定義順序為准。做到全局與具體情況的兼顧。同時它顯示的支持前置、后置和Terminable三種中間件,覆蓋了大部分的中間件場景,是一種相對不錯的設計。
但美中不足或者說場景覆蓋不夠友好的地方在於它以路由的方式組織中間件,會與controller有些脫節,每次定義controller中action行為的時候,還需要轉換為路由進行配置,略有些不方便。
總結來看
- Laravel踐行了讓controller更純粹的思想,中間件交給路由,controller只做它該做的事;
- 中間件與路由組靈活結合,能夠滿足應用場景;
- 前置、后置與Terminable支持了現有大部分的中間件需求;
- 自動生成十分方便擴展中間件,開發友好;
- 但對一個controller內多個action需要統一加入或統一不加入中間件的場景,支持不友好。
老生常談yii 2.0
github star 4668
yii框架首先是中國人開發的,star數雖然不是很多,但是功能也算豐富。
yii框架從1.1到2.0,經過了一個比較大的升級,支持了很多新的特性,如果不支持,只怕是要落伍了。
在yii框架1.1中,中間件干脆就叫filters了,十分的直白,分為pre-filter和post-fiter兩種,即前文中說的,在進入controller之前的過濾邏輯,和完成controller處理之后的過濾邏輯。
但是到了yii2.0之后,filters經過了一層升級,到了behaviors,明確了一點:重心放在了每一個controller的行為上,而不是像Laravel一樣controller很傻很單純。
yii框架的behaviors可以在controller或application中配置。
這里是一個訪問控制的filter,具體進行什么樣的訪問控制由className定義,同時對controller中的action支持“only”關鍵字,還有“”關鍵字,能夠支持排除法的功能,這個在一些場景下還是很有用的。同時“roles”也能夠支持你預先定義好的角色的概念,比如學生無法訪問教師后台,而教師無法訪問學生論壇等。
use yii\filters\AccessControl; public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'only' => ['create', 'update'], 'rules' => [ // allow authenticated users [ 'allow' => true, 'roles' => ['@'], ], // everything else is denied by default ], ], ]; }
當然,在Yii中你也可以自定義filter:
namespace app\components; use Yii; use yii\base\ActionFilter; class ActionTimeFilter extends ActionFilter { private $_startTime; public function beforeAction($action) { $this->_startTime = microtime(true); return parent::beforeAction($action); } public function afterAction($action, $result) { $time = microtime(true) - $this->_startTime; Yii::trace("Action '{$action->uniqueId}' spent $time second."); return parent::afterAction($action, $result); } }
這里明顯可以看出,這個filter針對action,分別在“beforeAction”和“afterAction”兩個階段進行了邏輯處理,完成了請求的計時工作。
所以總的來看,Yii框架中的中間件:
- 支持前置和后置兩個階段的自定義;
- 提供了基本的訪問控制中間件;
- 配置侵入到controller中,完成對controller行為的深度控制;
- 無法自動生成中間件,自定義成本略高。
大家伙 ZendFramework
ZendFramework是由zend公司推出的php框架,其目標就是建立一套大而全的php框架。以滿足企業應用開發的目標。
ZendFramework由很多不同的模塊構成,使用者可以通過相互組合的方式來實現自己想要的功能,同時也能夠不一次加載大而全的框架,十分的靈活。
比如有負責授權的"zend-authentication",或者是負責驗證碼的"zend-captcha"等等。
其中"zend-stratigility" 負責提供中間件以及中間件執行流的功能。
use Zend\Stratigility\MiddlewarePipe; use Zend\Diactoros\Server; require __DIR__ . '/../vendor/autoload.php'; $app = new MiddlewarePipe(); $server = Server::createServer($app, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); // Landing page $app->pipe('/', function ($req, $res, $next) { if (! in_array($req->getUri()->getPath(), ['/', ''], true)) { return $next($req, $res); } return $res->end('Hello world!'); }); // Another page $app->pipe('/foo', function ($req, $res, $next) { return $res->end('FOO!'); }); $server->listen();
這里的代碼給出了兩個中間件的例子。第一個是落地頁,監聽了root路徑,如果命中了這一路由規則,那么請求會被提前結束,返回給用戶“Hello world!”。
而第二個中間件去匹配foo這一路徑,模糊匹配的方式,如果命中了,會返回FOO並結束請求。
與Laravel類似,這里同樣支持使用next(可調用的變量)的方式將請求繼續向下傳遞。而這里中間件配置的方式也跟Laravel比較像,是統一在一個地方根據路由進行配置的,這樣完全可以按照如下的方式根據不同的路由定義不同的中間件處理邏輯:
$app->pipe('/api', $apiMiddleware); $app->pipe('/docs', $apiDocMiddleware); $app->pipe('/files', $filesMiddleware);
總結來看,ZendFramework的中間件:
- 主要側重在請求前置階段,淡化了請求后置或其他階段
- 通過路由的方式統一配置中間件,支持串行
- 並未預先定義中間件
我心目中的中間件設計
首先按照不同的類別列舉一下常見的中間件:
- 前置中間件:
- cookie驗證:驗證用戶的cookie
- 用戶角色驗證:定義不同的用戶角色並驗證
- 用戶權限驗證:配置不同的用戶權限,並驗證
- 安全相關,如CSRF校驗:CSRF校驗中間件
- http方法過濾:過濾特定的GET POST請求
- http或者page cache:對指定路徑的頁面進行緩存
- 跨域中間件:不用在nginx配置,而是通過框架的方式,針對某些域名或某些請求,提供跨域的服務。
- 后置中間件:
- 共同數據輸出:針對統一業務的公共數據,在后置中統一輸出
- 請求返回瀏覽器之后的中間件:
- 打印日志
- 更新session(Laravel)
所以一個php框架最好能夠:
- 定義核心可用中間件;
- 提供在不同階段擴展中間件的能力,不能太多,支持前置和后置即可覆蓋大部分場景;
- 統一配置中間件,方便管理所有的中間件,讓controller單純一些;
- 提供中間件自動生成與方便擴展功能。