我们都遇到过这个错误,至少可以说这很烦人。看起来我们总是做对了一切,一旦我们尝试渲染页面,就会产生万能的 Route Not Defined 错误。 在本文中,我们将发现如何修复 Laravel 中非常常见的路由未定义错误。此错误通常指向路由、控制器或视图文件。调查这些文件将帮助我们修复路由未定义问题。…
laravel中间件Middleware原理解析及实例, Laravel 中间件原理
什么是中间件
中间件并不是 Laravel 框架所独有的概念,而是一种被广泛应用的架构模式,尤其是在 Java 中。
在 Laravel 中,通常可以将中间件理解为包裹在应用外面的「层」(就像洋葱那样),用户请求需要经过定义在路由之上的中间件按照顺序层层处理之后才能被最终处理,这样,就方便我们在用户请求被处理前定义一些过滤器或装饰器,比如拒绝用户请求或者在请求中追加参数。
当然,还有一些中间件被定义为在用户请求处理之后、发送响应到用户终端之前执行,我们将这种中间件称之为「终端中间件」,比如用户 Session 和 Cookie 队列的底层实现就借助了终端中间件。
综上,在 Laravel 请求的生命周期中,中间件可以作用于请求处理执行之前和之后,我们可以绘制相应的流程图如下,以便于理解:
注:如果你对这个流程图不太理解,可以查看 Laravel 底层是如何处理 HTTP 请求的这篇教程。
简化起来就是这样:
前面我们在请求类 Request 剖析及响应类 Response 剖析中说过,每一个进入 Laravel 应用的请求都会被转化为 Illuminate Request 对象,然后经过所有定义的中间件的处理,以及匹配路由对应处理逻辑处理之后,生成一个 Illuminate Response 响应对象,再经过终端中间件的处理,最终返回给终端用户。
目录
- 什么是中间件
- 创建中间件
- array_reduce 函数
- 中间件源码分析
什么是中间件
中间件在很多框架中有所应用,提供了一种机制方便过滤进入 HTTP 的一些请求。
在 Laravel 中:Request -> Middleware -> Middleware等 -> Response
创建中间件
1、生成中间键类:php artisan make:middleware XXX
2、编写代码,在中间件类的 handle 方法中实现前置中间件 return $next($request)之前
或者后置中间件return $next($request)之后
3、注册中间件:在app/Http/Kernel.php
中注册一个中间件
array_reduce 函数
因为在源码中用到了 array_reduce 函数,所以先了解它的具体用法,方便后续理解。
// array_reduce — 用回调函数迭代地将数组简化为单一的值 mixed array_reduce( array $input, callable $function[, mixed $initial = NULL] ) // array_reduce() 将回调函数 function 迭代地作用到 input 数组中的每一个单元中,从而将数组简化为单一的值。 ## 参数 input 需要迭代的数组 $function 回调函数 initial 如果指定了可选参数 initial,该参数将被当成是数组中的第一个值来处理,或者如果数组为空的话就作为最终返回值。
array_reduce 函数举例1:
$arr = ['aaa', 'bbb', 'ccc'];
$res = array_reduce($arr, function($carry, $piple){
return $carry.$piple;
});
/*
结果:
1、第一次迭代:$carry=null,$piple='aaa',返回'aaa'
2、第二次迭代:$carry='aaa'.$piple='bbb',返回'aaabbb'
2、第二次迭代:$carry='aaabbb'.$piple='ccc',返回'aaabbbccc'
*/
array_reduce 函数举例2(带初始值情况):
$arr = ['aaa', 'bbb', 'ccc'];
$res = array_reduce($arr, function($carry, $piple){
return $carry.$piple;
}, '初始值-');
/*
结果:
1、第一次迭代:$carry='初始值-',$piple='aaa',返回'初始值-aaa'
2、第二次迭代:$carry='初始值-aaa'.$piple='bbb',返回'初始值-aaabbb'
2、第二次迭代:$carry='初始值-aaabbb'.$piple='ccc',返回'初始值-aaabbbccc'
*/
源码分析
有关于中间件的执行逻辑是从 public/index.php 的 handle 开始,进入 handle 方法 的
// 处理请求 $response = $kernel->handle( // 创建请求实例 $request = Illuminate\Http\Request::capture() );
一个细节可以让你很好的理解为什么注册中间件需要在 app/Http/Kernel.php 中修改相关参数。
在 app/Http/Kernel.php 中查看源码可以看到 app/Http/Kernel.php 这个文件的类继承 Illuminate/Foundation/Http/Kernel.php ,所以在 app/Http/Kernel.php 中注册的中间件将会在父类中处理。
// lluminate/Foundation/Http/Kernel.php protected $middleware = []; protected $middlewareGroups = []; protected $routeMiddleware = [];
接着查看,vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php 的 handle 方法的sendRequestThroughRouter
// $response = $this->sendRequestThroughRouter($request); protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); //这里之后就会去处理中间件 return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
处理中间件:
1、构造 Illuminate\Routing\Pipeline
实例
2、将请求实例 $request
赋值给 Pipeline 的 passable 属性
3、在 through 方法中判断是否启用中间件(默认启用),启用的话则将全局中间件数组 $middleware
赋值到 Pipeline
的 $pipes
属性
4、调用 then 其中 dispatchToRouter 返回一个闭包函数,这个闭包函数将在处理完全局中间件逻辑后执行。
Illuminate/Pipeline/Pipeline.php 的send方法、through 方法
public function send($passable) { $this->passable = $passable; return $this; } public function through($pipes) { $this->pipes = is_array($pipes) ? $pipes : func_get_args(); return $this; }
Illuminate/Pipeline/Pipeline.php 的 then方法
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
分析 then 方法:
调用 array_reduce 进行预处理,array_reverse($this->pipes) 指的是倒序的待处理的全局中间件数组。
$this->carry() : 返回的是回调函数。
// Illuminate/Routing/Pipeline
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
$slice = parent::carry();
$callable = $slice($stack, $pipe);
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
}
$this->prepareDestination($destination)
是 carry 函数中 $stack
的初始值,该函数返回的也是闭包函数,传入的闭包参数 $destination
为 $this->dispatchToRouter()
返回的闭包函数,其中包含了路由和中间件的匹配和执行 的相关逻辑。
// Illuminate/Routing/Pipeline 文件中
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
}
当用 array_reduce 遍历完所有的 pipe 后,因为 carry 返回的是闭包函数,所以现在也未能执行中间件的 handle 方法 ,
以下代码是将所有全局中间件和路由处理通过统一的闭包函数进行迭代调用。最终返回的 $pipeline
是一个闭包函数:
// carry 方法中返回 return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { try { $slice = parent::carry(); $callable = $slice($stack, $pipe); return $callable($passable); } catch (Exception $e) { return $this->handleException($passable, $e); } catch (Throwable $e) { return $this->handleException($passable, new FatalThrowableError($e)); } }; };
$pipe
的值是全局中间件的第一个值,$stack
代表之前迭代的所有全局中间件和路由处理闭包
当代码执行到 then 方法的 return $pipeline($this->passable);
时才开始执行以下片段:
return function ($passable) use ($stack, $pipe) { try { $slice = parent::carry(); $callable = $slice($stack, $pipe); return $callable($passable); } catch (Exception $e) { return $this->handleException($passable, $e); } catch (Throwable $e) { return $this->handleException($passable, new FatalThrowableError($e)); } };
其中 $passable
为当前的请求实例 $request
,$stack
和 $pipe
则为 array_reduce 处理后的变量,上述代码中调用了父类的 carry 方法:
protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if (is_callable($pipe)) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { [$name, $parameters] = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; } $response = method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); return $response instanceof Responsable ? $response->toResponse($this->getContainer()->make(Request::class)) : $response; }; }; }
当前迭代的 $pipe
是闭包的话,直接执行并返回;如果$pipe
是对象的话,则实例化该对象并且解析出中间件参数,然后调用对象实例的 handle
方法处理相应的业务逻辑并且返回$response
,最后判断返回的$response
是否为Responsable实例,如果是的话直接返回HTTP响应到客户端,否则继续处理后续的中间件。
当迭代到最后一个全局中间件时,请求进入$this->prepareDestination
中返回的闭包函数 return $destination($passable);
protected function prepareDestination(Closure $destination) { return function ($passable) use ($destination) { return $destination($passable); }; } # prepareDestination 返回的是一个匿名函数,调用 prepareDestination 的时候并没有真正执行这个匿名函数( $destination($passable) )
通过代码来源发现最终执行的是:Illuminate\Foundation\Http\Kernel.php中的 dispatchToRouter方法返回的闭包函数:
function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request); };
其中 dispatch 方法如下:
// Illuminate\Routing\Router.php 文件内
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
dispatchToRoute 执行的业务逻辑如下:
// Illuminate\Routing\Router.php 文件内
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
// 返回给定路由的响应。 protected function runRoute(Request $request, Route $route) { $request->setRouteResolver(function () use ($route) { return $route; }); $this->events->dispatch(new Events\RouteMatched($route, $request)); return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) ); }
runRoute 中根据请求和路由定义的信息,去执行相应的闭包函数或者控制器方法。
核心函数 runRouteWithinStack:
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
return $this->prepareResponse( $request, $route->run() );
准备好响应数据
接着来到 public/index.php 中的 $response->send();
将响应数据返回给客户端
最后是 terminate 逻辑的调用,Laravel 中间件会检查全局中间件和路由中间件中是否包含 trminate 方法,如果包含的话则认为改中间件为 终端中间件,并执行其中的 terminate 方法。
terminate 中间件对应的底层代码如下:
protected function terminateMiddleware($request, $response) { $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge( $this->gatherRouteMiddleware($request), $this->middleware ); foreach ($middlewares as $middleware) { if (! is_string($middleware)) { continue; } [$name] = $this->parseMiddleware($middleware); $instance = $this->app->make($name); if (method_exists($instance, 'terminate')) { $instance->terminate($request, $response); } } }
laravel中间件Middleware原理解析
1、身份中间件 实例
legend3/app/Http/Middleware/Home/User.php
这是写的中间件的代码,逻辑就是判断用户是否登录,没登录就退出
<?php namespace App\Http\Middleware\Home; use Closure; use Auth; class User { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { //前端的登录中间件 //作用是用Auth::guard('user')->check()来验证用户登录, //如果登录成功就进行下一步 //如果没有登录成功就返回到登录页 if(!Auth::guard('user')->check()){ return redirect('/login'); } return $next($request); } }
legend3/app/Http/Kernel.php
这里是注册中间件,相当于给中间件取别名,使用的时候可以直接通过别名调用
protected $routeMiddleware = [ //fry //后端的登录中间件注册 'admin.auth'=>Middleware\Admin\Admin::class, //前端的登录中间件注册 'home.auth'=>Middleware\Home\User::class, // 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ];
在配置文件auth中注册guard
如果使用Auth::guard(‘user’)->check()
中的guard,就需要注册guard
如果只是作为普通中间件使用,不使用guard,就不需要
例如$this->middleware(‘home.auth’)->except([‘index’,’get_video_comment’]);
使用
在各个控制器中
class VideoController extends Controller { public function __construct() { //排除'index'方法,进行中间件的验证 $this->middleware('home.auth')->except(['index','get_video_comment']); // $this->middleware('home.auth')->except(['get_video_comment']); } 省略若干代码... }
如果没有登录,就会被被中间件拦截,跳转到登录页面
2、pjax中间件 实例
注册
中间件代码及位置
pjax中间件代码中的逻辑也非常简单,如果不是pjax请求或者是重定向,就不处理这个请求
否则就处理这个请求
本文:laravel中间件Middleware原理解析及实例, Laravel 中间件原理