【laravel 实现杂谈】ServiceProvider -- register/boot - (sunznx) 振翅飞翔
06 September 2019

写下本文的日期是 2019-09-07 。3 年前,我就听说 laravel 的大名了,那时候还没开始使用 laravel 就开始看 laravel 的文档。然而,看了半天还是毛都不懂,甚至一直打瞌睡。
我在 segmentfault 还问了新手问题 php - laravel 的 ServiceProvider 的 boot 方法和 providers 方法如何使用 - SegmentFault 思否 。 转眼 3 年过去了,虽然现在还是个菜鸡。

理解 ServiceProvider 的 register 和 boot

laravel 和 ServiceProvider 相关的启动器有两个 Bootstrap\RegisterProviders::classBootstrap\BootProviders::class

RegisterProviders 启动器主要用来注册服务,即调用 register 方法。
BootProviders 启动器的功能我们也能猜出来,是用来启动服务的,即调用 boot 方法。

用 “伪代码” 来看如下:

# RegisterProviders
foreach ($this->serviceProviders as $provider) {
    $provider->register();
}

# BootProviders
foreach ($this->serviceProviders as $provider) {
    $provider->boot();
}

这里有一个需要注意的地方,laravel 是先在 RegisterProviders 遍历了所有 ServiceProvider 的 register 方法,然后在 BootProviders 遍历了所有 ServiceProvider 的 boot 方法。为什么不采用下面这种方式来加载服务:

foreach ($this->serviceProviders as $provider) {
    $provider->register();
    $provider->boot();
}

假设我们有一个 AService 依赖 BService,我们 BService 没有注册,直接调用 AService 的 boot 方法,那么狠显然就会有问题。因此,只能先把所有的服务先注册了,再启动,这时候 BService 一定是注册过了的

懒加载

在上面提到的服务依赖中,假设 AService 没有被任何服务依赖(通常我们自己写的 Providers 都不会被其他服务依赖),那么我们是可以做到延迟注册的。延迟注册只是很简单的在 RegisterProviders 和 BootProviders 调用完了之后,再把需要延迟注册的服务 (register+boot) 一次,伪代码如下:

foreach ($this->deferredServices as $provider) {
    $provider->register();
    $provider->boot();
}

关于懒加载,这边还有一个小插曲。在 Illuminate/Database/DatabaseServiceProvider.php 我们可以看到如下注册信息:

$this->app->singleton('db.factory', function ($app) {
    return new ConnectionFactory($app);
});

$this->app->singleton('db', function ($app) {
    return new DatabaseManager($app, $app['db.factory']);
});

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

为什么 laravel 不注册成下面这种

$this->app->instance('db.factory', new ConnectionFactory($this->app));
$this->app->instance('db', new DatabaseManager($this->app, $this->app['db.factory']));
$this->app->instance('db.connection', $this->app['db']->connection());

如果采用 后者 ,那么 生成的 db, db.factory, db.connection 是实时生成的,而通过一个函数包裹着的方式,是惰性的。这和函数式变成的 惰性计算 的思想有点类似。举一个工作中遇到的例子,来展示下这种方式的巧妙之处。

总所周知,laravel 的配置文件在 .env 文件里面,然后为了所谓 “安全” 起见,运维要求我们对于 .env 敏感信息需要加密的,例如:

DB_USERNAME=eyJpdiI6IjlMQ2tRWDFKcWRoN...
DB_PASSWORD=eyJpdiI6Iko3cTZWRV...

假设 laravel 框架不是采用 惰性加载 的方式,那么只能通过修改框架源码的方式(修改 connection() 获取 config 的方式)来实现了。因为有个 惰性加载 ,那么我可以新建一个 Provider,如下:

class DecryptDBInfoServiceProvider extends ServiceProvider
{
    public function register()
    {
        \config([
            'database.connections.mysql.password' => app('encrypter')->decrypt(env('DB_PASSWORD', '')),
            'database.connections.mysql.username' => app('encrypter')->decrypt(env('DB_USERNAME', ''))
        ]);
    }
}

在这件事上,我已经对 laravel 顶礼膜拜了…

deferredServices

laravel 是如何判断一个 ServiceProvider 是否是延迟注册的?首先看看如下两段代码

// Illuminate/Foundation/ProviderRepository.php
protected function compileManifest($providers)
{
    foreach ($providers as $provider) {
        $instance = $this->createProvider($provider);

        if ($instance->isDeferred()) {
            foreach ($instance->provides() as $service) {
                $manifest['deferred'][$service] = $provider;
            }

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

        ...
    }
    ...
}

// Illuminate/Support/ServiceProvider.php
public function isDeferred()
{
    return $this instanceof DeferrableProvider;
}

因此,一个 deferred 的 ServiceProvider 就必须满足以下几个条件:

  1. implements DeferrableProvider
  2. provides() 方法需要声明自己提供了哪些 service (如果这个方法返回为空,那么也不算是 deferred ServiceProvider)
  3. 最好是调用 php artisan clear-compiled 或者 rm bootstrap/cache/packages.php bootstrap/cache/services.php ,因为这两个文件会有缓存