导读:今天我们来看看中间件的相关功能
Slim中的中间件分两种:应用级中间件、路由级中间件。
应用中间件基于Rack协议实现,可以在应用对象调用之前或之后检查、分析、或修改应用环境变量、请求对象、响应对象。
每个中间件类都继承自抽象类Middleware,且需要实现其抽象方法call。所有注册的中间件组成一个中间件栈,其结构类似于一个洋葱,先注册的中间件在里层,后注册的在外层,最里层的是应用对象自身,请求从外到里逐层进行处理,任何一层都可以根据条件直接响应请求或递归调用往里一层/下一个中间件。
以中间件SessionCookie与MethodOverride为例,其call方法实现如下所示:
// 中间件SessionCookie
public function call()
{
// 加载session数据
$this->loadSession();
// 调用下一个中间件
$this->next->call();
// 保存session数据
$this->saveSession();
}
// 中间件MethodOverride
public function call()
{
$env = $this->app->environment();
if (isset($env['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
// Header commonly used by Backbone.js and others
$env['slim.method_override.original_method'] = $env['REQUEST_METHOD'];
$env['REQUEST_METHOD'] = strtoupper($env['HTTP_X_HTTP_METHOD_OVERRIDE']);
} elseif (isset($env['REQUEST_METHOD']) && $env['REQUEST_METHOD'] === 'POST') {
// HTML Form Override
$req = new \Slim\Http\Request($env);
// $this->settings['key'] 默认为_METHOD
$method = $req->post($this->settings['key']);
if ($method) {
$env['slim.method_override.original_method'] = $env['REQUEST_METHOD'];
$env['REQUEST_METHOD'] = strtoupper($method);
}
}
// 调用下一个中间件
$this->next->call();
}
路由级中间件可以是任何可被调用的东西(is_callable返回true)。在相关路由的回调触发之前,会逐个调用绑定到这个路由的所有路由级中间件,代码实现如下所示:
// 路由类Route的dispatch方法:
/**
* Dispatch route
*
* This method invokes the route object's callable. If middleware is
* registered for the route, each callable middleware is invoked in
* the order specified.
*
* @return bool
*/
public function dispatch()
{
// 逐个调用绑定的路由级中间件,以当前路由对象作为参数传入
foreach ($this->middleware as $mw) {
call_user_func_array($mw, array($this));
}
// 调用路由回调,并将通过路由模式从URL中正则匹配到的参数传入回调
$return = call_user_func_array($this->getCallable(), array_values($this->getParams()));
return ($return === false) ? false : true;
}
先来看看Slim应用对象的call方法:
/**
* Call
*
* This method finds and iterates all route objects that match the current request URI.
*/
public function call()
{
try {
if (isset($this->environment['slim.flash'])) {
$this->view()->setData('flash', $this->environment['slim.flash']);
}
$this->applyHook('slim.before');
ob_start();
$this->applyHook('slim.before.router');
$dispatched = false;
// 路由匹配
$matchedRoutes = $this->router->getMatchedRoutes($this->request->getMethod(), $this->request->getResourceUri());
// 逐个路由分发执行
foreach ($matchedRoutes as $route) {
try {
$this->applyHook('slim.before.dispatch');
$dispatched = $route->dispatch();
$this->applyHook('slim.after.dispatch');
if ($dispatched) {
break;
}
} catch (\Slim\Exception\Pass $e) {
continue;
}
}
// 当路由的回调抛出非Pass异常时,则会响应404
// 这貌似不好吧?
if (!$dispatched) {
$this->notFound();
}
$this->applyHook('slim.after.router');
$this->stop();
} catch (\Slim\Exception\Stop $e) {
$this->response()->write(ob_get_clean());
} catch (\Exception $e) {
if ($this->config('debug')) {
throw $e;
} else {
try {
$this->response()->write(ob_get_clean());
$this->error($e);
} catch (\Slim\Exception\Stop $e) {
// Do nothing
}
}
}
}
其中用于路由匹配的Router类的getMatchedRoutes方法实现如下所示:
/**
* Return route objects that match the given HTTP method and URI
* @param string $httpMethod The HTTP method to match against
* @param string $resourceUri The resource URI to match against
* @param bool $reload Should matching routes be re-parsed?
* @return array[\Slim\Route]
*/
public function getMatchedRoutes($httpMethod, $resourceUri, $reload = false)
{
if ($reload || is_null($this->matchedRoutes)) {
$this->matchedRoutes = array();
foreach ($this->routes as $route) {
// 如果当前请求的HTTP方法不被当前route支持且不是ANY,则跳过
if (!$route->supportsHttpMethod($httpMethod) && !$route->supportsHttpMethod("ANY")) {
continue;
}
// 否则继续匹配环境变量PATH_INFO
if ($route->matches($resourceUri)) {
$this->matchedRoutes[] = $route;
}
}
}
return $this->matchedRoutes;
}
其中用于PATH_INFO匹配的Route类的matches方法实现如下所示:
/**
* Matches URI?
*
* Parse this route's pattern, and then compare it to an HTTP resource URI
* This method was modeled after the techniques demonstrated by Dan Sosedoff at:
*
* http://blog.sosedoff.com/2009/09/20/rails-like-php-url-router/
*
* @param string $resourceUri A Request URI
* @return bool
*/
public function matches($resourceUri)
{
//Convert URL params into regex patterns, construct a regex for this route, init params
// preg_replace_callback — 执行一个正则表达式搜索并且使用一个回调进行替换
$patternAsRegex = preg_replace_callback(
'#:([\w]+)\+?#',
array($this, 'matchesCallback'),
// 括号中的部分表示可选
// 如:/archive(/:year(/:month(/:day)))
str_replace(')', ')?', (string)$this->pattern)
);
// 即使pattern最后有斜杠/,对于URL来说也是可选的
if (substr($this->pattern, -1) === '/') {
$patternAsRegex .= '?';
}
$regex = '#^' . $patternAsRegex . '$#';
// 大小写不敏感
if ($this->caseSensitive === false) {
$regex .= 'i';
}
//Cache URL params' names and values if this route matches the current HTTP request
// 正则匹配
if (!preg_match($regex, $resourceUri, $paramValues)) {
return false;
}
foreach ($this->paramNames as $name) {
if (isset($paramValues[$name])) {
if (isset($this->paramNamesPath[$name])) {
$this->params[$name] = explode('/', urldecode($paramValues[$name]));
} else {
$this->params[$name] = urldecode($paramValues[$name]);
}
}
}
return true;
}
其中正则搜索替换的回调方法matchesCallback的实现如下所示:
/**
* Convert a URL parameter (e.g. ":id", ":id+") into a regular expression
* @param array $m URL parameters
* @return string Regular expression for URL parameter
*/
protected function matchesCallback($m)
{
$this->paramNames[] = $m[1];
if (isset($this->conditions[$m[1]])) {
return '(?P<' . $m[1] . '>' . $this->conditions[$m[1]] . ')';
}
if (substr($m[0], -1) === '+') {
$this->paramNamesPath[$m[1]] = 1;
return '(?P<' . $m[1] . '>.+)';
}
return '(?P<' . $m[1] . '>[^/]+)';
}