深入剖析 Laravel 服务容器

之前在 深度挖掘 Laravel 生命周期 一文中,我们有去探究 Laravel 究竟是如何接收 HTTP 请求,又是如何生成响应并最终呈现给用户的工作原理。

本章将带领大家研究另一个 Laravel 框架的核心内容:「服务容器」。有阅读过 Laravel 文档 的朋友应该有注意到在「核心架构」篇章中包含了几个主题:生命周期服务容器服务提供者FacadesConcracts.

今天就让我们一起来揭开「Laravel 服务容器」的神秘面纱。

提示:本文内容较长可能需要耗费较多的阅读时间,另外文中包含 Laravel 内核代码建议选择合适的 IDE 或文本编辑器进行源码阅读。

目录结构

  • 序章
  • 依赖注入基本概念
    • 什么是依赖注入
    • 什么是依赖注入容器
    • 什么是控制反转(IoC)
  • Laravel 服务容器是什么
    • 小结
  • Laravel 服务容器的使用方法
    • 管理待创建类的依赖
    • 常用绑定方法
      • bind 简单绑定
      • singleton 单例绑定
      • instance 实例绑定
      • contextual-binding 上下文绑定
      • 自动注入和解析
  • Laravel 服务容器实现原理
    • 注册基础服务
      • 注册基础服务提供者
      • 注册核心服务别名到容器
    • 管理所需创建的类及其依赖
      • bind 方法执行原理
      • make 解析处理
  • 资料

序章

如果您有阅读我的前作 深度挖掘 Laravel 生命周期 一文,你应该已经注意到「APP 容器」、「服务容器」、「绑定」和「解析」这些字眼。没错这些技术都和「Laravel 服务容器」有着紧密的联系。

在学习什么是「Laravel 服务容器」之前,如果您对「IoC(控制反转)」、「DI(依赖注入)」和「依赖注入容器」等相关知识还不够了解的话,建议先学习一下这些资料:

虽然,这些学习资料都有细致的讲解容器相关的概念。但介绍一下与「Laravel 服务容器」有关的基本概念仍然有必要。

依赖注入基本概念

这个小结会捎带讲解下「IoC(控制反转)」、「DI(依赖注入)」和「依赖注入容器」这些概念。

什么是依赖注入

应用程序对需要使用的依赖「插件」在编译(编码)阶段仅依赖于接口的定义,到运行阶段由一个独立的组装模块(容器)完成对实现类的实例化工作,并将其「注射」到应用程序中称之为「依赖注入」。

一言以蔽之:面向接口编程。

至于如何实现面向接口编程,在 依赖注入系列教程 的前两篇中有实例演示,感兴趣的朋友可以去阅读这个教程。更多细节可以阅读 Inversion of Control Containers and the Dependency Injection pattern深入浅出依赖注入

什么是依赖注入容器

在依赖注入过程中,由一个独立的组装模块(容器)完成对实现类的实例化工作,那么这个组装模块就是「依赖注入容器」。

通俗一点讲,使用「依赖注入容器」时无需人肉使用 new 关键字去实例化所依赖的「插件」,转而由「依赖注入容器」自动的完成一个模块的组装、配置、实例化等工作。

什么是控制反转(IoC)

IoC 是 Inversion of Control 的简写,通常被称为控制反转,控制反转从字面上来说比较不容易被理解。

要掌握什么是「控制反转」需要整明白项目中「控制反转」究竟「反转」了哪方面的「控制」,它需要解决如何去定位(获取)服务所需要的依赖的实现。

实现控制反转时,通过将原先在模块内部完成具体实现类的实例化,移至模块的外部,然后再通过「依赖注入」的方式将具体实例「注入」到模块内即完成了对控制的反转操作。

「依赖注入」的结果就是「控制反转」的目的,也就说 控制反转 的最终目标是为了 实现项目的高内聚低耦合,而 实现这种目标 的方式则是通过 依赖注入 这种设计模式。

