深入剖析 Laravel 服务容器

本文首发于 深入剖析 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(Container::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-contain...

博客:深度挖掘 Laravel 生命周期

博客:Laravel 核心 ——IoC 服务容器

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

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

https://blog.tanteng.me/2016/01/laravel-co...

https://juejin.im/entry/5916a557a0bb9f005f...

教程:从 1 行代码开始,带你系统性地理解 Service Container 核心概念

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 6年前 自动加精
liuqing_hu
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 9

这么好的帖子没有人评论吗,感谢楼主分享。

6年前 评论
命中水

好文,对服务容器有了新的认识。不过源码部分还是不太能够理解,还需要自己多读多想。
感谢~

6年前 评论

写的很不错,对于 Laravel 的服务容器的理解更深入了,感谢作者。

5年前 评论

看完了,作者写的很详细。唯一不足的是在文章中发现几个错别字。 :grin:

5年前 评论

非常棒!感谢作者!

4年前 评论

很透彻。谢谢分享。

4年前 评论

好文,感谢分享

2年前 评论