> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 套件自動偵測的內部結構

> 解讀 PackageManifest 類別的內部實作，說明 composer.json 的 extra.laravel 如何被快取，並以 bootstrap/cache/packages.php 的形式讀取。

## 關於本頁

在[套件開發基礎](/zh-TW/advanced/package-development)中介紹了只要在 `composer.json` 撰寫 `extra.laravel` 段落，服務提供者與 Facade 便會自動註冊。本頁將於原始碼層面解說其背後 `Illuminate\Foundation\PackageManifest` 類別的運作方式。

<Info>
  本頁為[套件開發基礎](/zh-TW/advanced/package-development)的姊妹頁。建議先閱讀自動偵測的基本使用方式。
</Info>

## 自動偵測機制的整體樣貌

```mermaid theme={null}
flowchart TD
    A["composer install / update"] --> B["Composer 觸發 post-autoload-dump<br>事件"]
    B --> C["Illuminate\\Foundation\\ComposerScripts::postAutoloadDump()"]
    C --> D["刪除 bootstrap/cache/packages.php 等<br>快取檔案"]
    D --> E["下次啟動時執行 PackageManifest::build()"]
    E --> F["讀取 vendor/composer/installed.json"]
    F --> G["蒐集各套件的 extra.laravel"]
    G --> H["dont-discover 的排除處理"]
    H --> I["寫入 bootstrap/cache/packages.php"]
    I --> J["Application 讀取 providers()/aliases()<br>並註冊服務提供者"]
```

## `PackageManifest` 類別

自動偵測的核心是 `Illuminate\Foundation\PackageManifest`。以下為框架 13.x 時的實作（摘要）。

```php theme={null}
class PackageManifest
{
    public function providers()
    {
        return $this->config('providers');
    }

    public function aliases()
    {
        return $this->config('aliases');
    }

    public function config($key)
    {
        return (new Collection($this->getManifest()))
            ->flatMap(fn ($configuration) => (array) ($configuration[$key] ?? []))
            ->filter()
            ->all();
    }

    protected function getManifest()
    {
        if (! is_null($this->manifest)) {
            return $this->manifest;
        }

        if (! is_file($this->manifestPath)) {
            $this->build();
        }

        return $this->manifest = is_file($this->manifestPath)
            ? $this->files->getRequire($this->manifestPath)
            : [];
    }
}
```

重點有以下 3 個。

* **manifest 讀入一次後會快取於記憶體**（`$this->manifest` 屬性）。單一請求內即便多次呼叫 `providers()` 也僅有 1 次 file I/O。
* **僅在 manifest 檔案不存在時執行 `build()`**。一般營運中並非每次都會建置。
* 實體只是 `return` 一個樸素 PHP 陣列的檔案（`bootstrap/cache/packages.php`），僅需 `require` 即可以最快的形式讀取。

## Manifest 的建置處理

`build()` 方法為實際彙整 `composer.json` 資訊的部分。

```php theme={null}
public function build()
{
    $packages = [];

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

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

    $this->write((new Collection($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());
}
```

重要之處是並非直接 parse `composer.json`，而是讀取 **`vendor/composer/installed.json`**。這是 Composer 於執行 `composer install` / `composer update` 時產生的、包含所有已安裝套件 metadata 的檔案。因為各套件 `composer.json` 中的 `extra` 段落已彙整於此，Laravel 端只信任並讀取 Composer 管轄下的資訊。

<Info>
  `installed.json` 一般會被 `.gitignore`，因此於初次設定時（尚無 `vendor/` 之狀態）自動偵測無法運作。要於 `composer install` 完成後才會首次建置快取。
</Info>

### `dont-discover` 的兩種寫法

`dont-discover` 於套件端或應用程式端的 `composer.json` 皆可撰寫，但意義不同。

```json title="套件端的 composer.json" theme={null}
"extra": {
    "laravel": {
        "providers": ["Acme\\Courier\\CourierServiceProvider"],
        "dont-discover": []
    }
}
```

```json title="應用程式端的 composer.json" theme={null}
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}
```

觀察 `build()` 的實作，`$ignore` 陣列是將各套件的 `configuration['dont-discover']` 以 `array_merge` 累積。因此技術上，套件本身也可以「停用自身相依套件的自動偵測」（例如：不希望內部使用的子套件的 Provider 重複註冊時）。但實務上較常使用的仍是應用程式端的停用。

### 於 `dont-discover` 指定 `*`

若 `packagesToIgnore()` 回傳的陣列中包含 `*`，則 `$ignoreAll = true`，會**將所有套件的自動偵測整體停用**。可用於 CI 或測試環境希望避免自動偵測的額外負擔，或希望於 `bootstrap/providers.php` 完全手動管理的情境。

```json theme={null}
"extra": {
    "laravel": {
        "dont-discover": ["*"]
    }
}
```

## 快取檔案的實體

`getCachedPackagesPath()` 若有環境變數 `APP_PACKAGES_CACHE` 則回傳其值，否則回傳 `bootstrap/cache/packages.php`。