以上就是一些有关服务容器的一些基本概念。和我前面说的一样,本文不是一篇讲解依赖注入的文章,所以更多的细节需要大家自行去学习我之前列出的参考资料。

接下来才是今天的正餐,我将从以下几个角度讲解 Laravel 服务容器的相关内容:

  • Laravel 服务容器是什么;
  • Laravel 服务容器的使用方法;
  • Laravel 服务容器技术原理。

Laravel 服务容器是什么

Laravel 文档 中,有一段关于 Laravel 服务容器的介绍:

Laravel 服务容器是用于管理类的依赖和执行依赖注入的工具。依赖注入这个花俏名词实质上是指:类的依赖项通过构造函数,或者某些情况下通过「setter」方法「注入」到类中。

划下重点,「Laravel 服务容器」是用于 管理类的依赖执行依赖注入工具

通过前一节「依赖注入基本概念」相关阐述,我们不难得出这样一个简单的结论「Laravel 服务容器」就是「依赖注入容器」。

其实,服务容器作为「依赖注入容器」去完成 Laravel 所需依赖的注册、绑定和解析工作只是 「Laravel 服务容器」核心功能之一;另外,「Laravel 服务容器」还担纲 Laravel 应用的注册程序的功能。

节选一段「深度挖掘 Laravel 生命周期」一文中有关服务容器的内容:

创建应用实例即实例化 Illuminate\Foundation\Application 这个服务容器,后续我们称其为 APP 容器。在创建 APP 容器主要会完成:注册应用的基础路径并将路径绑定到 APP 容器 、注册基础服务提供者至 APP 容器 、注册核心容器别名至 APP 容器 等基础服务的注册工作。

所以要了解 Larvel 服务容器必然需要研究 Illuminate\Foundation\Application 的构造函数:

    /**
     * Create a new Illuminate application instance.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php#L162:27
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

没错在 Application 类的构造函数一共完成 3 个操作的处理功能:

  • 通过 registerBaseBindings() 方法将「App 实例(即 Laravel 服务容器)」自身注册到「Laravel 服务容器」;
  • 通过 registerBaseServiceProviders() 注册应用 Laravel 框架的基础服务提供者;
  • 通过 registerCoreContainerAliases() 将具体的「依赖注入容器」及其别名注册到「Laravel 服务容器」。

这里所说的「注册」归根到底还是在执行「Laravel 服务容器」的「绑定(bind)」操作,完成绑定接口到实现。

为了表名我所言非虚,让我们看看 registerBaseBindings() 方法:

    /**
     * Register the basic bindings into the container. 注册 App 实例本身到 App 容器
     *
     * @return void
     */
    protected function registerBaseBindings()
    {
        static::setInstance($this);

        $this->instance('app', $this);
        $this->instance(Container::class, $this);
        $this->instance(PackageManifest::class, new PackageManifest(
            new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
        ));
    }

我们知道 instance() 方法会将对象实例 $this 绑定到容器的 appContainer::class 接口。后续无论是通过 app()->make('app') 还是 app()->make(ontainer::class) 获取到的实现类都是 $this(即 Laravel 服务容器实例) 对象。有关 instance 的使用方法可以查阅 Laravel 服务容器解析文档,不过我也会在下文中给出相关使用说明。

到这里相信大家对「Laravel 服务容器」有了一个比较清晰的理解了。

小结

我们所说的「Laravel 服务容器」除了担纲「依赖注入容器」职能外;同时,还会作为 Laravel 项目的注册中心去完成基础服务的注册工作。直白一点讲在它的内部会将诸多服务的实现类「绑定」到「Laravel 服务容器」。总结起来它的作用主要可以归为以下 2 方面:

  1. 注册基础服务;
  2. 管理所需创建的类及其依赖。

Laravel 服务容器的使用方法

Laravel 服务容器在使用时一般分为两个阶段:使用之前进行绑定(bind)完成将实现绑定到接口;使用时对通过接口解析(make)出服务。

Laravel 内置多种不同的绑定方法以用于不同的使用场景。但无论哪种绑定方式,它们的最终目标是一致的:绑定接口到实现。

