漫谈php框架之中间件
市面上常见的php框架有很多,最近因为有技术需求,所以对常见的php框架的中间件进行了一些了解。各个框架尽管在目标上对php框架的定义大同小异,但是在实现方式上却各有不同,且看下文:
定义
首先什么是php的中间件?
根据zend-framework中的定义:
所谓中间件是指提供在请求和响应之间的,能够截获请求,并在其基础上进行逻辑处理,与此同时能够完成请求的响应或传递到下一个中间件的代码。
这一介绍十分的简洁,但却略显抽象,接下来我们通过例子来一个个看。
处在原始时代的CI
首先来看CI框架,php star数 12830.
作为一款非常简洁的框架,CI被吐槽的不少,但是也有很多人喜欢。首先来看它官方给出的一张请求时序图:
![CI%u6846%u67B6%u8BF7%u6C42%u65F6%u5E8F](http://7rfkw4.com1.z0.glb.clouddn.com/appflowchart.png)
根据上文中对中间件的定义,那么对于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单纯一些;
- 提供中间件自动生成与方便扩展功能。