Laravel 底层分析:生命周期——启动框架(第二部分)
本篇用于介绍 Laravel 5.6 底层源码
绑定内核
在第一部分中,我们回顾了一旦创建了应用实例以及加载和核心服务提供者之后会发生什么。接下来的的内容是关于内核的。正如 bootstrap/app.php
中所做的那样,在我们新建应用程序后,我们尝试将HTTP和控制台内核的单例绑定到容器中。
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
请注意,事实上我们在 app/Http/
中已经绑定了我们的内核实现。 然而,你很可能没有注意到我们的内核继承了 Illuminate\Foundation\Http\Kernel
类。在我们将内核类和异常处理程序储存在容器中之后,还没有做任何的修改。仅仅是将其保存起来以备后用。这部分是在 app.php
文件中而不是隐藏在 vendor
目录中的某个地方实现的,所以我们可以修改任何一个绑定的实现。你可以(但是可能永远不会)将 App\Http\Kernel
与你想要的任何内核实现交换。到目前为止没有实例化内核,也没有创建内核对象。那么接下来就会发生什么呢。也请注意,我们将接口绑定为 key 用于从容器中获取,但我们事实上正在解析 App\Http\Kernel
。
解析内核
在 index.php
文件中存在一行代码如下:
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
这是完美绑定接口的例子,我们绑定了内核接口,但是实际上是解析具体类的实现。这是让事情变得有趣的地方。
当我们调用 make()
之后,我们可以看到创建了一个新的内核实例并且储存在 $kernel
变量中。让我们看看内核类的构造函数( App\Http\Kernel
类也继承了内核类)。
// Illuminate\Foundation\Http\Kernel
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
请注意, $app
和 $router
参数正在构造函数中被解析和自动注入。 我们将在 谁想知道更多 部分带大家了解 make()
命令。
注意,这里的 $router
只是 Router 类的一个新实例,并且不包含任何路由或任何配置,仅仅只是一个事件分派器,你应该记得,我们在第 1 部分中将其绑定到容器。 如果你不相信上面所述,你自己 dd
一个 Router 类看看就知道了,如下:
Router {#36 ▼
#events: Dispatcher {#37 ▶}
#container: Application {#7 ▶}
#routes: RouteCollection {#39 ▼
#routes: []
#allRoutes: []
#nameList: []
#actionList: []
}
#current: null
#currentRequest: null
#middleware: []
#middlewareGroups: []
+middlewarePriority: []
#binders: []
#patterns: []
#groupStack: []
}
现在我们看代码。在注入容器和路由器之后,我们将获得全局中间件列表并将它们分配给路由器。
Note: 如果您想知道 $router->property = $contents
如何更改 $router
类的内容(不能使用常规变量),请记住这一点 PHP 通过引用传递类 (这意味着,如果您修改 $router
变量,实际类的内容就会被修改)。
我们还获得了在 app/Http/Kernel.php
中定义的中间件组和路由中间件。并将它们分配给路由器中的类属性,仅此而已。基本上,内核构造函数从您自己的内核类中读取中间件列表,并将它们传递给路由器,这样路由器就可以对其进行操作。 注意,route 集合仍然是空的。
捕获请求
这里开始是真正的魔法发生的地方。让我们看下面的代码片段:
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
你可能以为 handle()
方法会首先被调用,但情况并非如此。 首先要做的是捕获请求。让我们看一下是怎样进行的。
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
我们可以看到,这个方法从全局变量中创建并返回了一个新的 Request 实例。如果我们查看的话,你会注意到那个很长的名称的方法叫做 enableHttpMethodParameterOverride
。 你有可能在你的表单中使用了 method_field('DELETE')
,对吧?在 enableHttpMethodParameterOverride()
中, 我们仅将其同样名字的属性设置为 true
来使 Laravel 能够从请求中读取 _method
来确定我们是哪种请求方式。接着,我们从全局变量中 ($_SERVER, $_POST, $_GET, $_COOKIE, ...
) 利用来自 Symfony 中 HttpFoundation 组件中的 Request 类来创建一个新的 Symfony Request 类。 从 Symfony中的 Request 类我们可以 看到 createFromGlobals()
方法, 具体如下:
public static function createFromGlobals()
{
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
return $request;
}
基本上, 这些代码调用了 另一个静态方法 叫 createRequestFromFactory
, 这个方法本质上返回了一个自身的新实例:
private static function createRequestFromFactory($bunchOfServerVariables)
{
// ... 我们暂时不需要理会的
return new static($bunchOfServerVariables);
}
这一切都在 Symfony 的 Request 组件上完成, 我可能有时会深入研究它. Symfony 的 Request 类 (例如 cookies, headers, server vars, …) 在初始化 (在构造方法中 读取了所有的全局变量并且存储它们. createFromGlobals
方法中剩余的代码监测我们的请求类型是否是 PUT, DELETE, 或者 PATCH, 若是则只读取输入变量 . 截至目前, headers , server variables 和 cookies 都已经被读取了. 让我们看会 Laravel 的 capture
方法. 下一个是 createFromBase
, 它接受了 Symfony 刚刚创建的 request, 复制里面所有内容并且实例化一个 Laravel 的 Request 类.
public static function createFromBase(SymfonyRequest $request)
{
// 现在我们不需要关心这个
if ($request instanceof static) {
return $request;
}
$content = $request->content;
$request = (new static)->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);
$request->content = $content;
$request->request = $request->getInputSource();
return $request;
}
到目前为止, 我们有了一个请求信息, 例如请求类型, URI, 请求头, cookies, 等等. 你可以在 capture 方法执行的时候用 dd($request)
检查 Request 类.
内核处理(Kernel handling)
接着,我们调用 handle()
方法来处理我们捕获的请求。让我们查看一下 Kernel 类中的 handle
方法。
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
请注意,当我们再次调用 enableHttpMethodParameterOverride()
方法时,仅仅是为了确保可以读到 _method
参数(以防 Symfony 修改了它们的实现)。之后在那里标注一个标识,我们通过路由传递请求来创建一个响应,我们调度一个处理请求的事件,并且将该响应返回给用户。如果这个过程中出现了某些方法失败或者抛出异常的情况, Laravel’s 的异常处理(绑定在我们的 app.php
当中)将会收集并且向用户报告/渲染异常信息。现在来看看 sendRequestThroughRouter
,我们可以看到一些很酷的事情:
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());
}
首先要做的第一件事,我们将请求类的实例绑定到容器中(每个HTTP请求只能有一个请求对象)并且清除了已解析的请求类的实例。之后我们调用 bootstrap()
方法来执行我们所准备的一切。让我们来看看那个方法,可以看到我们实际上是在容器中调用了 $this->app->bootstrapWith($bootstrappers)
方法,从 Kernel@$bootstrappers
属性中传递了 bootstrappers 列表:
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
启动核心组件
bootstrapWith
方法仅触发了事件,声明我们正在启动组件。 针对每个组件都会调用 bootstrap()
方法。 在这里我不会粘贴所有的组件代码,他们的名字已经足以说明他们的作用,但是我会在下面的列表中总结一下他们的作用。你也可以在这里查看这些组件。
LoadEnvironmentVariables
创建一个DotEnv
类并且读取.env
文件- 同时也会检查配置文件是否缓存, 如果有缓存那么不会再次读取
.env
文件 - 同样也会检查我们是否在指定的环境中,并读取
.env.{environment}
文件
- 同时也会检查配置文件是否缓存, 如果有缓存那么不会再次读取
LoadConfiguration
检查缓存的文件并且读取之 (如果有的话), 否则会使用 Symfony 的 Finder 组件来读取所有的配置文件- 同时也会设置对应的时区在你的
app.php
配置中:date_default_timezone_set($config->get('app.timezone', 'UTC'));
- 设置默认的 UTF-8 编码:
mb_internal_encoding('UTF-8');
- 同时也会设置对应的时区在你的
HandleExceptions
使用 PHP 的错误报告方法用来处理我们的自定义错误,这样我们能够使用类似 Whoops! 的 package 来显示调用栈
RegisterFacades
通过将它的实现储存到容器中来注册全部的 facades 。所以当你调用Facade::method()
时,Laravel将会从容器中解析出该类,并且对它们调用非静态的method()
方法- 在文档中查看real-time facades
- 也可以在包中使用 Facades
RegisterProviders
字面上代表了$app->registerConfiguredProviders();
。该方法循环遍历了config/app.php
文件中的providers
数组,并将这些服务提供者注册到容器中- 它还读取了所有的包服务提供者并且绑定了他们
- 注意,这就是评估服务提供者的
register()
方法内容的地方
BootProviders
字面上代表了$app->boot();
,它在遍历全部已经注册的服务提供商的同时对每一个服务提供商调用boot()
方法。- 注意,这就是评估服务提供者的
boot()
方法内容的地方
- 注意,这就是评估服务提供者的
我相信你已经注意到了 register()
方法和 boot()
方法之间的区别。如果你需要一些核心框架服务,需要等待直到框架将注册完全部的服务提供者,然后将你的代码储存到 boot()
方法中。如果你需要在提供者注册上完成一些事宜,那就把代码放到 register()
方法中。
在评估这些代码之后,接下来的内容就是通过路由器发送请求并获取响应。现在,如果你转储了 Application 类的内容,可以看到我们已经准备好的一切,所有路由已经被加载完毕,数据库连接也初始化完成。在这个课程中,我们还将了解一些核心组件,来查看在哪里创建的连接,在哪里加载的路由等。现在我们已经到了第2部分的结尾,希望你已经明白了如何捕获你的 HTTP 请求并且理解框如何启动框架。
谁想知道更多( Who wants to know more)
正如我所说, 让我们一起来看一下 Container 上的 make()
方法。 make()
只是 resolve() 方法的一个代理 。现在看一下 resolve()
,我们看到一堆代码(我已经删除了注释):
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
我们看到,我们尝试为别名加载类(例如,为 app 别名解析 Illuminate\Container\Container
),然后检查是否需要 上下文绑定
如果我们不需要上下文绑定,并且我们已经在 $instances
属性中绑定了类的实例,则直接返回该类实例。
如果我们没有已绑定的实例,我们应该构建 class 并且用 Reflection 来解析所有依赖项。
我们看到它尝试获得别名的具体实现(记住接口绑定)并查看该类是否可构建(可以实例化)。如果类是可构建的,我们 调用 build()
方法,然后我们使用 Reflection 来获取类的构造函数,并获取它的所有参数以递归方式解析它们,如果构造函数没有参数,只需 return new $class
就可以了。
如果我们有依赖关系,只需递归调用它们的 resolve()
还有一些额外的检查来查看该类是否应该是单例(或共享),然后只解析一个实例并继续解析同一个类。最后我们只是发送一些其他事件,就结束了!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
深度好文。可惜看不懂