这样的好处是在项目的编码阶段建立起接口和实现的映射关系,到使用阶段通过抽象类(接口)解析出它的具体实现,这样就实现了项目中的解耦。

在讲解这些绑定方法前,先讲一个 Laravel 服务容器的使用场景。

管理待创建类的依赖

通过向服务容器中绑定需要创建的类及其依赖,当需要使用这个类时直接从服务容器中解析出这个类的实例。类的实例化及其依赖的注入,完全由服务容器自动的去完成。

举个示例,相比于通过 new 关键词创建类实例:

<?php
$dependency = new ConfigDependency(config('cache.config.setting'));
$cache = new MemcachedCache($dependency);

每次实例化时我们都需要手动的将依赖 $dependency 传入到构造函数内。

而如果我们通过「Laravel 服务容器」绑定来管理依赖的话:

<?php
App::bind(Cache::class, function () {
    $dependency = new ConfigDependency(config('cache.config.setting'));
    return $cache = new MemcachedCache($dependency);
});

仅需在匿名函数内一次创建所需依赖 $dependency,再将依赖传入到服务进行实例化,并返回服务实例。

此时,使用 Cache 服务时只要从「Laravel 服务容器」中解析(make)出来即可,而无需每次手动传入 ConfigDependency 依赖再实例化服务。因为,所有的依赖注入工作此时都由 Laravel 服务容器 自动的给我们做好了,这样就简化了服务处理。

下面演示了如何解析出 Cache 服务:

<?php
$cache = App::make(Cache::class);

先了解 Laravel 服务容器的一个使用场景,会对学习服务容器的 绑定方式 大有裨益。

Laravel 服务容器解析 - 绑定 这部分的文档我们知道常用的绑定方式有:

  • bind($abstract, $concrete) 简单绑定:将实现绑定到接口,解析时每次返回新的实例;
  • singleton($abstract, $concrete) 单例绑定:将实现绑定到接口,与 bind 方法不同的是首次解析是创建实例,后续解析时直接获取首次解析的实例对象;
  • instance($abstract, $instance) 实例绑定:将实现实例绑定到接口;
  • 上下文绑定和自动注入。

接下来我们将学习这些绑定方法。

常用绑定方法

bind 简单绑定

bind 方法的功能是将服务的实现绑定到抽象类,然后在每次执行服务解析操作时,Laravel 容器都会重新创建实例对象。

bind 的使用方法已经在「管理待创建类的依赖」一节中有过简单的演示,它会在每次使用 App::make(Cache::class) 去解析 Cache 服务时,重新执行「绑定」操作中定义的闭包而重新创建 MemcachedCache 缓存实例。

bind 方法除了能够接收闭包作为实现外,还可以:

  • 接收具体实现类的类名;
  • 接收 null 值以绑定自身。

singleton 单例绑定

采用单例绑定时,仅在首次解析时创建实例,后续使用 make 进行解析服务操作都将直接获取这个已解析的对象,实现了 共享 操作。

绑定处理类似 bind 绑定,只需将 bind 方法替换成 singleton 方法即可:

App::singleton(Cache::class, function () {
    $dependency = new ConfigDependency(config('cache.config.setting'));
    return $cache = new MemcachedCache($dependency);
});

instance 实例绑定

实例绑定的功能是将已经创建的实例对象绑定到接口以供后续使用,这种使用场景类似于 注册表

比如用于存储用户模型:

<?php
// 创建一个用户实例
$artisan = new User('柳公子');

// 将实例绑定到服务容器
App::instance('login-user', $artisan);

// 获取用户实例
$artisan = App::make('login-user');

contextual-binding 上下文绑定

在了解上下文绑定之前,先解释下什么是上下文,引用「轮子哥」的一段解释:

每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。 「编程中什么是「Context(上下文)」?」 - vczh的回答

上下文绑定在 Laravel 服务容器解析 - 上下文绑定 文档中给出了相关示例:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

在项目中常会用到存储功能,得益于 Laravel 内置集成了 FlySystemFilesystem 接口,我们很容易实现多种存储服务的项目。

