Laravel 底层分析:生命周期和容器 Container(第一部分)

Laravel

本篇用于介绍 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::classPackageManifest::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 接口并绑定其实现可以是 RedisStoreMemcachedStoreFileStore 等的缓存接口。您可以轻松地交换实现并使用相同的 Cache::class 键解析它们。

如下 Laravel 文档中的一段:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher' // 这可以用任何具体的实现来替换
);

// 通过调用 app(App\Contracts\EventPusher::class) 来解析 RedisEventPusher 对象
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://crnkovic.me/laravel-behind-the-s...

译文地址:https://learnku.com/laravel/t/29566

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2

温故而知新

4年前 评论

明白原理才能更好的应用,赞!

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!