我独自走进 Laravel5.5 的❤(七)
laravel 的中间件原理。
一、中间件的使用
中间件的使用也是十分之简单的鸭,仔细看看文档就可以使用得轻车熟路了。
1.1创建中间件
php artisan make:middleware FunnyMiddleware
1.2定义中间件
创建完成之后,我们可以看见如下的一个内容。
a. 原始状态
public function handle($request, Closure $next)
{
return $next($request);
}
b. 前置中间件
public function handle($request, Closure $next)
{
if(! session()->has('auth')) {
abort(404);
}
return $next($request);
}
c.后置中间件
public function handle($request, Closure $next)
{
$response = $next($request)
if(! session('auth')->id == 1) {
\Log('用户' . session('auth')->id . '访问了' . __CLASS__ . '::' . __FUNCTION__);
}
return $response;
}
1.3 使用中间件
a. 在路由中使用(这个部分网上例子太多了,自行看文档~)
b.在控制器中使用(这个部分网上例子太多了,自行看文档~)
c. 在 App\Providers\RouteServiceProvider.php 中使用,这里属于一种全局注册中间件的方法。
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapAdminRoutes();//注册 mapAdminRoutes 方法
//
}
protected function mapAdminRoutes()
{
Route::prefix('admin')
->middleware('admin')
->namespace($this->namespace)
->group(base_path('route/admin.php'));
//这里就是,前缀为 admin,namespace为App\Http\Controllers,路由文件为admin.php的路由都会被绑定 'admin' 中间件。
}
然后呢,做完上面的工作, 若合乎情理,被绑定中间件的路由都会使用中间件过滤请求。
二、源码解析(lara 中间件实现原理)
又到了,让人热血澎湃的时刻。
每当分析框架源码,内心就莫名的激动,这感觉就好像轻轻地抚摸着美女套在屁屁上光滑的底裤(好吧,开慢点)。
为了便于理解,依旧以 request 处理过程作为主线。
2.1 request 被发往路由器之前
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
var_dump($request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
//是的,这个是 lara 中间件处理 request 涉及到的第一步,此函数在之前提到过,$this->bootstrap() 会绑定服务提供者
//在 return 中会返回被中间件过滤的 request
//说是这么说,但实际上压根看不懂这是什么玩意
2.2 关于 Illuminate\Routing\Pipeline::class
由于 return 中返回的东西是对这玩意的对象的处理,所以就先来看看这是什么鬼,和中间件有什么鬼关联。
control B 打开发现文件内容如下:
use Closure;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Pipeline\Pipeline as BasePipeline;
use Symfony\Component\Debug\Exception\FatalThrowableError;
class Pipeline extends BasePipeline {
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));
}
};
};
}
}
所以这是一个 Illuminate\Pipeline\Pipeline 的子类,傀儡政权。
仔细看了一下内容,发现重写了 prepareDestination、carry、handleExceotion三个方法。ok,直接看一下他爸。
/**
* Set the object being sent through the pipeline.
* 设置通过管道发送的对象
* @param mixed $passable
* @return $this
*/
public function send($passable)
{
$this->passable = $passable;
return $this;
}
/**
* Set the array of pipes.
* 将管道转为数组形式
* @param array|mixed $pipes
* @return $this
*/
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
/**
* Run the pipeline with a final destination callback.
* 将最终的回调结果推进管道
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
一步一步来
2.2.1 send
send($request) 把 request 传入并设置为 passable 属性,下面就是打印出来的passable ,一目了然,就是传入的request,毫无争议。
protected 'passable' =>
object(Illuminate\Http\Request)[42]
protected 'json' => null
protected 'convertedFiles' => null
protected 'userResolver' => null
protected 'routeResolver' => null
public 'attributes' =>
object(Symfony\Component\HttpFoundation\ParameterBag)[44]
...
public 'request' =>
object(Symfony\Component\HttpFoundation\ParameterBag)[50]
...
public 'query' =>
object(Symfony\Component\HttpFoundation\ParameterBag)[50]
...
public 'server' =>
object(Symfony\Component\HttpFoundation\ServerBag)[46]
...
public 'files' =>
object(Symfony\Component\HttpFoundation\FileBag)[47]
...
public 'cookies' =>
object(Symfony\Component\HttpFoundation\ParameterBag)[45]
...
public 'headers' =>
object(Symfony\Component\HttpFoundation\HeaderBag)[48]
...
protected 'content' => null
protected 'languages' => null
protected 'charsets' => null
protected 'encodings' => null
protected 'acceptableContentTypes' => null
protected 'pathInfo' => null
protected 'requestUri' => null
protected 'baseUrl' => null
protected 'basePath' => null
protected 'method' => null
protected 'format' => null
protected 'session' => null
protected 'locale' => null
protected 'defaultLocale' => string 'en' (length=2)
private 'isHostValid' (Symfony\Component\HttpFoundation\Request) => boolean true
private 'isForwardedValid' (Symfony\Component\HttpFoundation\Request) => boolean true
2.2.2 through
through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 表面意思就是如果容器跳过中间件则设置通道为空数组,否则为路由绑定的中间件。
那么这个容器注入的中间件是在什么时候获得的呢?没错,就是在容器刚创建的时候,就注入了。父老乡亲们,看这里。
// bootstrap\app.php
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
没错,Illuminate\Contracts\Http\Kernel::class 单例绑定了 App\Http\Kernel::class
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
在make 后便生成实例。
然而,这里扩展一下,除了 app\kernel中的 alias
在 Illuminate\Foundation\Application 中还有自加载的核心组件
public function registerCoreContainerAliases()
{
foreach ([
'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
'db' => [\Illuminate\Database\DatabaseManager::class],
'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
'files' => [\Illuminate\Filesystem\Filesystem::class],
'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class],
'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class],
'hash' => [\Illuminate\Contracts\Hashing\Hasher::class],
'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
'log' => [\Illuminate\Log\Writer::class, \Illuminate\Contracts\Logging\Log::class, \Psr\Log\LoggerInterface::class],
'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
'auth.password' => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class],
'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
'redirect' => [\Illuminate\Routing\Redirector::class],
'redis' => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
'session' => [\Illuminate\Session\SessionManager::class],
'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
2.2.3 then
then($this->dispatchToRouter())
先来看看 $this->dispatchToRouter()
//设置路由分发回调函数
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);//这个看下面的 a
return $this->router->dispatch($request);//这个看下面的 b
};
}
a. Illuminate\Container\Container::class->instance
//实例化抽象类
public function instance($abstract, $instance)
{
// 删除 abstractAlias
$this->removeAbstractAlias($abstract); //这个看下面
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
// We'll check to determine if this type has been bound before, and if it has
// we will fire the rebound callbacks registered with the container and it
// can be updated with consuming classes that have gotten resolved here.
//我们将检查并决定这种类型是否在之前被绑定,如果是,我们将启动容器重新绑定的回调,
// 而且我们可以使用已经映射的消费类进行更新
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
Illuminate\Container\Container::class->removeAbstractAlias
protected function removeAbstractAlias($searched)
{
if (! isset($this->aliases[$searched])) { //如果没有设置了指定的alias 则不处理
return;
}
//否则,找到已经注入的 abstractAlias(alias 的 key) 并且删除
foreach ($this->abstractAliases as $abstract => $aliases) {
foreach ($aliases as $index => $alias) {
if ($alias == $searched) {
unset($this->abstractAliases[$abstract][$index]);
}
}
}
}
b. Illuminate\Router\Route::class->dispatch
/**
* Dispatch the request to the application.
* 分发请求到应用中
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
/**
* Dispatch the request to a route and return the response.
* 分发请求到其中一条路由并且返回响应
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
/**
* Find the route matching a given request.
* 根据请求找到相匹配的路由
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route); //这个就是上面提到的实例化函数
return $route;
}
// Illuminate\Routing\RouteCollection::class->match
/**
* Find the first route matching a given request.
* 找到第一条匹配请求的路由
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
// First, we will see if we can find a matching route for this current request
// method. If we can, great, we can just return it so that it can be called
// by the consumer. Otherwise we will check for routes with another verb.
//首先,我们将会根据目前的请求方法找到对应的路由,如果可以,我们可以只是
//返回它。否则我们将会根据其他的动作寻找对应的路由
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
// If no route was found we will now check if a matching route is specified by
// another HTTP verb. If it is we will need to throw a MethodNotAllowed and
// inform the user agent of which HTTP verb it should use for this route.
//如果指定其他的http动作也没有路由没有匹配的路由,我们将会抛出请求方法不允许异常
//并且通知浏览器应该使用那种http动作来请求该路由。(好像并没有这个通知)
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
// Illuminate\Routing\Router::class->runRoute
/**
* Return the response for the given route.
* 根据指定的路由返回响应
* @param Route $route
* @param Request $request
* @return mixed
*/
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)
);
}
/**
* Run the given route within a Stack "onion" instance.
* 在洋葱堆(Stack "onion"这玩意是啥,大神给我解释一下行不)实例里运行给定的路由
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
//这里有点奇怪,这段和Illuminate\Foundation\Http\Kernel::class::sendRequestThroughRouter中的
// through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 不是一样的?
$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()
);
});
}
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
// 倒序所有管道,并利用每个管道对请求进行过滤
//array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
// Illuminate\Routing\Pipeline::class->carry
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));
}
};
};
}
// Illuminate\Pipeline\Pipeline::class->carry
/**
* Get a Closure that represents a slice of the application onion.
* 获取一个表示洋葱应用的切片的闭包。(所以lara的开发者是亲切地称lara容器为洋葱?)
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
//如果管道是一个匿名函数类的实例,我们会直接调用并且我们会在容器外映射这个管道
//并且用适合的方法和理由来调用它并返回结果
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
//如果这个管道是一个字符串,我们将会解析这个字符串并且映射出这个类的依赖并注入容器。
//我们会建立可回调并执行管道函数,提供需要的参数
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
//如果管道已经是一个对象,我们只会制作一个回调并且把它推进管道这样。
//当这个对象被我们完全实例化,便没有需要去做任何额外的解析和格式化。
$parameters = [$passable, $stack];
}
return method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
};
};
}
//所以,我们可以发现 lara 使用管道设计模式解决了请求动作的中间件、路由匹配、路由请求方法处理等问题。
最后,将管道处理过的响应返回浏览器。
<--------------------- End ---------------------->
以上便是中间件涉及到的流程,如果有啥不对,请指出,互相学习。
至此,
致敬知识。
本作品采用《CC 协议》,转载必须注明作者和本文链接