Laravel容器与服务提供者

Laravel容器继承图:
laravel-container
这里的Illuminate\Container类实现了Illuminate\Contracts\Container\Container和ArrayAccess接口提供的方法,Illuminate\Foundation\Application实现了Illuminate\Contracts\Foundation\Application接口提供的方法。注:Illuminate\Contracts\Foundation\Application接口继承自Illuminate\Contracts\Container\Container,大体上从语义上,Application也必须是一个容器,要实现容器提供的方法。

在一个服务提供者中,可以通过$this->app变量来访问到应用容器。也可以在任何地方使用app()全局方法获取到。应用容器提供了很多方法,主要分两类:一类是基本容器方法 、一类是服务提供者容器方法。

Laravel中对于容器中的对象叫服务,一般可以认为是一个类名(或者是接口名,用接口名对应具体的类),由于类名可能比较长,所以引入了别名,别名就是一个映射关系。

一般情况,一个服务要可用,首先需要绑定到容器,容器根据绑定生成实例:

$app = app();

$app->bind(‘Ifeeline\Test’, function($app){
	return new Ifeeline\TestInstance();
});

当调用make()时就会取出绑定,然后生成实例。具体实现上就稍微复杂一些:

public function bind($abstract, $concrete = null, $shared = false)
{
    // If the given types are actually an array, we will assume an alias is being
    // defined and will grab this "real" abstract class name and register this
    // alias with the container so that it can be used as a shortcut for it.
    if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }

    // If no concrete type was given, we will simply set the concrete type to the
    // abstract type. This will allow concrete type to be registered as shared
    // without being forced to state their classes in both of the parameter.
    $this->dropStaleInstances($abstract);

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

    // If the factory is not a Closure, it means it is just a class name which is
    // bound into this container to the abstract type and we will just wrap it
    // up inside its own Closure to give us more convenience when extending.
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

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

    // If the abstract type was already resolved in this container we'll fire the
    // rebound listener so that any objects which have already gotten resolved
    // can have their copy of the object updated via the listener callbacks.
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

第一个参数可以是一个数组,用来指定类型的同时指定别名,举例:
Ifeeline\ExampleClass 对应别名为 example-class:

// 别名类似
// use XXX\YY as XX
$aliases[‘example-class’] = ‘Ifeeline\ExampleClass’;

$app->bind([‘Ifeeline\ExampleClass’ => ‘example-class’], function($app) {});

第二参数可以是空,也可以是字符串,或者是闭包、第三参数表明这个绑定生成的对象是否是共享的。

如果第二参数未提供,就是直接实例化类。如果提供了字符串,就是实例化这个类型的对象。如果是闭包,对象的实例化由该闭包完成。

对于希望用一个接口对应某个实现的情况:

$this->app->bind('接口', '接口实现类');

从实现上可以看到,一个bind()调用实际是产生如下内容:

$this->bindings[‘类名’] = [
‘concrete’ => ,
‘shared’ =>
];

其中concrete必定对应一个闭包函数,这个闭包函数第一个参数是容器实例,第二参数是生成实例时需要用到的参数,所以,如果自己传递一个闭包进去,可以这样:

$app->bind(‘类名’, function($app, $params) {}); 

如果调用bind()时第二参数不是闭包则会自动构建一个闭包函数,只是这个闭包函数第二参数为空而已。自动产生的闭包已经指定了如何产生对象,比如如果bind()未提供第二参数,就是直接调用$app->build()生成对象而已,如果提供了则使用$app->make()来生成对象。

容器的build和make方法是不同的,build()方法是根据反射来生成对象,具体来说是取出构造方法,然后解析构造方法的依赖,自动注入然后实例化对象。而make()方法直接从已有实例中返回实例,如果没有则从binding中取回concrete,由它来决定接下来是执行build方法还是make方法(递归)。总体来说,最终产生对象的,还是会调用到build方法。make方法最后还会根据binding中的shared来决定这个实例是否放入$this->instances,一旦放入这个数组,那么以后都用它。

如果需要产生一个单例绑定,可以使用$app->singlton(),它实际是bind()方法的封装而已。

如果希望把一个实例直接放入$instances数组,可以用$app->instance(‘类目’, new Object())。

如果要直接产生一个类的实例,不需要首先绑定,直接调用make即可。换句话说,绑定是在需要把接口对应到实现类,或者需要对生成的实例做配置时(传递闭包)才需要这样做。

不管信不信,对于一个容器,就只有这些内容。其它的都是一些扩展。比如绑定可以做所谓的上下文绑定,绑定分标签,容器事件等。

接下来就是需要知道在框架初始化的时候,绑定是怎么写入进来的,这个就是ServiceProvider的概念。

框架初始化流程:
实例化Application后,建设两个单例Kernel绑定到容器,然后调用Kernel的handle方法,这个方法最终会调用到Kernel的bootstrap()方法,这个方法最终把它自己的bootstrap数组(bootstrappers方法返回)传递到容器的bootstrapWith
方法,这个方法循环实例化从Kernel传递过来的bootstrap数组,然后调用其bootstrap方法,Kernel传递过来的bootstrap数组在其父类中(子类可覆盖):

//Illuminate\Foundation\Http\Kernel
protected $bootstrappers = [
    'Illuminate\Foundation\Bootstrap\DetectEnvironment',
    'Illuminate\Foundation\Bootstrap\LoadConfiguration',
    'Illuminate\Foundation\Bootstrap\ConfigureLogging',
    'Illuminate\Foundation\Bootstrap\HandleExceptions',
    'Illuminate\Foundation\Bootstrap\RegisterFacades',
    'Illuminate\Foundation\Bootstrap\RegisterProviders',
    'Illuminate\Foundation\Bootstrap\BootProviders',
];

