【laravel 实现杂谈】Package Discovery - (sunznx) 振翅飞翔
06 September 2019

在讲 Package Discovery 的实现之前,先讲下这个东西是什么。

在没有 Package Discovery 的时候,在 laravel 里面,要想使用一个 ServiceProvider,就先要在 config/app.php'providers' 里面 或者 在 AppServiceProvider 加上对应的配置,如下:

# option 1
'providers' => [
    ...
    \Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class
],

# option 2
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
    }
}

有了 Package Discovery 之后,作为 package 开发者,假如我写了一个 package,并且在 composer.json 添加类似如下内容,那么 laravel 就会自动实现注册,而不需要手动再去注册

{
    ...

    "extra": {
        "laravel": {
            "providers": [
                "Barryvdh\\Debugbar\\ServiceProvider"
            ],
            "aliases": {
                "Debugbar": "Barryvdh\\Debugbar\\Facade"
            }
        }
    }
}

当然,你会说这实际上没什么暖用啊。事实上,我也是这么想的。但是竟然有了,我们就看看他是怎么实现的。好奇一下…

laravel 实现 Package Discovery 的相关代码在 Illuminate/Foundation/PackageManifest.php 里面,PackageManifest 这个 class 在 容器启动的时候就会有如下的配置:

protected function registerBaseBindings()
{
    ...

    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}

public function getCachedPackagesPath()
{
    return Env::get('APP_PACKAGES_CACHE', $this->bootstrapPath().'/cache/packages.php');
}

实际上,结合 PackageManifest 的构造函数,我们实际上就可以 YY 一波 laravel 是怎么实现这个功能的了

class PackageManifest
{
    ...

    public function __construct(Filesystem $files, $basePath, $manifestPath)
    {
        $this->files = $files;
        $this->basePath = $basePath;
        $this->manifestPath = $manifestPath;
        $this->vendorPath = $basePath.'/vendor';
    }
}

PackageManifest 的关键方法是 build

public function build()
{
    $packages = [];

    if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) {
        $packages = json_decode($this->files->get($path), true);
    }

    $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore());

    $this->write(collect($packages)->mapWithKeys(function ($package) {
        return [$this->format($package['name']) => $package['extra']['laravel'] ?? []];
    })->each(function ($configuration) use (&$ignore) {
        $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []);
    })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) {
        return $ignoreAll || in_array($package, $ignore);
    })->filter()->all());
}

build 方法首先会读取 composer/installed.json ,内容大概如下:

[
    {
        "name": "barryvdh/laravel-ide-helper",
        "version": "dev-master",
        "version_normalized": "9999999-dev",
        ...
         "extra": {
            "branch-alias": {
                "dev-master": "2.6-dev"
            },
            "laravel": {
                "providers": [
                    "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
                ]
            }
        },
        ...
    },
    {
        "name": "barryvdh/reflection-docblock",
        "version": "v2.0.6",
        ...
    },
    ...
]

最后,会将读取的内容生成一份缓存文件放在 manifestPath(即 bootstrap/cache/packages.php) 里面,内容大概如下:

<?php return array (
  'barryvdh/laravel-ide-helper' => array (
    'providers' => array (
      0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
    ),
  ),
  'fideloper/proxy' => array (
    'providers' => array (
      0 => 'Fideloper\\Proxy\\TrustedProxyServiceProvider',
    ),
  ),
  ...
);

laravel 在启动的时候会读取这个文件(如果不存在会重新创建),然后根据这个文件的内容去注册 ServiceProvider

为了实现 这些 cache/packages.php 文件的更新,laravel 会在 composer.json 的 scripts 的 post-autoload-dump 里面挂了钩子

post-autoload-dump: occurs after the autoloader has been dumped, either during install/update, or via the dump-autoload command.

"scripts": {
    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
        "@php artisan package:discover --ansi",
        "php artisan here"
    ]
}

package:discover 命令对应的代码在 Illuminate/Foundation/Console/PackageDiscoverCommand.php 里面,实际上也是调用了 build

public function handle(PackageManifest $manifest)
{
    $manifest->build();

    foreach (array_keys($manifest->manifest) as $package) {
        $this->line("Discovered Package: <info>{$package}</info>");
    }

    $this->info('Package manifest generated successfully.');
}