Laravel 是自動發現擴充套件包是怎樣實現的

Epona發表於2019-07-11

我們都知道,在 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 擴充套件包自動發現的原理了。

參考

There's nothing wrong with having a little fun.

相關文章