【Laravel系列6.3】框架启动与服务容器源码

2023-03-03 13:28:43 浏览数 (3)

框架启动与服务容器源码

了解了服务容器的原理,要处理的问题,以及 Laravel 中如何使用服务容器以及服务提供者之后,我们就进入到了源码的学习中。其实服务容器的源码还是比较好理解的,毕竟我们已经自己实现过一个简单的服务容器了。在这里,我们也顺便看一下 Laravel 框架启动时的容器加载情况。

框架启动

通过之前的学习,我们已经了解到 Laravel 是单一入口文件的框架。所以我们直接去 public/index.php 查看这个入口文件。

代码语言:javascript复制
// public/index.php
$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = tap($kernel->handle(
    $request = Request::capture()
))->send();

$kernel->terminate($request, $response);

天啊,这也太明显了吧,上来就加载了一个 bootstrap/app.php 这个文件,然后就开始使用 $app->make() 来调用容器的实现方法了。那么我们很清楚地就可以发现,这个 bootstrap/app.php 就是一个服务容器。话不多说,马上进入到 bootstrap/app.php 文件中。

代码语言:javascript复制
$app = new IlluminateFoundationApplication(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

$app->singleton(
    IlluminateContractsHttpKernel::class,
    AppHttpKernel::class
);

$app->singleton(
    IlluminateContractsConsoleKernel::class,
    AppConsoleKernel::class
);

$app->singleton(
    IlluminateContractsDebugExceptionHandler::class,
    AppExceptionsHandler::class
);

return $app;

我们首先实例化了一个 IlluminateFoundationApplication 对象,然后再实例化了几个单例服务,分别是 Http 的核心 Kernel 和命令行 Console 的核心 Kernel 对象,另外还有一个异常控制对象。到这里,你也一定会想到了,这个 IlluminateFoundationApplication 就是我们整个 Laravel 框架的核心,也就是服务容器实现的核心。

Container 服务容器

打开 laravel/framework/src/Illuminate/Foundation/Application.php 文件,我们可以看到这个类继承的是一个叫做 Container 的类,这个单词就是容器的意思。从这里我们就可以看出,Laravel 是以 Application 也就是应用的意思来代替容器,但其实这个应用就是一个容器。由此可见,本身整个运行起来的 Laravel 就是一个超大的 Application 应用。

bind

在 Application 中,我们可以看到熟悉的 make() 和 boot() 方法,而 bind()、instance()、singleton() 方法则都在它的父类 Container 中实现的,我们先来看看 bind() 方法。

代码语言:javascript复制
public function bind($abstract, $concrete = null, $shared = false)
{
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }

        $concrete = $this->getClosure($abstract, $concrete);
    }

    $this->bindings[$abstract] = compact('concrete', 'shared');

    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

首先 dropStaleInstances() 是如果已经有同名的容器实现,也就是 instaces 数组中有的话,清理掉它,然后看实现参数 concrete 是否为空,如果为空的话把容器名称赋值给实现。接下来,判断实现是否是匿名函数形式的,如果不是的话,转换成一个匿名函数形式的实现方法。然后通过 compact() 函数将参数转换成数据并保存在 bindings 数组中。

想必这两个 instances 和 bindings 是干什么的不用我再多解释了吧。最后的 resolved() 方法是判断这个服务是否在默认的别名应用中,是否已经有 resolved 解决方案实例,如果有的话,调用 rebound() 对象 make() 它出来。

很明显,框架的代码比我们实现的服务容器代码可复杂多了,但是大体思想是一致的。至于后面的一些比较诡异的 resolved() 和 rebound() 是干嘛用的,我们后面再说。

singleton() 方法的实现就是调用的 bind() 方法,只不过最后一个 $shared 参数默认给了一个 ture 。从名字可以看出,这个 shared 是共享的意思,而 singleton 是单例的意思,暂时我们推测,在 make() 的时候,我们会根据这个变量来确定要实现加载的这个对象是不是使用单例模式。答案我们在看 make() 方法的时候再研究。

instance

singleton() 方法直接调用的就是 bind() 方法,只是最后一个参数默认给了一个 true ,所以我们也就不多说了,主要来看一下另外一个 instance() 方法。其实从上面代码就可以看出,bind() 方法的第二个参数只能是 Closure 或者 string 以及 null 类型的。如果我们想直接绑定一个实例,就需要使用 instance() 方法。

代码语言:javascript复制
public function instance($abstract, $instance)
{
    $this->removeAbstractAlias($abstract);

    $isBound = $this->bound($abstract);

    unset($this->aliases[$abstract]);

    $this->instances[$abstract] = $instance;

    if ($isBound) {
        $this->rebound($abstract);
    }

    return $instance;
}

在之前我们自己实现的那个容器类中,在 bind() 方法中直接进行了判断,如果是实例则直接放到 instances 数组中,而在 Laravel 中,则是分开了,必须在 instance() 方法中才会将实例保存到 instances 数组。

make

最后我们再来看一下 make() 方法,也就是从服务容器中获得我们的需要的对象。

