Laravel 服务容器的具体实现

文章目录

  • 没写完
      • 前言
        • 容器的定义
        • Laravel容器的解释
      • 服务容器
        • 容器的获取
        • 容器绑定
          • 简单绑定
          • 绑定接口到实现
          • 绑定一个单例
          • 绑定实例
          • 绑定初始数据
          • 上下文绑定
          • 标记
        • 服务解析
          • make
        • 依赖注入的实现
      • 后记

没写完

前言

容器的定义

容器,字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。当然,有这样一种容器,它存放的不是文本、数值,而是对象、对象的描述(类、接口)或者是提供对象的回调,通过这种容器,我们得以实现许多高级的功能,其中最常提到的,就是 “解耦” 、“依赖注入(DI)”。

Laravel容器的解释

Laravel 的核心就是一个 IoC 容器,根据文档,称其为“服务容器”,顾名思义,该容器提供了整个框架中需要的一系列服务。作为初学者,很多人会在这一个概念上犯难,因此,我打算从一些基础的内容开始讲解,通过理解面向对象开发中依赖的产生和解决方法,来逐渐揭开“依赖注入”的面纱,逐渐理解这一神奇的设计理念。

Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具。

依赖注入:只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动 new 的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)(个人觉得最清晰的解释

服务容器

容器的获取

  • 使用函数获取

app这个辅助函数定义在vendor/laravel/lumen-framework/src/helpers.php 里面,,这个文件定义了很多help函数,并且会通过composer自动加载到项目中(autoload_static.php文件中可以看到它,composer的具体实现可以自己百度去看看)。所以,在参与http请求处理的任何代码位置都能够访问其中的函数,比如app()

$app = app();
  • 使用门面获取

这个其实是用到Facade(门面),在laravel中会存在一个名称为aliases的数组,专门用来配置一些类型的别名,在lumenApp的别名是在vendor/barryvdh/laravel-ide-helper/src/Generator.php:244中定义了的,还有一些其他的门面定义,可以自行查看

use App;

App::basePath();
  • 依赖注入

比如:在服务提供者里面直接使用$this->applaravel在实例化服务提供器的时候,会把laravel容器实例注入到这个$app上面。所以我们在服务提供器里面,始终能通过$this->$app访问到laravel容器实例,而不需要再使用app()函数或者App Facade了。

abstract class ServiceProvider
{
    protected $app;
	...
}

$tis->app->singleton(...);

容器绑定

如果类没有依赖任何接口,就没有必要将类绑定到容器中。容器不需要指定如何构建这些对象,因为它可以使用反射自动解析这些对象。

在接口\Illuminate\Contracts\Container\Container中定义了容器绑定和解析的各种方法,接口Container存在一个实现类\Illuminate\Container\Container\Laravel\Lumen\Application继承了它的实现类,容器$app就是类Application的实现

下面先看看各个方法做了些什么事,最后看Lumen从入口进来是如何调用服务提供器的

简单绑定

前面说了我们在服务提供器里面可以通过 $this->app 属性访问容器。我们可以通过 bind 方法注册绑定,传递我们想要注册的类或接口名称再返回类的实例的 Closure
注意,我们接受容器本身作为解析器的参数。然后,我们可以使用容器来解析正在构建的对象的子依赖。

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

我们看看bind方法具体的做了些什么事

public function bind($abstract, $concrete = null, $shared = false)
{
    $this->dropStaleInstances($abstract);

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

    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

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

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

1、根据一个名称,删除已有的所有实例和别名,就是删除instance数组和aliases数组中已存在的值

protected function dropStaleInstances($abstract)
{
    unset($this->instances[$abstract], $this->aliases[$abstract]);
}

2、判断是否给了具体类型$concrete,没有的话把抽象类型$abstract赋值给具体类型$concrete
3、如果具体类型$concrete不是一个闭包,不是的话,则根据抽象类型$abstract和具体类型$concrete转成一个闭包

  • 抽象类型和具体类型相同,返回一个build的实例,实例化给定的具体实例
  • 不相同则返回makeWith的实例,从容器中解析出一个实例
protected function getClosure($abstract, $concrete)
{
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }

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

4、以$abstractkey将具体类型$concrete以及$shared的值绑定到bindings的成员数组中
5、判断$abstract是否被解析,已经被解析,存在实例,rebound一次?(不太懂这个操作,不知道对不对)

绑定接口到实现

服务容器有一个强大的功能,就是将接口绑定到给定实现。例如,如果我们有一个 EventPusher 接口和一个 RedisEventPusher 实现。编写完接口的 RedisEventPusher 实现后,我们就可以在服务容器中注册它,像这样:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

这么做相当于告诉容器:当一个类需要实现 EventPusher 时,应该注入 RedisEventPusher。现在我们就可以在构造函数或者任何其他通过服务容器注入依赖项的地方使用类型提示注入 EventPusher 接口:

use App\Contracts\EventPusher;

/**
 * 创建一个新的类实例
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

这个用法感觉还可以,但是一个接口也只是能绑定一个实现,其他要同一个接口,不同实现的,这个就没啥用了(laravel给出了上下文绑定的方式),但是对修改方便,实现一个新类,修改一下绑定就可以了(个人想法)

绑定一个单例

singleton 方法将类或接口绑定到只能解析一次的容器中。绑定的单例被解析后,相同的对象实例会在随后的调用中返回到容器中:

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

看看singleton方法中做了些什么事情

public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}

shared设置为true,然后调用build方法,参见上面

绑定实例

你也可以使用 instance 方法将现有对象实例绑定到容器中。给定的实例会始终在随后的调用中返回到容器中:

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

直接创建一个类对象,然后调用instance,看看instance()方法做了些什么

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);
    }
}

1、从上下文绑定的别名aliases中删除已存在的abstractAliases缓存
2、在bindingsinstancesaliases中查找是否存在抽象类型$abstract的绑定
3、删除aliases中的绑定
4、在instances上绑定该实例
5、根据第二步的判断,如果存在实例,rebound一次(又来了,不知道干嘛?)

绑定初始数据

当你有一个类不仅需要接受一个注入类,还需要注入一个基本值(比如整数)。你可以使用上下文绑定来轻松注入你的类需要的任何值

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

这种场景,我没有使用过,我们也大概看看他们是做些什么处理

public function when($concrete)
{
    return new ContextualBindingBuilder($this, $this->getAlias($concrete));
}

public function needs($abstract)
{
    $this->needs = $abstract;

    return $this;
}

public function give($implementation)
{
    $this->container->addContextualBinding(
        $this->concrete, $this->needs, $implementation
    );
}

1、when()函数里面实例化了一个ContextualBindingBuilder对象(上下文绑定对象),传入container对象以及在aliases数组最终的信息
2、needs()函数,对当前对象的成员变量needs赋值抽象类型$abstract
3、give()函数,调用container对象的addContextualBinding方法根据抽象类型和具体类型添加一个上下文绑定到contextual这个变量中,值为给定的变量(give里面的参数)

上下文绑定

有时候,你可能有两个类使用了相同的接口,但你希望每个类都能注入不同的实现。例如,两个控制器可能需要依赖不同的 Illuminate\Contracts\Filesystem\Filesystem 契约 实现。 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');
          });

函数调用和绑定初始数据一样

标记

有时候,你可能需要解析某个「分类」下的所有绑定。例如,你正在构建一个报表的聚合器,它接收一个包含不同 Report 接口实现的数组。注册了 Report 实现后,你可以使用 tag 方法为其分配标签:

$this->app->bind('SpeedReport', function () {
    //
});

$this->app->bind('MemoryReport', function () {
    //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

服务被标记后,你可以通过 tagged 方法轻松地将它们全部解析:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

tag函数将第一个参数的信息,绑定container的成员变量tags中;
tagged函数将这个标签中的所有抽象类型都实例化,返回。相当于给构造函数传了一个数组,数组内有很多不同的对象

绑定大概就是这么多方式了

服务解析

我现阶段的代码都是在Controlleruse xxFacaed,然后使用调用`````

make
public function make($abstract)
{
    $abstract = $this->getAlias($abstract);

    if (array_key_exists($abstract, $this->availableBindings) &&
        ! array_key_exists($this->availableBindings[$abstract], $this->ranServiceBinders)) {
        $this->{$method = $this->availableBindings[$abstract]}();

        $this->ranServiceBinders[$method] = true;
    }

    return parent::make($abstract);
}

1、获取抽象类型$abstract的最终别名
2、判断$abstract是否默认可用的绑定availableBindings,而且未执行,则先执行availableBindings中对应的方法
3、调用父类的make方法,该方法只有一句return $this->resolve($abstract);

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);

    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;
    }

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

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

    array_pop($this->with);

    return $object;
}

从容器中解析给定的类型:
1、获取最终的别名信息
2、判断是否需要绑定上下文,存在参数或者

依赖注入的实现

路由中的request就是在解析完所有的请求信息时,使用instance的方式绑定到容器的,后面不论在哪里调用request对象,都是同一个

后记

本文一些定义抄的地址
https://learnku.com/articles/4698/laravel-core-ioc-service-container
https://laravelacademy.org/post/769.html

你可能感兴趣的:(后端)