Laravel HTTP层 中间键

什么是中间键

对于一个Web应用来说,在一个请求真正处理前,我们可能会对请求做各种各样的判断,然后才可以让它继续传递到更深层次中。而如果我们用if else这样子来,一旦需要判断的条件越来越来多,会使得代码更加难以维护,系统间的耦合会增加,而中间件就可以解决这个问题。我们可以把这些判断独立出来做成中间件,可以很方便的过滤请求。

Laravel中的中间件:

在Laravel中,中间件的实现其实是依赖于管道Illuminate\Pipeline\Pipeline这个类实现的,我们先来看看触发中间件的代码。很简单,就是处理后把请求转交给一个闭包就可以继续传递了。

    public function handle($request, Closure $next) {
        //对请求做的一些事
        return $next($request);
    }

中间件内部实现原理

return (new Pipeline($this->container))
        ->send($request)
        ->through($middleware)
        ->then(function ($request) use ($route) {
                return $this->prepareResponse(
                    $request,
                    $route->run($request)
                );
    });

可以看到,中间件执行过程调用了三个方法。再来看看这三个方法的代码:

send方法

public function send($passable){
    $this->passable = $passable;
    return $this;
}

send方法就是设置了需要在中间件中流水处理的对象,在这里就是HTTP请求实例。

through方法

public function through($pipes){
    $this->pipes = is_array($pipes) ? $pipes :func_get_args();
    return $this;
}

through方法就是设置一下需要经过哪些中间件处理。

then方法

public function then(Closure $destination){
   //接受一个闭包作为参数,然后经过getInitialSlice包装,而getInitialSlice返回的其实也是一个闭包
   $firstSlice = $this->getInitialSlice($destination);
   //反转中间件数组,主要是利用了栈的特性
   $pipes = array_reverse($this->pipes);
   //call_user_func 就是执行了一个array_reduce返回的闭包
   return call_user_func(
       //array_reduce来用回调函数处理数组。其实 arrary_reduce 就是包装闭包然后移交给call_user_func来执行
       array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
   );
}

由于aray_reduce的第二个参数需要一个函数,我们这里重点看看getSlice()方法的源码

protected function getSlice(){
    return function ($stack, $pipe) {   //这里$stack
        return function ($passable) use ($stack, $pipe) {
            if ($pipe instanceof Closure) {
                return call_user_func($pipe, $passable, $stack);
            } else {
                list($name, $parameters) = $this->parsePipeString($pipe);
                    return call_user_func_array([$this->container->make($name),                             $this->method],
                        array_merge([$passable, $stack],
                        $parameters)
                    );
                }
            };
        };
    }

看到可能会很头晕,闭包返回闭包的。简化一下就是getSlice()返回一个函数A,而函数A又返回了函数B。为什么要返回两个函数呢?因为我们中间在传递过程中是用$next($request)来传递对象的,而$next($request)这样的写法就表示是执行了这个闭包,这个闭包就是函数A,然后返回函数B,可以给下一个中间件继续传递。

再来简化一下代码就是:

//这里的$stack其实就是闭包,第一次遍历的时候会传入$firstSlice这个闭包,
//以后每次都会传入下面的那个function; 而$pipe就是每一个中间件
array_reduce($pipes, function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
    };
}, $firstSlice);

再来看这一段代码:

/*判断是否为闭包,这里就是判断中间件形式是不是闭包,是的话直接执行并且传入$passable[请求实例]和$stack[传递给下一个中间件的闭包],
并且返回*/
if ($pipe instanceof Closure) {
   return call_user_func($pipe, $passable, $stack);
//不是闭包的时候就是形如这样Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode执行
}  else {
   //解析,把名称返回,这个$parameters看了许久源码还是看不懂,应该是和参数相关,不过不影响我们的分析
   list($name, $parameters) = $this->parsePipeString($pipe);
   //从容器中解析出中间件实例并且执行handle方法
    return call_user_func_array([$this->container->make($name), $this->method],
        //$passable就是请求实例,而$stack就是传递的闭包
        array_merge([$passable, $stack], $parameters)
    );
}

再看一张图片:

Laravel 中间键原理

一次迭代传入上一次的闭包和需要执行的中间件,由于反转了数组,基于栈先进后出的特性,所以中间件3第一个被包装,中间件1就在最外层了。要记得,arrary_reduc他不执行中间件代码,而是包装中间件。

看到这里应该明白了,array_reduce最后会返回func3,那么call_user_func(func3,$this->passable)实际就是:

return call_user_func($middleware[0]->handle, $this->passable, func2);

而我们的中间件中的handle代码是:

public function handle($request, Closure $next) {
    return $next($request);
}

这里就相当于return func2($request),这里的$request就是经过上一个中间件处理过的。所以正果中间件的过程就完了,理解起来会有点绕,只要记得最后是由最外面的call_user_func来执行中间件代码的.

创建中间键

首先,通过Artisan命令建立一个中间件。

php artisan make:middleware [中间件名称]

通过 Artisan 命令 make:middleware:

php artisan make:middleware CheckAge

这个命令会在 app/Http/Middleware 目录下创建一个新的中间件类CheckAge,在这个中间件中,我们只允许提供的 age
大于 200 的请求访问路由,否则,我们将用户重定向到 home