代码语言:javascript复制
public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    if ($raiseEvents) {
        $this->fireBeforeResolvingCallbacks($abstract, $parameters);
    }

    $concrete = $this->getContextualConcrete($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    if (is_null($concrete)) {
        $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;
    }

    if ($raiseEvents) {
        $this->fireResolvingCallbacks($abstract, $object);
    }

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

make() 方法实际上调用的是 resolve() 这个方法,在这个方法内部,我们可以看到最后直接返回的就是一个 object 变量,很明显,它将会是一个对象。这个 object 是通过前面的一系列判断并调用相应的方法来获得的,通过 getAlias() 我们会获得需要实例化的对象是否有别名设置,这个设置主要是框架内部的很多对象都会进行一个别名配置,通常是框架比较核心的一些组件,然后 getContextualConcrete() 我们会获得当前容器中绑定的对象信息,接下来在 isBuildable() 中,判断容器名是否和我们传递过来的名称相同,以及容器内容是否是一个回调函数。如果两者有其一符合条件就进入 build() 方法,如果都不符合使用查找到的容器名两次调用 make() 方法。从这里我们会发现,服务实例化的核心转移到了 build() 方法中。

代码语言:javascript复制
public function build($concrete)
{
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    try {
        $reflector = new ReflectionClass($concrete);
    } catch (ReflectionException $e) {
        throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    }

    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);

        throw $e;
    }

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

在 build() 方法中,先判断绑定的容器内容是不是一个回调函数,如果是的话,直接调用这个回调函数并且返回了。如果不是回调函数的话,下面的内容相信大家也不会陌生了,通过 反射 的方式来创建对象。高大上不,如果你在 bind() 方法中,使用的是一个 AppContainerTestiPhone12::class ,这样的类字符串,那么它就会通过反射来生成这个对应的对象。

resolveDependencies() 用来解决类实例化时构造函数的依赖问题,需要的参数也是通过上面反射时 getParameters() 方法获取的。

ServiceProvider 服务提供者

通过上面的几个方法学习,我们了解到了整个 Laravel 容器中最重要的几个方法,也就是绑定实现以及获得具体的实例对象,是不是和我们自己实现的那个服务容器非常像。当然,就像之前我们说过的,在框架中的实现会比我们自己的实现要复杂很多。接下来我们看看服务提供者是怎么加载的。

回到 public/index.php 中,我们可以看到一段代码。

代码语言:javascript复制
$response = tap($kernel->handle(
    $request = Request::capture()
))->send();

这里调用了 kernel 的 handle() 方法,进入 vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php 的 handle() 方法之后继续查看 sendRequestThroughRouter() 方法,在这个方法中调用了一个 bootstrap() 方法。

代码语言:javascript复制
// vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

// vendor/laravel/framework/src/Illuminate/Foundation/Application.php
public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);

        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
    }
}

$this->bootstrappers() 返回的就是在 Kernel 中的那个 bootstrappers 属性,然后通过 vendor/laravel/framework/src/Illuminate/Foundation/Application.php 中的 bootstrapWith() 方法来加载这些预定义的服务提供者。

不对呀,这里都是预定义的服务提供者,我们自定义的那些服务提供者是在哪里加载的呢?

代码语言:javascript复制
protected $bootstrappers = [
    IlluminateFoundationBootstrapLoadEnvironmentVariables::class,
    IlluminateFoundationBootstrapLoadConfiguration::class,
    IlluminateFoundationBootstrapHandleExceptions::class,
    IlluminateFoundationBootstrapRegisterFacades::class,
    IlluminateFoundationBootstrapRegisterProviders::class,
    IlluminateFoundationBootstrapBootProviders::class,
];

注意看 RegisterProviders ,bootstrapWith() 方法会直接调用这些预定义服务提供者的 bootstrap() 方法,而 RegisterProviders 中的 bootstrap() 方法只有一行代码。

代码语言:javascript复制
public function bootstrap(Application $app)
{
    $app->registerConfiguredProviders();
}

它又回来继续调用 vendor/laravel/framework/src/Illuminate/Foundation/Application.php 中的 registerConfiguredProviders() 方法。

代码语言:javascript复制
public function registerConfiguredProviders()
{
    $providers = Collection::make($this->config['app.providers'])
                    ->partition(function ($provider) {
                        return strpos($provider, 'Illuminate\') === 0;
                    });

    $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

    (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                ->load($providers->collapse()->toArray());
}

其实到这里就已经很明显了,我们看到了 $this->config['app.providers'] 这个变量,它就是获得的 config/app.php 中的 providers 里面的内容,然后通过后面的代码将这些服务提供者注册到服务容器中。

当所有定义好的服务提供者注册完成后,会继续进行 $bootstrappers 中 BootProviders 服务提供者的注册,它会调用每个服务提供者的 boot() 方法完成各个服务的启动加载。这一下,你就知道为什么 boot() 方法可以调用到所有的服务了吧。

框架核心

通过来回查看 Kernel 和 Application ,相信你已经明白整个框架的核心就是在这两个类之间来回倒腾。默认的服务实例以及服务提供者都在 Application 的构造函数中进行了预加载,比如说路由、门面等等。而我们自定义的那些服务提供者则是通过 RegisterProviders 并进行配置读取后也完成了加载。

除些之外 Application 的 registerCoreContainerAliases() 中做好了许多别名对象的服务配置,当你搞不清楚为什么 $this->make('app') 可以使用的时候,就可以到这里来看一看。这些别名实例的定义最大的用途其实是在 门面 中使用,这个我们后面在讲到门面的时候还会再说。

总结

其实关于服务容器还有很多值得我们深入学习和挖掘的内容,但限于篇幅和本人的水平有限,这里只是梳理了一个大概的流程。大家可以继续顺着这两个核心的类,也就是 Kernel 和 Application 继续研究和探索,相信你的收获一定会更多。

1 人点赞