前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Laravel系列6.3】框架启动与服务容器源码

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

作者头像
硬核项目经理
发布2023-03-03 13:28:43
1.9K0
发布2023-03-03 13:28:43
举报

框架启动与服务容器源码

了解了服务容器的原理,要处理的问题,以及 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 Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

我们首先实例化了一个 Illuminate\Foundation\Application 对象,然后再实例化了几个单例服务,分别是 Http 的核心 Kernel 和命令行 Console 的核心 Kernel 对象,另外还有一个异常控制对象。到这里,你也一定会想到了,这个 Illuminate\Foundation\Application 就是我们整个 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() 方法中,使用的是一个 \App\ContainerTest\iPhone12::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 = [
    \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,
];

注意看 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 继续研究和探索,相信你的收获一定会更多。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农老张 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 框架启动与服务容器源码
    • 框架启动
      • Container 服务容器
        • bind
        • instance
        • make
      • ServiceProvider 服务提供者
        • 框架核心
          • 总结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档