```php theme={null}
public function getCachedPackagesPath()
{
    return $this->normalizeCachePath('APP_PACKAGES_CACHE', 'cache/packages.php');
}
```

直接開啟此檔案，可以看到只是 `return` 一個單純的關聯陣列。

```php theme={null}
<?php return array (
  'acme/courier' => 
  array (
    'providers' => 
    array (
      0 => 'Acme\\Courier\\CourierServiceProvider',
    ),
  ),
);
```

套件名稱作為 key，因此可透過 `php artisan package:discover` 的輸出確認「偵測到哪些套件」。

## 快取被重建的時機

`Illuminate\Foundation\ComposerScripts` 掛勾 3 個 Composer 事件，皆呼叫相同的 `clearCompiled()`。

```php theme={null}
public static function postInstall(Event $event)
{
    require_once $event->getComposer()->getConfig()->get('vendor-dir').'/autoload.php';
    static::clearCompiled();
}

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

    if (is_file($configPath = $laravel->getCachedConfigPath())) {
        @unlink($configPath);
    }

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

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

也就是說，不論執行 `composer install` / `composer update` / `composer dump-autoload` 任一者，設定快取、服務快取、套件快取都會一併被刪除。下次 Laravel 啟動時 `PackageManifest::build()` 便會執行，從 `installed.json` 的最新狀態重新建置。

這是標準 Laravel 專案於 `composer.json` 的 `scripts` 中所註冊的行為。

```json title="Laravel 應用程式的 composer.json（節錄）" theme={null}
"scripts": {
    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
    ],
    "post-update-cmd": [
        "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
    ],
    "post-root-package-install": [
        "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
    ],
    "post-create-project-cmd": [
        "@php artisan key:generate --ansi"
    ]
}
```

## 以 `php artisan package:discover` 手動重建

當快取仍為舊資料，或未經 Composer 直接變更 `vendor/` 的情況下，可透過 `package:discover` 指令手動重建。

```shell theme={null}
php artisan package:discover
```

此指令的實體是非常薄的 wrapper。

```php theme={null}
#[AsCommand(name: 'package:discover')]
class PackageDiscoverCommand extends Command
{
    public function handle(PackageManifest $manifest)
    {
        $this->components->info('Discovering packages');

        $manifest->build();

        (new Collection($manifest->manifest))
            ->keys()
            ->each(fn ($description) => $this->components->task($description))
            ->whenNotEmpty(fn () => $this->newLine());
    }
}
```

僅呼叫 `$manifest->build()` 並輸出結果，幾乎不含指令專屬邏輯。於 CI Pipeline 中使用 `composer install --no-scripts` 等 Composer 事件不會觸發的情況下，需明確呼叫此指令。

<Warning>
  於套件測試中使用 [Orchestra Testbench](/zh-TW/advanced/package-testing) 時，Testbench 提供自身的 `vendor/bin/testbench package:discover` 指令。詳情請參考[以 Testbench 進行套件測試](/zh-TW/advanced/package-testing)。與 Artisan 的 `package:discover` 為不同物，是為 Testbench 專用的骨架應用程式建置 manifest。
</Warning>

## 部署時的注意事項

正式部署時通常執行 `composer install --no-dev --optimize-autoloader`，但若加上 `--no-scripts`，則套件快取不會被更新。建議於部署腳本中加入明確的重建步驟以保安全。

```shell theme={null}
composer install --no-dev --optimize-autoloader --no-scripts
php artisan package:discover --ansi
php artisan config:cache
php artisan route:cache
```

順序也很重要。`package:discover` 需先於 `config:cache` 執行。若設定快取先被建立，後才加入的套件設定（如以 `mergeConfigFrom` 註冊的設定）可能無法反映。

## 總結

* 自動偵測並非依 `composer.json` 的靜態內容，而是讀取 Composer 產生的 `vendor/composer/installed.json` 運作。
* 結果以純 PHP 陣列快取於 `bootstrap/cache/packages.php`，僅需 `require` 即可高速讀取。
* 快取於 `composer install/update/dump-autoload` 各事件會自動被刪除，並於下次啟動時重新建置。
* 使用 `--no-scripts` 執行 Composer 的環境中，需明確呼叫 `php artisan package:discover`。
* 於 `dont-discover` 指定 `*` 可整體停用自動偵測。

## 相關頁面

* [套件開發基礎](/zh-TW/advanced/package-development) — 服務提供者與 `extra.laravel` 的基本使用方式
* [延遲服務提供者](/zh-TW/advanced/deferred-provider) — 最佳化已偵測提供者的載入時機
* [以 Orchestra Testbench 進行套件測試](/zh-TW/advanced/package-testing) — Testbench 專屬的 `package:discover` 指令


## Related topics

- [Laravel 套件開發](/zh-TW/advanced/package-development.md)
- [以 Testbench Workbench 推進套件開發](/zh-TW/advanced/package-workbench.md)
- [Laravel 11 以後的應用程式結構](/zh-TW/advanced/app-structure.md)
- [自訂驗證 Guard 的實作](/zh-TW/advanced/custom-auth-guard.md)
- [引擎 API 整合應用開發指南 - VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/app-guide.md)