<?php
namespace App\Http\Middleware;
use Closure;
class CheckAge{ 
/** 
* 返回请求 [过滤器]
* 
* @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); 
  }
}

正如你所看到的,如果 age <= 200,中间件会返回一个 HTTP 重定向到客户端;否则,请求会被传递下去。将请求往下传递可以通过调用回调函数 $next 并传入 $request

理解中间件的最好方式就是将中间件看做 HTTP 请求到达目标动作之前必须经过的“层”,每一层都会检查请求并且可以完全拒绝它。

中间件之前/之后
一个中间件是请求前还是请求后执行取决于中间件本身。比如,以下中间件会在请求处理前执行一些任务:

<?php

namespace App\Http\Middleware;

use Closure;

class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // 执行动作

        return $next($request);
    }
}

而下面这个中间件则会在请求处理后执行其任务:

<?php

namespace App\Http\Middleware;

use Closure;

class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // 执行动作

        return $response;
    }
}

注册中间件

全局中间件

如果你想要中间件在每一个 HTTP 请求期间被执行,只需要将相应的中间件类设置到 app/Http/Kernel.php 的数组属性 $middleware 中即可。

protected $middleware = [
        //这是自带的例子
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        //这是我注册的中间件
        \App\Http\Middleware\TestMiddleware::class,
    ];

分配中间件到路由

如果你想要分配中间件到指定路由,首先应该在 app/Http/Kernel.php 文件中分配给该中间件一个key,默认情况下,该类的 $routeMiddleware 属性包含了 Laravel 自带的中间件,要添加你自己的中间件,只需要将其追加到后面并为其分配一个key,例如:

// 在 App\Http\Kernel 类中...

protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

中间件在 HTTP Kernel 中被定义后,可以使用 middleware 方法将其分配到路由:

Route::get('admin/profile', function () {
    //
})->middleware('auth');

使用数组分配多个中间件到路由:

Route::get('/', function () {
    //
})->middleware('first', 'second');

分配中间件的时候还可以传递完整的类名:

use App\Http\Middleware\CheckAge;

Route::get('admin/profile', function () {
    //
})->middleware(CheckAge::class);

中间件组

有时候你可能想要通过指定一个键名的方式将相关中间件分到同一个组里面,从而更方便将其分配到路由中,这可以通过使用 HTTP Kernel$middlewareGroups 属性实现。

Laravel 自带了开箱即用的 web 和 api 两个中间件组以分别包含可以应用到 Web UIAPI 路由的通用中间件:

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        'auth:api',
    ],
];

中间件组可以被分配给路由和控制器动作,使用和单个中间件分配同样的语法。再次申明,中间件组的目的只是让一次分配给路由多个中间件的实现更加方便:

Route::get('/', function () {
    //
})->middleware('web');

Route::group(['middleware' => ['web']], function () {
    //
});

注:默认情况下, RouteServiceProvider 自动将中间件组 web 应用到 routes/web.php 文件。

中间件参数

中间件还可以接收额外的自定义参数,例如,如果应用需要在执行给定动作之前验证认证用户是否拥有指定的角色,可以创建一个 CheckRole来接收角色名作为额外参数。

额外的中间件参数会在 $next 参数之后传入中间件:

<?php

namespace App\Http\Middleware;

use Closure;

class CheckRole
{
    /**
     * 运行请求过滤器
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @param string $role
     * @return mixed
     * translator http://laravelacademy.org
     */
    public function handle($request, Closure $next, $role)
    {
        if (! $request->user()->hasRole($role)) {
            // Redirect...
        }

        return $next($request);
    }

}

中间件参数可以在定义路由时通过 :分隔中间件名和参数名来指定,多个中间件参数可以通过,分隔:

Route::put('post/{id}', function ($id) {
    //
})->middleware('role:editor');

中间件代码分析

中间件在请求阶段会调用自己的handle()方法
同时中间件也可以在响应阶段使用,这时,会掉用它的terminate()方法。
所以,当需要在响应发出后使用中间件只需要重写terminate()方法即可。

<?php

namespace App\Http\Middleware;

use Closure;

class TestMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request);
    }
    public function terminate($request, $response)
    {
        //这里是响应后调用的方法
    }
}

handle()方法

handle()方法有两个参数
$request --->请求信息,里面包含了输入,URL,上传文件等等信息。
$next --->闭包函数。将接下来需要执行的逻辑装载到了其中。

返回值:
通过上文对参数的描述可以了解到:
当我们在中间件中return $next($request);时,相当与把请求传入接下来的逻辑中。
同时,中间件也可以返回重定向,不运行之前的逻辑。
例如,希望将页面重定向到'/welcome'的页面return redirect('welcome')

注意,这里是重定向到"/welcome"这个地址的route而不是"welcome"这个页面(view)。

terminate()方法

参数
$request --->请求信息,里面包含了输入,URL,上传文件等等信息。
$response -->响应消息,包含了逻辑处理完成后传出到的响应消息。

因为terminate()方法只是在响应后进行一些处理所以没有返回值。

终止中间件

有时候中间件可能需要在 HTTP 响应发送到浏览器之后做一些工作。比如,Laravel 内置的session中间件会在响应发送到浏览器之后将 Session 数据写到存储器中,为了实现这个功能,需要定义一个终止中间件并添加 terminate 方法到这个中间件:

<?php

namespace Illuminate\Session\Middleware;

use Closure;

class StartSession
{
    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response)
    {
        // 存储session数据...
    }
}

terminate方法将会接收请求和响应作为参数。定义了一个终止中间件之后,还需要将其加入到 HTTP kernel 的全局中间件列表中。

当调用中间件上的 terminate 方法时,Laravel 将会从服务容器中取出该中间件的新的实例,如果你想要在调用 handleterminate方法时使用同一个中间件实例,则需要使用容器的 singleton方法将该中间件注册到容器中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容