musicode / test

test
14 stars 1 forks source link

[Laravel] ServiceProvider #13

Open musicode opened 9 years ago

musicode commented 9 years ago

在 app/config/app.php 中,配置了一个 providers 数组,这包含了所有已注册的 ServiceProvider。

初始化相关的代码有两行:

$providers = $config['providers'];
$app->getProviderRepository()->load($app, $providers);

可见逻辑分两步:

  1. 实例化 ProviderRepository
  2. 加载已配置的 provider

    ServiceProvider

ServiceProvider 是提供某类服务的对象,具体可参考 Laravel 文档

我们分析一下 Illuminate\Log\LogServiceProvider,这个类比较简单,很适合拿来学习。

use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider { }

所有的 ServiceProvider 都继承自 ServiceProvider 抽象类。这个类有两个属性:

比较重要的方法有以下这些:

除了 isDeferred 比较直白以外,其他方法需要更加详细的介绍。

register

前面提过,ProviderRepository 对象会调用 load 方法批量注册 ServiceProvider,其中核心步骤是 $app->register($provider),在 app 对象的 register 方法中,会调用 ServiceProvider 的 register 方法。

register 是 ServiceProvider 类唯一的抽象方法,所以在子类中必须实现它,我们可以做一些初始化的操作,最常见的就是把组件注册到 IoC 容器。

Laravel 文档的建议如下:

如果您的应用程序有很多 IoC 绑定,或是想要分门别类,在不同文件组织绑定,您可以注册绑定在 服务提供者。

commands

commands 是一个已实现的方法,可以通过它把 Artisan 命令注册到 Artisan console 实例中。

provides

当 defer 属性为 true 时,provides 才有意义。

对于延迟加载的 ServiceProvider,app 对象可以通过 loadDeferredProvider 方法手动加载,这个方法需要一个 service 参数,即通过需要的服务名称加载对应的 ServiceProvider。

所以当我们描述一个 ServiceProvider 时,也需要给出它对外提供的服务名称,这个名称必须是全局唯一的,所以有类似命名空间的设计,如下:

public function provides()
{
    return array('log', 'Psr\Log\LoggerInterface');
}

LogServiceProvider 采用的是以 \ 作为分隔符的形式,从整个框架来看,大部分采用的是以 . 作为分隔符的形式。

when

provides 一样,也是当 defer 属性为 true 时才有意义。

我们来看下 ProviderRepository 的 registerLoadEvents 方法,大概就知道 when 的作用了。

protected function registerLoadEvents(Application $app, $provider, array $events)
{
    if (count($events) < 1) return;

    $app->make('events')->listen($events, function() use ($app, $provider)
    {
        $app->register($provider);
    });
}

也就是全局监听 when 事件数组,一旦触发事件就注册 ServiceProvider。

package

介绍 package 之前,先介绍一个辅助方法 guessPackagePath。

package 的 ServiceProvider 路径一般是 vender/vendorName/packageName/src/ServiceProvider.php,如果要猜 package 的路径,最简单的方式就是 ../..guessPackagePath 正是这样实现的。

package 用于注册包,比如包的配置,模板等,方法签名如下:

public function package($package, $namespace = null, $path = null)

常见的用法为 package('package/namespace'),这里分析一下 package 的执行步骤:

  1. 把 "package/namespace" 解析成 package 和 namespace
  2. 获取 package 的 path,如果没传,则通过 guessPackagePath 方法猜测
  3. 判断 package 目录下是否有 config 目录,如果有,注册到 Config 对象
  4. 判断 package 目录下是否有 lang 目录,如果有,注册到 Translator 对象
  5. 判断项目模板目录(app/views)下是否有 packages/{package} 目录,如果有,注册为模板的命名空间
  6. 判断 package 目录下是否有 views 目录,如果有,注册为模板的命名空间

其中第 2、3、5、6 步较难理解,我们会在 Config、Translator、View 章节介绍,这里就不展开了。

ProviderRepository

ProviderRepository 看起来像是 ServiceProvider 的容器,但实际上,它更多的是在维护一份配置。

通过上一节我们知道,每个 ServiceProvider 对象都有三种特性,如下:

所有已注册的 ServiceProvider 对象可在 app/config/app.php 找到,即使是核心组件,数量也超过了 10 个,如何管理这么多对象呢?

Laravel 为此设计了一份全量配置,格式如下:

{
    "providers": [ ],
    "eager": [ ],
    "deferred": { },
    "when": { }
}

这份配置以 json 文件的格式保存在硬盘上(app/storage/meta/services.json),当应用初始化时,首先会检查硬盘是否有该文件,并且要求该文件的配置和 app/config/app.php 保持一致,如果不符合要求,会以 php 文件为准,更新 json 文件。

然后,根据 eager、deffered、when 的配置各自处理,比如监听 when 事件等,具体逻辑在介绍 ServiceProvider 时大致讲过,这里就不再赘述。

值得一提的是,ProviderRepository 并非是 ServiceProvider 的容器,说白了它只做两件事:

  1. 维护配置
  2. 应用配置

所谓 “容器” 的职责,最后还是交给了 IoC 容器,即 app 对象。