示例中将用户头像存储到本地,将用户上传的小视频存储到云服务。那么这个时就需要区分这样不同的使用场景(即上下文或者说环境)。

当用户存储头像(PhotoController::class)需要使用存储服务(Filesystem::class)时,我们将本地存储驱动,作为实现给到 PhotoController::class

function () {
    return Storage::disk('local');
}

而当用户上传视频 VideoController::class,需要使用存储服务(Filesystem::class)时,我们则将云服务驱动,作为实现给到 VideoController::class

function () {
    return Storage::disk('s3');
}

这样就实现了基于不同的环境获取不同的服务实现。

自动注入和解析

「Laravel 服务容器」功能强大的原因在于除了提供手动的绑定接口到实现的方法,还支持自动注入和解析的功能。

我们在编写控制器时,经常会使用类型提示功能将某个类作为依赖传入构造函数;但在执行这个类时却无需我们去实例化这个类所需的依赖,这一切归功于自动解析的能力。

比如,我们的用户控制器需要获取用户信息,然后在构造函数中定义 User 模型作为依赖:

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
class UserController
{
    private $user = null;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

然后,当访问用户模块时 Laravel 会自动解析出 User 模型,而无需手动的常见模型示例。

除了以上几种数据绑定方法外还有 tag(标签绑定)extend(扩展绑定) 等,毫无疑问这些内容在 Laravel 文档 也有介绍,所以这里就不再过多介绍了。

下一节,我们将深入到源码中去窥探下 Laravel 服务容器是如何进行绑定和解析处理的。

Laravel 服务容器实现原理

要了解一项技术的实现原理,免不了去探索源码,源码学习是个有意思的事情。这个过程不但让我们理解它是如何工作的,或许还会带给我们一些意外惊喜。

我们知道 Laravel 服务容器其实会处理以下两方面的工作:

  1. 注册基础服务;
  2. 管理所需创建的类及其依赖。

注册基础服务

关于注册基础服务,在「深度挖掘 Laravel 生命周期」一文中其实已经有所涉及,但并并不深入。

本文将进一步的研究注册基础服务的细节。除了研究这些服务究竟如何被注册到服务容器,还将学习它们是如何被使用的。所有的这些都需要我们深入到 Illuminate\Foundation\Application 类的内部:

    /**
     * Create a new Illuminate application instance.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php#L162:27
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

前面我们已经研究过 registerBaseBindings() 方法,了解到该方法主要是将自身绑定到了服务容器,如此我们便可以在项目中使用 $this->app->make('something') 去解析一项服务。

现在让我们将焦点集中到 registerBaseServiceProvidersregisterCoreContainerAliases 这两个方法。

注册基础服务提供者

打开 registerBaseServiceProviders 方法将发现在方法体中仅有 3 行代码,分别是注册 EventServiceProviderLogServiceProviderRoutingServiceProvider 这 3 个服务提供者:


    /**
     * Register all of the base service providers. 注册应用基础服务提供者
     *
     * @return void
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));
        $this->register(new LogServiceProvider($this));
        $this->register(new RoutingServiceProvider($this));
    }

    /**
     * Register a service provider with the application.
     *
     * @param  \Illuminate\Support\ServiceProvider|string  $provider
     * @param  array  $options
     * @param  bool   $force
     * @return \Illuminate\Support\ServiceProvider
     */
    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }

        // If the given "provider" is a string, we will resolve it, passing in the
        // application instance automatically for the developer. This is simply
        // a more convenient way of specifying your service provider classes.
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }

        // 当服务提供者存在 register 方法时,执行 register 方法,完成绑定处理
        if (method_exists($provider, 'register')) {
            $provider->register();
        }

        $this->markAsRegistered($provider);

        // If the application has already booted, we will call this boot method on
        // the provider class so it has an opportunity to do its boot logic and
        // will be ready for any usage by this developer's application logic.
        // 执行服务提供者 boot 方法启动程序
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

    /**
     * Boot the given service provider. 启动给定服务提供者
     *
     * @param  \Illuminate\Support\ServiceProvider  $provider
     * @return mixed
     */
    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }

