Laravel 自动发现扩展包是怎样实现的

我们都知道,在 Laravel5.5 版本以后加入了一项新功能——自动发现扩展包。简单来说就是之前我们在引入第三方扩展的时候需要在config/app.php中将对应的 Provider 和 Facade 进行注册,而在5.5之后我们就无需进行这样的操作了。下面我们分析一下它的实现原理。

composer脚本

那么什么是composer脚本呢?

一个脚本,在 Composer 中,可以是一个 PHP 回调(定义为静态方法)或任何命令行可执行的命令。脚本对于在 Composer 运行过程中,执行一个资源包的自定义代码或包专用命令是非常有用的。

—— 出自 Composer 文档

在 composer 执行过程中会触发一系列的事件(事件列表可以到(这里)查看),我们可以通过事件来触发对应的脚本。

我们在 Laravel 项目的 composer.json 文件中可以看到这样一段代码:

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

上面的代码表示当我们执行composer installcomposer update或者composer dump-autoload的时候就会触发数组内的这两个命令。

postAutoloadDump

这个命令很简单,清空缓存的服务和之前自动发现的扩展。代码如下:

    public static function postAutoloadDump(Event $event)
    {
        require_once $event->getComposer()->getConfig()->get('vendor-dir').'/autoload.php';

        static::clearCompiled();
    }

   protected static function clearCompiled()
    {
        $laravel = new Application(getcwd());

        if (file_exists($servicesPath = $laravel->getCachedServicesPath())) {
            @unlink($servicesPath);
        }

        if (file_exists($packagesPath = $laravel->getCachedPackagesPath())) {
            @unlink($packagesPath);
        }
    }

这个执行完毕之后接着执行php artisan package:discover命令。

扩展包发现

package:discover命令在Illuminate\Foundation\Console\PackageDiscoverCommand中,具体如下:

    use Illuminate\Foundation\PackageManifest;

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

这个方法调用了 Illuminate\Foundation\PackageManifest 类中的 build 方法。在这里 Laravel 处理扩展包的自动发现。

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

在这个方法中,Laravel 首先查找 vendor/composer/installed.json 文件,这个文件是由 composer 生成的,里面包含了一份完整的已经安装完成的扩展包信息,Laravel 在这个文件中查找所有包含 extra.laravel 的部分:

"extra": {
     "laravel": {
        "providers": [
            "BeyondCode\\DumpServer\\DumpServerServiceProvider"
        ]
    }
}

Laravel获取到包含有 extra.laravel 的内容之后,然后查找项目中composer.json中的extra.laravel.dont-discover部分,将里面的内容忽略掉。如果你想将所有的扩展包禁用自动发现,改为下面的配置即可, 即'*'的部分。

    "extra": {
        "laravel": {
            "dont-discover": ['*']
        }
    },

当内容收集完毕之后,Laravel 会将其写入到 bootstrap/cache/packages.php 文件:

<?php

return [
  'beyondcode/laravel-dump-server' => [
    'providers' => [
      0 => 'BeyondCode\\DumpServer\\DumpServerServiceProvider',
    ],
  ]
];

扩展包注册

Laravel使用了2个引导类来完成 Provider 和 Facade 的注册:

  • \Illuminate\Foundation\Bootstrap\RegisterFacades
  • \Illuminate\Foundation\Bootstrap\RegisterProviders

第一个使用了 Illuminate\Foundation\AliasLoader 来加载所需要的内容。其中包括packages.php文件,而针对这个文件的查找是通过 PackageManifest::aliases() 来处理的。

// in RegisterFacades::bootstrap()

AliasLoader::getInstance(array_merge(
    $app->make('config')->get('app.aliases', []),
    $app->make(PackageManifest::class)->aliases()
))->register();

与之类似, Provider 的处理方法则是通过 RegisterProviders 中的 bootstrap 方法。

    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }

其中调用了 Foundation\Application 中的 registerConfiguredProviders 方法:

    public function registerConfiguredProviders()
    {
        $providers = Collection::make($this->config['app.providers'])
                        ->partition(function ($provider) {
                            return Str::startsWith($provider, 'Illuminate\\');
                        });

        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }

这基本上就是 Laravel 扩展包自动发现的原理了。

参考

本作品采用《CC 协议》,转载必须注明作者和本文链接
There's nothing wrong with having a little fun.
本帖由系统于 3年前 自动加精
Epona
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 5

补充一下:即使把bootstrap/cache目录下的文件删掉,第一次访问Laravel时还是会自动生成,并不依赖于composer 脚本

3年前 评论

本地的扩展包不知道为什么死活不能自动加载,不会更新进去

3年前 评论
lufeijun1234

这么好的文章居然没什么点击量

4年前 评论

写的很清晰!点赞 :+1: :+1: :+1:

3年前 评论

清晰明了,赞

3个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!