Laravel 底层分析:生命周期和容器 Container(第一部分)
本篇用于介绍 Laravel 5.6底层源码
最早加载的文件
一旦你打开某个网站,比如 http://example.com,你的 Web
服务器(nginx, Apache, ...)首先指向的是 public
目录下的 index.php
。 所以,你对网站的每一次请求都会先走到这个文件,让我们来看下 index.php
文件的代码:
<?php
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
那么让 我们开始吧...
define('LARAVEL_START', microtime(true));
这行代码本质上启动了一个计时器,从而可以计算启动框架需要多长时间等等。一个有趣的事实是这个常量在框架的生命周期中从来没有被使用过。
require __DIR__.'/../vendor/autoload.php';
这行代码引入了 Composer 启动文件, 基本上每一个 PHP 文件在这里进行加载和使用, 因此没有必要在代码中进行 require MyClass.php
。 注意这里并没有涉及到 Laravel 的任何内容
从这里事情变得有意思了...
$app = require_once __DIR__.'/../bootstrap/app.php';
让我们查看 /bootstrap/app.php
文件, 毕竟这是 $app
变量所包含的全部内容。
<?php
// 实例化 Application
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
// Http Kernel 单例
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
// Console Kernel 单例
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
// 异常处理 单例
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
// 返回 $app
return $app;
创建一个新的应用程序类实例
在这里,我们做了一些事情。基本上,我们启动框架概要,注册HTTP/控制台内核,并将应用程序的实例返回index.php。注意这里的app.php是一个应用程序工厂,这就是为什么它返回$app变量的原因,尽管$app由于需要而在全局上下文中。现在有代表LARAVER应用实例的Illuminate\Foundation\Application
类。这个类包含从环境、Laravel版本、容器、路径等所有内容。一旦我们创建了这个类的新实例,我们就将根目录传递给构造函数中的项目,并调用两个方法。注意,应用程序类扩展了容器类。调用的方法是
$this->setBasePath($basePath);
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
我们将检查这些方法中的每一个,并在每次调用后查看应用程序类的内容。
在容器中注册路径
setBasePath($rootDirectory)
方法在容器中注册了所有相关的路径, 例如 app 根路径, 存储路径,资源路径等。 注意:你可以很方便的更改配置, 例如:
class MyCoolApplication extends Illuminate\Foundation\Application
{
public function langPath()
{
// /languages/*
return $this->basePath.DIRECTORY_SEPARATOR.'language';
}
public function configPath()
{
// resources/configuration/*.php
return $this->resourcesPath().DIRECTORY_SEPARATOR.'configuration';
}
}
// 实例化自定义的 Application 来替代 Laravel 默认的
$app = new MyCoolApplication(
realpath(__DIR__.'/../')
);
在我们绑定完路径后,我们的程序结构看起来十分空旷:
Application {#7 ▼
#basePath: "/myCoolProject"
#hasBeenBootstrapped: false
#booted: false
...
#instances: array:9 [▼
"path" => "/myCoolProject/app"
"path.base" => "/myCoolProject"
"path.lang" => "/myCoolProject/resources/lang"
"path.config" => "/myCoolProject/config"
"path.public" => "/myCoolProject/public"
"path.storage" => "/myCoolProject/storage"
"path.database" => "/myCoolProject/database"
"path.resources" => "/myCoolProject/resources"
"path.bootstrap" => "/myCoolProject/bootstrap"
]
#aliases: []
#abstractAliases: []
...
}
你可以看到,还没有任何东西进行加载,容器目前只包含了路径。此时,任何路径都可以通过 $app->make('path.{what-you-want}')
方法进行解析。
在容器中注册自身
在 registerBaseBindings()
方法中,我们将自身(自身=应用类)设置为静态实例(所以我们可以用 单例模式 来解决这个问题)。这样做是因为我们只需要 一 个全局容器。接下来,我们在应用类上绑定一些别名,例如 app, Container::class
(Illuminate\Container\Container) 并且我们也可以做一件特定的事儿--绑定由 Illuminate\Foundation\PackageManifest
类为代表的 package-loader。 请注意,在这些类中的构造函数中除了设置相关的基础路径和将 FileSystem 类存放在其自身中之外,不要做任何其他的处理。此时尚未加载任何包。 在设置完别名之后,我们可以通过从自身中调用 app()
, app(Container::class)
或 app('Illuminate\Container\Container')
来解析容器。
static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
$this->instance(PackageManifest::class, new PackageManifest(
new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));
我们甚至可以猜测到我们的当前容器中的内容是什么样的... 它将包含路径,app,Container::class
和 PackageManifest::class
。让我们来看看:
Application {#7 ▼
...
#instances: array:12 [▼
"path" => "/myCoolProject/app"
"path.base" => "/Users/josip/Code/poslovi"
"path.lang" => "/myCoolProject/resources/lang"
"path.config" => "/myCoolProject/config"
"path.public" => "/myCoolProject/public"
"path.storage" => "/myCoolProject/storage"
"path.database" => "/myCoolProject/database"
"path.resources" => "/myCoolProject/resources"
"path.bootstrap" => "/myCoolProject/bootstrap"
"app" => Application {#7}
"Illuminate\Container\Container" => Application {#7}
"Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶}
]
#aliases: []
#abstractAliases: []
...
}
至此,我们已经完成了在容器中的注册,因此我们在需要的时候可以随时解析全局实例。接下来,是在生命周期中注册核心服务提供者。
注册核心服务提供者
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
这些是我们首先需要的3个核心服务。让我们看一下 register($provider)
方法中的具体内容。
// 因为这些提供者并没有被注册,所以没有什么意义
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
// 因为这里我们传递的是类的实例,并不是类的名称,所以也没有什么意义
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
// 在我们的每个提供者中调用 register() 方法
if (method_exists($provider, 'register')) {
$provider->register();
}
// 因为在我们的类中并没有这些属性,所以这里对我们没有什么意义
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
// 因为在我们的类中并没有这些属性,所以这里对我们没有什么意义
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$this->singleton($key, $value);
}
}
// 我们已经将该服务提供者设置为已注册,所以并不会载入第二次
$this->markAsRegistered($provider);
// 因为我们还没有被启动,所以对于我们没有什么意义
if ($this->booted) {
$this->bootProvider($provider);
}
return $provider; // 对我们来讲没什么意义
事件和日志的服务提供者(事件调度器和日志记录器)在容器中只绑定其具体实现的单例实例。当路由提供者启动时会配置路由器和包含的一些必要服务,例如 URL 生成器,重定向和控制调度器。关于路由器的讲解将会是一个单独的课程,因为它是框架中的一个复杂部分。 现在来看看容器,我们可以看到核心服务提供者已经被加载了。请注意,此时你的路由文件尚未被加载,我们只配置了路由请求所需要的所有内容。你的请求还尚未被路由。
Application {#7 ▼
...
#serviceProviders: array:3 [▼
0 => EventServiceProvider {#10 ▶}
1 => LogServiceProvider {#13 ▶}
2 => RoutingServiceProvider {#16 ▶}
]
...
#bindings: array:9 [▼
"events" => array:2 [▶]
"log" => array:2 [▶]
"router" => array:2 [▶]
"url" => array:2 [▶]
"redirect" => array:2 [▶]
"Psr\Http\Message\ServerRequestInterface" => array:2 [▶]
"Psr\Http\Message\ResponseInterface" => array:2 [▶]
"Illuminate\Contracts\Routing\ResponseFactory" => array:2 [▶]
"Illuminate\Routing\Contracts\ControllerDispatcher" => array:2 [▶]
]
#methodBindings: []
#instances: array:12 [▼
"path" => "/myCoolProject/app"
"path.base" => "/myCoolProject"
"path.lang" => "/myCoolProject/resources/lang"
"path.config" => "/myCoolProject/config"
"path.public" => "/myCoolProject/public"
"path.storage" => "/myCoolProject/storage"
"path.database" => "/myCoolProject/database"
"path.resources" => "/myCoolProject/resources"
"path.bootstrap" => "/myCoolProject/bootstrap"
"app" => Application {#7}
"Illuminate\Container\Container" => Application {#7}
"Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶}
]
...
}
注册核心类
在我们加载好日志记录器,路由器和事件调度器之后,便可以开始注册其他所有内容。这就是 registerCoreContainerAlisases()
的作用。让我们看看它的内容,我们可以看到全部加载完成的服务,例如认证,管理器,邮件收发器,数据库。
注意,我们还没有加载 .env 文件、配置或实例化与数据库的连接。我们仅仅在容器中加载和储存类,一切正在建立中。 炫酷的部分(解析请求, 连接数据库等)将在之后进行,并且我会在这个系列的下一部分来解释其原因和方法。
谁想知道更多
由于 singleton()
和 instance()
在整个框架中被调用的很多次,让我们来看看这些核心方法。 singleton()
方式中只是将 bind()
方法中的 $shared
参数设置为 true
之后执行。这个方法实现了绑定到容器,每当我们需要的时候可以解析它。
public function bind($abstract, $concrete = null, $shared = false)
{
// 在我们的例子中, $share = true
// 如果没有设置 $concrete ,$concrete会被设置与 $abstract 相同,或者只向下解析闭包
// 获取实例
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
// 将 $abstract 作为键 ,参数组成的数组作为值储存在 $bindings 属性中
$this->bindings[$abstract] = compact('concrete', 'shared');
// 如果已经在此容器中解析了抽象类型,
// 我们将触发反弹监听器。
// 以便已经解析的任何对象都可以通过侦听器回调更新对象的副本。
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
同时,instance()
方法将已经实例化的类绑定到容器。
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
// 我们将检查以前是否绑定过此对象。
// 如果绑定过,则触发 rebound 方法,进行重新绑定。
// 下面会自动覆盖 $abstract 字符串标识对应的绑定对象。
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
注意,这些方法有一个参数 $abstract
,之所以有这个参数,是因为我们可以将任何具体的实例或实现替换为另一个,但仍然使用相同的键解析它;即 $abstract
相当于 PHP 关联数组中的 key,而数组中的 value 就是绑定到容器中的对象,当我们想使用这个对象的时候,就可以通过 key 进行解析获取。 例如,拥有 Cache 接口并绑定其实现可以是 RedisStore、MemcachedStore、FileStore 等的缓存接口。您可以轻松地交换实现并使用相同的 Cache::class
键解析它们。
如下 Laravel 文档中的一段:
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher' // 这可以用任何具体的实现来替换
);
// 通过调用 app(App\Contracts\EventPusher::class) 来解析 RedisEventPusher 对象
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
温故而知新
明白原理才能更好的应用,赞!