Laravel 服务容器在执行注册方法时,需要进行如下处理:

  1. 如果服务提供者存在 register 方法,会将服务实现绑定到容器操作 $provider->register();;
  2. 如果服务提供者存在 boot 方法,会在 bootProvider 方法内执行启动方法来启动这个服务。

值得指出的是在服务提供者的 register 方法中,最好仅执行「绑定」操作。

为了更好的说明服务提供者仅完成绑定操作,还是让我们来瞧瞧 EventServiceProvider 服务,看看它究竟做了什么:

<?php

namespace Illuminate\Events;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider. 注册服务提供者
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

没错 EventServiceProvider 所做的全部事情,仅仅通过 register 方法将闭包绑定到了服务容器,除此之外就什么都没有了。

注册核心服务别名到容器

用过 Laravel 框架的朋友应该知道在 Laravel 中有个别名系统。最常见的使用场景就是设置路由时,可以通过 Route 类完成一个新路由的注册,如:

Route::get('/', function() {
    return 'Hello World';
});

得益于 Laravel Facades 和别名系统我们可以很方便的通过别名来使用 Laravel 内置提供的各种服务。

注册别名和对应服务的映射关系,便是在 registerCoreContainerAliases 方法内来完成的。由于篇幅所限本文就不做具体细节的展开,后续会单独出一篇讲解别名系统的文章。