//Illuminate\Foundation\Console\Kernel
protected $bootstrappers = [
    'Illuminate\Foundation\Bootstrap\DetectEnvironment',
    'Illuminate\Foundation\Bootstrap\LoadConfiguration',
    'Illuminate\Foundation\Bootstrap\ConfigureLogging',
    'Illuminate\Foundation\Bootstrap\HandleExceptions',
    'Illuminate\Foundation\Bootstrap\RegisterFacades',
    'Illuminate\Foundation\Bootstrap\SetRequestForConsole',
    'Illuminate\Foundation\Bootstrap\RegisterProviders',
    'Illuminate\Foundation\Bootstrap\BootProviders',
];

Console的Kernel多了一个SetRequestForConsole。这里主要关注Illuminate\Foundation\Bootstrap\RegisterProviders类:

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;

class RegisterProviders
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}

仅仅调用了容器的registerConfiguredProviders()方法,继续跟踪这个方法:

// Illuminate\Foundation\Application
public function registerConfiguredProviders()
{
    $manifestPath = $this->getCachedServicesPath();

    (new ProviderRepository($this, new Filesystem, $manifestPath))
                ->load($this->config['app.providers']);
}

这里的$this->getCachedServicesPath()返回bootstrap/cache/services.json路径,然后由Illuminate\Foundation\ProviderRepository类实例来加载ServiceProvider。

首先去检测bootstrap/cache/services.json文件是否存在,如果存在并且与$this->config[‘app.providers’]比较无变化则直接使用这个结果,否则就循环$this->config[‘app.providers’]:

foreach ($providers as $provider) {
//创建实例
    $instance = $this->createProvider($provider);

    // When recompiling the service manifest, we will spin through each of the
    // providers and check if it's a deferred provider or not. If so we'll
    // add it's provided services to the manifest and note the provider.
    if ($instance->isDeferred()) {
        foreach ($instance->provides() as $service) {
            $manifest['deferred'][$service] = $provider;
        }

        $manifest['when'][$provider] = $instance->when();
    }

    // If the service providers are not deferred, we will simply add it to an
    // array of eagerly loaded providers that will get registered on every
    // request to this application instead of "lazy" loading every time.
    else {
        $manifest['eager'][] = $provider;
    }
}

这里是判断服务提供者是否是延时的(就是用到是才实例化服务),如果是就调用provides()来获取延时服务,添加到$manifest[‘deferred’][$service]中,其值是服务提供者的名称,表示这个服务由它提供。并且调用when()方法,用来实现当某事件触发时,实例化这个服务提供者。最后把非延时的服务提供者放入$manifest[‘eager’]。

然后把$manifest数组保存到bootstrap/cache/services.json,下次就继续使用这个数组,看起来是这个样子:

{
    "providers": [
        "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider",
    ],
    "eager": [
        "Illuminate\\Auth\\AuthServiceProvider",
    ],
    "deferred": {
        "command.app.name": "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider",
    },
    "when": {
        "Illuminate\\Foundation\\Providers\\ArtisanServiceProvider": [],
    }
}

取回这个ServiceProvider数组后,会执行如下代码:

//Illuminate\Foundation\ProviderRepository 的load()方法
foreach ($manifest['when'] as $provider => $events) {
    $this->registerLoadEvents($provider, $events);
}

// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
    $this->app->register($this->createProvider($provider));
}

$this->app->addDeferredServices($manifest['deferred']);

循环when,注册when中注册的事件,实现当某时间触发时实例化此服务提供者并调用其register方法。然后循环调用eager中的服务提供者的register方法。最后把延时服务合并到容器的deferredServices中。

从这个过程可见,服务提供者的provides方法暴露了其提供的服务,对一个延时服务提供者来说是必须的。同样,为了实现某些事件触发时对应的延时服务可用,可以定义when方法。另外,如果一个服务提供者是延时的,那么它提供的所有服务都是延时的,实例化一个延时服务时,在同一个延时服务提供者提供的服务都会实例化,因为实例化一个延时服务实际调用服务提供者的register方法。

最后,服务提供者的boot方法被调用(延时服务提供者则在register后被调用)

关于延时服务的生成:

public function make($abstract, array $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    if (isset($this->deferredServices[$abstract])) {
        $this->loadDeferredProvider($abstract);
    }

    return parent::make($abstract, $parameters);
}

当make一个延时服务时,首先把服务提供者实例调用register方法,然后就跟一般的make无异。

服务提供者的register方法主要提供服务,往容器中注入绑定(不完全是)。举例如下:

protected function registerManager()
{
    $this->app->singleton('queue', function ($app) {
        // Once we have an instance of the queue manager, we will register the various
        // resolvers for the queue connectors. These connectors are responsible for
        // creating the classes that accept queue configs and instantiate queues.
        $manager = new QueueManager($app);

        $this->registerConnectors($manager);

        return $manager;
    });

    $this->app->singleton('queue.connection', function ($app) {
        return $app['queue']->connection();
    });
}

这样,如果需要queue实例,只需要$app->make(‘queue’)即可。

对应非延时服务,注册到容器中的仅仅是绑定,只有真需要的时候才会产生实例,避免了用不到也实例化。另外,对应延时服务,则是在用到时才实例化服务提供者,然后才开始做绑定,最后才实例化,所有对于使用概率较低的服务,可以把其变为延时服务。