不过现在还是有必要浏览下 Laravel 提供了哪些别名服务:

    /**
     * Register the core class aliases in the container. 在容器中注册核心服务的别名
     *
     * @return void
     */
    public function registerCoreContainerAliases()
    {
        foreach ([
            'app'                  => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
            'auth'                 => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
            'auth.driver'          => [\Illuminate\Contracts\Auth\Guard::class],
            'blade.compiler'       => [\Illuminate\View\Compilers\BladeCompiler::class],
            'cache'                => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
            'cache.store'          => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
            'config'               => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
            'cookie'               => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
            'encrypter'            => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
            'db'                   => [\Illuminate\Database\DatabaseManager::class],
            'db.connection'        => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
            'events'               => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
            'files'                => [\Illuminate\Filesystem\Filesystem::class],
            'filesystem'           => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
            'filesystem.disk'      => [\Illuminate\Contracts\Filesystem\Filesystem::class],
            'filesystem.cloud'     => [\Illuminate\Contracts\Filesystem\Cloud::class],
            'hash'                 => [\Illuminate\Contracts\Hashing\Hasher::class],
            'translator'           => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
            'log'                  => [\Illuminate\Log\Writer::class, \Illuminate\Contracts\Logging\Log::class, \Psr\Log\LoggerInterface::class],
            'mailer'               => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
            'auth.password'        => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
            'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
            'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
            'queue.connection'     => [\Illuminate\Contracts\Queue\Queue::class],
            'queue.failer'         => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
            'redirect'             => [\Illuminate\Routing\Redirector::class],
            'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
            'request'              => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
            'router'               => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
            'session'              => [\Illuminate\Session\SessionManager::class],
            'session.store'        => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
            'url'                  => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
            'validator'            => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
            'view'                 => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

管理所需创建的类及其依赖

对于 Laravel 服务容器来讲,其内部实现上无论是 bindsingletontag 还是 extend 它们的基本原理大致类似。所以本文中我们仅研究 bind 绑定来管中窥豹。

我们知道绑定方法定义在 Laravel 服务容器 Illuminate\Foundation\Application 类内,而 Application继承自 Illuminate\Container\Container 类。这些与服务容器绑定相关的方法便直接继承自 Container 类。

bind 方法执行原理

bind 绑定作为最基本的绑定方法,可以很好的说明 Laravel 是如何实现绑定服务处理的。

下面摘出 Container 容器中 bind 方法及其相关联的方法。由于绑定处理中涉及较多方法,所以我直接将重要的代码片段相关注释做了翻译及补充说明,以便阅读:

    /**
     * Register a binding with the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        // 如果未提供实现类 $concrete,我们直接将抽象类作为实现 $abstract。
        // 这之后,我们无需明确指定 $abstract 和 $concrete 是否为单例模式,
        // 而是通过 $shared 标识来决定它们是单例还是每次都需要实例化处理。
        $this->dropStaleInstances($abstract);

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

        // 如果绑定时传入的实现类非闭包,即绑定时是直接给定了实现类的类名,
        // 这时要稍微处理下将类名封装成一个闭包,保证解析时处理手法的统一。
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

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

        // 最后如果抽象类已经被容器解析过,我们将触发 rebound 监听器。
        // 并且通过触发 rebound 监听器回调,将任何已被解析过的服务更新最新的实现到抽象接口。
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    /**
     * Get the Closure to be used when building a type. 当绑定实现为类名时,则封装成闭包并返回。
     *
     * @param  string  $abstract
     * @param  string  $concrete
     * @return \Closure
     */
    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

    /**
     * Fire the "rebound" callbacks for the given abstract type. 依据给定的抽象服务接口,触发其 "rebound" 回调
     *
     * @param  string  $abstract
     * @return void
     */
    protected function rebound($abstract)
    {
        $instance = $this->make($abstract);

        foreach ($this->getReboundCallbacks($abstract) as $callback) {
            call_user_func($callback, $this, $instance);
        }
    }

    /**
     * Get the rebound callbacks for a given type. 获取给定抽象服务的回调函数。
     *
     * @param  string  $abstract
     * @return array
     */
    protected function getReboundCallbacks($abstract)
    {
        if (isset($this->reboundCallbacks[$abstract])) {
            return $this->reboundCallbacks[$abstract];
        }

        return [];
    }

bind 方法中,主要完成以下几个方面的处理:

  • 干掉之前解析过的服务实例;
  • 将绑定的实现类封装成闭包,以确保后续处理的统一;
  • 针对已解析过的服务实例,再次触发重新绑定回调函数,同时将最新的实现类更新到接口里面。

在绑定过程中,服务容器并不会执行服务的解析操作,这样有利于提升服务的性能。直到在项目运行期间,被使用时才会真正解析出需要使用的对应服务,实现「按需加载」。

make 解析处理

解析处理和绑定一样定义在 Illuminate\Container\Container 类中,无论是手动解析还是通过自动注入的方式,实现原理都是基于 PHP 的反射机制。

所有我们还是直接从 make 方法开始去挖出相关细节:

    /**
     * Resolve the given type from the container. 从容器中解析出给定服务具体实现
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

    /**
     * Resolve the given type from the container. 从容器中解析出给定服务具体实现
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    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);

        // 已准备就绪创建这个绑定的实例。下面将实例化给定实例及内嵌的所有依赖实例。
        // 到这里我们已经做好创建实例的准备工作。只有可以构建的服务才可以执行 build 方法去实例化服务;
        // 否则也就是说我们的服务还存在依赖,然后不断的去解析嵌套的依赖,知道它们可以去构建(isBuildable)。
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // 如果我们的服务存在扩展(extend)绑定,此时就需要去执行扩展。
        // 扩展绑定适用于修改服务的配置或者修饰(decorating)服务实现。
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // 如果我们的服务已单例模式绑定,此时无要将已解析的服务缓存到单例对象池中(instances),
        // 后续便可以直接获取单例服务对象了。
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

        $this->fireResolvingCallbacks($abstract, $object);

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

        array_pop($this->with);

        return $object;
    }

    /**
     * Determine if the given concrete is buildable. 判断给定的实现是否立马进行构建
     *
     * @param  mixed   $concrete
     * @param  string  $abstract
     * @return bool
     */
    protected function isBuildable($concrete, $abstract)
    {
        // 仅当实现类和接口相同或者实现为闭包时可构建
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    /**
     * Instantiate a concrete instance of the given type. 构建(实例化)给定类型的实现类(匿名函数)实例
     *
     * @param  string  $concrete
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
        // 如果给定的实现是一个闭包,直接执行并闭包,返回执行结果
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        $reflector = new ReflectionClass($concrete);

        // 如果需要解析的类无法实例化,即试图解析一个抽象类类型如: 接口或抽象类而非实现类,直接抛出异常。
        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();

        // 解析出所有依赖
        $instances = $this->resolveDependencies(
            $dependencies
        );

        array_pop($this->buildStack);

        // 这是我们就可以创建服务实例并返回。
        return $reflector->newInstanceArgs($instances);
    }

    /**
     * Resolve all of the dependencies from the ReflectionParameters. 从 ReflectionParameters 解析出所有构造函数所需依赖
     *
     * @param  array  $dependencies
     * @return array
     */
    protected function resolveDependencies(array $dependencies)
    {
        $results = [];

        foreach ($dependencies as $dependency) {
            // If this dependency has a override for this particular build we will use
            // that instead as the value. Otherwise, we will continue with this run
            // of resolutions and let reflection attempt to determine the result.
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            // 构造函数参数为非类时,即参数为 string、int 等标量类型或闭包时,按照标量和闭包解析;
            // 否则需要解析类。
            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
    }

    /**
     * Resolve a non-class hinted primitive dependency. 依据类型提示解析出标量类型(闭包)数据
     *
     * @param  \ReflectionParameter  $parameter
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolvePrimitive(ReflectionParameter $parameter)
    {
        if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name))) {
            return $concrete instanceof Closure ? $concrete($this) : $concrete;
        }

        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }

        $this->unresolvablePrimitive($parameter);
    }

    /**
     * Resolve a class based dependency from the container. 从服务容器中解析出类依赖(自动注入)
     *
     * @param  \ReflectionParameter  $parameter
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolveClass(ReflectionParameter $parameter)
    {
        try {
            return $this->make($parameter->getClass()->name);
        }

        catch (BindingResolutionException $e) {
            if ($parameter->isOptional()) {
                return $parameter->getDefaultValue();
            }

            throw $e;
        }
    }

以上,便是 Laravel 服务容器解析的核心,得益于 PHP 的反射机制,实现了自动依赖注入和服务解析处理,概括起来包含以下步骤:

    1. 对于单例绑定数据如果一解析过服务则直接返回,否则继续执行解析;
    1. 非单例绑定的服务类型,通过接口获取绑定实现类;
    1. 接口即服务或者闭包时进行构建(build)处理,构建时依托于 PHP 反射机制进行自动依赖注入解析出完整的服务实例对象;否则继续解析(make)出所有嵌套的依赖;
    1. 如果服务存在扩展绑定,解析出扩展绑定结果;
    1. 如果绑定服务为单例绑定类型(singleton),将解析到的服务加入到单例对象池;
    1. 其它处理如触发绑定监听器、将服务标记为已解析状态等,并返回服务实例。

更多细节处理还是需要我们进一步深入的内核中才能发掘出来,但到这其实已经差不太多了。有兴趣的朋友可以亲自了解下其它绑定方法的源码解析处理。

以上便是今天 Laravel 服务容器的全部内容,希望对大家有所启发。

资料

感谢一下优秀的学习资料:

https://www.insp.top/learn-laravel-container

https://laravel-china.org/articles/10421/depth-mining-of-laravel-life-cycle

https://laravel-china.org/articles/4698/laravel-core-ioc-service-container

https://hk.saowen.com/a/6c880512a3a01a10b07fb53364394b81eff931065ce33ec7ad36caac44e07852

http://rrylee.github.io/2015/09/23/laravel-container/

Laravel构造函数自动依赖注入
https://juejin.im/entry/5916a557a0bb9f005fe07b3a https://laravel-china.org/topics/3361/starting-with-the-1-line-of-code-with-a-systematic-understanding-of-the-core-concepts-of-service-container

关于 “深入剖析 Laravel 服务容器” 的一个意见

发表评论

电子邮件地址不会被公开。 必填项已用*标注