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

# 延遲服務提供者

> 解說如何實作 DeferrableProvider 介面來延遲載入服務。這是套件效能最佳化中不起眼但相當重要的功能。

Laravel 啟動時註冊的服務提供者，即便在請求過程中一次都沒使用其功能，仍會每次被載入。延遲服務提供者（Deferred Service Provider）能解決此問題，將載入延後至服務實際被使用為止。

<Info>
  本頁以[套件開發基礎](/zh-TW/advanced/package-development)的知識為前提。建議先理解服務提供者的基本機制後再閱讀。
</Info>

## 為什麼需要延遲提供者

一般的服務提供者於每次請求都會呼叫 `register()` 與 `boot()`。像郵件、Queue、Cache 等並非每個頁面都使用的功能也都要每次初始化實屬浪費。

```php theme={null}
// 此提供者於每個請求都會呼叫 register()
class ReportServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // 每次都綁定沉重的報表服務
        $this->app->singleton(ReportGenerator::class, function ($app) {
            return new ReportGenerator(
                $app->make(PdfRenderer::class),
                $app->make(ChartRenderer::class),
                $app->make(DataExporter::class),
            );
        });
    }
}
```

將此提供者延遲化後，僅在實際產生報表的請求時才會被初始化。

***

## DeferrableProvider 介面

`Illuminate\Contracts\Support\DeferrableProvider` 是僅具備 `provides()` 方法的簡單介面。

```php theme={null}
namespace Illuminate\Contracts\Support;

interface DeferrableProvider
{
    public function provides();
}
```

僅需實作此介面，提供者便會進入延遲模式。

***

## 基本實作

延遲提供者的實作分為 3 個步驟。

<Steps>
  <Step title="implements DeferrableProvider">
    ```php theme={null}
    use Illuminate\Contracts\Support\DeferrableProvider;
    use Illuminate\Support\ServiceProvider;

    class ReportServiceProvider extends ServiceProvider implements DeferrableProvider
    {
        // ...
    }
    ```
  </Step>

  <Step title="於 register() 綁定服務">
    與一般提供者相同，於 `register()` 撰寫綁定。

    ```php theme={null}
    public function register(): void
    {
        $this->app->singleton(ReportGenerator::class, function ($app) {
            return new ReportGenerator(
                $app->make(PdfRenderer::class),
                $app->make(ChartRenderer::class),
                $app->make(DataExporter::class),
            );
        });

        $this->app->singleton(ReportRepository::class, function ($app) {
            return new ReportRepository($app->make('db'));
        });
    }
    ```
  </Step>

  <Step title="於 provides() 回傳所註冊的服務">
    `provides()` 必須回傳於 `register()` 綁定的**所有服務**。Laravel 會依此清單判斷「當要求哪些服務時要載入此提供者」。

    ```php theme={null}
    public function provides(): array
    {
        return [
            ReportGenerator::class,
            ReportRepository::class,
        ];
    }
    ```
  </Step>
</Steps>

***

## Service Manifest 的機制

Laravel 於啟動時會產生一個 `bootstrap/cache/services.php` 的 manifest 檔。此檔案中儲存了延遲提供者所提供的服務清單。

```php theme={null}
// bootstrap/cache/services.php 範例（自動產生）
return [
    'providers' => [
        // 與 bootstrap/providers.php 相同的清單
    ],
    'eager' => [
        // 立即載入的提供者
        App\Providers\AppServiceProvider::class,
    ],
    'deferred' => [
        // 服務名稱 => 提供者類別 的對應
        'App\Services\ReportGenerator' => ReportServiceProvider::class,
        'App\Repositories\ReportRepository' => ReportServiceProvider::class,
        'cache'     => Illuminate\Cache\CacheServiceProvider::class,
        'cache.store' => Illuminate\Cache\CacheServiceProvider::class,
        'queue'     => Illuminate\Queue\QueueServiceProvider::class,
    ],
    'when' => [],
];
```

透過此 manifest，Laravel 無需讀取檔案便能掌握「此服務由哪個提供者提供」。實際的提供者僅會在對應服務首次被解析時才會載入。

<Tip>
  若新增或變更提供者，請重新產生 manifest。

  ```shell theme={null}
  php artisan optimize:clear
  # 或
  php artisan clear-compiled
  ```
</Tip>

### 內部運作流程

```mermaid theme={null}
sequenceDiagram
    participant App as Laravel<br>應用程式
    participant Container as 服務<br>容器
    participant Manifest as services.php<br>manifest
    participant Provider as ReportService<br>Provider

    App->>Manifest: 啟動時載入 manifest
    Manifest-->>App: 回傳 deferred 服務清單
    Note over App: 此時提供者尚未載入

    App->>Container: $app->make(ReportGenerator::class)
    Container->>Manifest: 確認是否為 deferred 服務
    Manifest-->>Container: 由 ReportServiceProvider 提供
    Container->>Provider: 呼叫 register() + boot()
    Provider-->>Container: 註冊綁定
    Container-->>App: 回傳 ReportGenerator 的實例
```

***

## provides() 方法的重要性

若 `provides()` 中有**遺漏**，該服務將永遠無法被解析。

```php theme={null}
public function register(): void
{
    $this->app->singleton(ReportGenerator::class, fn ($app) => new ReportGenerator());

    // 新增的綁定
    $this->app->singleton('report', fn ($app) => $app->make(ReportGenerator::class));
}

public function provides(): array
{
    return [
        ReportGenerator::class,
        'report',  // ← 以字串 key 綁定的也不要忘了記載
    ];
}
```

使用 `$bindings` / `$singletons` 屬性時，也同樣要納入 `provides()`。

```php theme={null}
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public $singletons = [
        AnalyticsClient::class => DefaultAnalyticsClient::class,
    ];

    public function provides(): array
    {
        // 回傳 $singletons 列舉的所有 key
        return [
            AnalyticsClient::class,
        ];
    }
}
```

***

## 延遲提供者的限制

延遲提供者適合於**僅以向 Container 註冊綁定為目的**的提供者。以下這類在 `boot()` 中進行的作業，其提供者無法延遲化。

| 限制             | 原因                   |
| -------------- | -------------------- |
| 路由註冊           | 路由於應用程式啟動時解析，延遲則無法運作 |
| 全域中介層註冊        | 需於請求處理前完成註冊          |
| 事件監聽器註冊（常時需要者） | 事件觸發前若未註冊則無法運作       |
| 新增 Blade 指令    | 需於視圖編譯前註冊            |

<Warning>
  可以在延遲提供者中撰寫 `boot()` 方法，但其內容在服務被解析前不會執行。若將「總是需要的處理」如路由或中介層寫在 `boot()`，將導致預期外的行為。
</Warning>

***

## when() 方法 — 由事件觸發的註冊

使用 `when()` 方法，可在特定事件觸發時載入提供者。適用於 Job 處理系統等僅在特定情境下需要的提供者。

```php theme={null}
class ReportServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(ReportGenerator::class, fn () => new ReportGenerator());
    }

    public function provides(): array
    {
        return [ReportGenerator::class];
    }

    /**
     * 除了服務的綁定解析外，
     * 指定的事件觸發時也會載入提供者。
     */
    public function when(): array
    {
        return [
            \App\Events\ReportRequested::class,
        ];
    }
}
```

當 `when()` 中回傳的事件觸發時，即便服務未直接被解析，提供者也會被載入。

***

## 套件開發的活用

當作為第三方套件發布時，延遲提供者對使用者應用程式的效能也有幫助。

### 建議的模式

```php theme={null}
namespace Acme\Analytics;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/analytics.php', 'analytics');

        $this->app->singleton(AnalyticsManager::class, function ($app) {
            return new AnalyticsManager($app->make('config')->get('analytics'));
        });

        $this->app->singleton('analytics', fn ($app) => $app->make(AnalyticsManager::class));
    }

    public function boot(): void
    {
        // boot() 僅用於註冊 publishes
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../config/analytics.php' => config_path('analytics.php'),
            ], 'analytics-config');
        }
    }

    public function provides(): array
    {
        return [
            AnalyticsManager::class,
            'analytics',
        ];
    }
}
```

<Info>
  `mergeConfigFrom()` 內部會檢查設定是否已快取，因此於延遲提供者的 `register()` 中呼叫也是安全的。但若設定已被快取則不會執行任何事。
</Info>

### 以 `runningInConsole()` 分離 Artisan 指令註冊

因為指令註冊僅在 Artisan 啟動時需要，可以透過 `runningInConsole()` 進行條件判斷。但若要延遲提供指令的提供者，需將指令類別也納入 `provides()`，或另外準備專用於指令的提供者。

```php theme={null}
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            AnalyticsFlushCommand::class,
        ]);
    }
}
```

***

## Laravel Core 中的使用範例

Laravel Core 提供者中許多都是延遲提供者。這是為了不將每次請求不會用到的所有服務都立即載入的設計。

| 提供者                          | 提供的服務                                   |
| ---------------------------- | --------------------------------------- |
| `CacheServiceProvider`       | `cache`, `cache.store`, `RateLimiter`   |
| `QueueServiceProvider`       | `queue`, `queue.worker`, `queue.failer` |
| `MailServiceProvider`        | `mail.manager`, `mailer`                |
| `RedisServiceProvider`       | `redis`, `redis.connection`             |
| `HashServiceProvider`        | `hash`, `hash.driver`                   |
| `ValidationServiceProvider`  | `validator`, `validation.presence`      |
| `TranslationServiceProvider` | `translator`                            |
| `BroadcastServiceProvider`   | `Broadcast`                             |

於僅有 API 端點的應用程式中，`MailServiceProvider` 或 `BroadcastServiceProvider` 也可能在請求過程中一次都不被載入。

***

## 是否應延遲化的判斷基準

<AccordionGroup>
  <Accordion title="適合延遲化的服務">
    * 並非在所有請求中使用的服務（郵件、報表、外部 API Client 等）
    * 初始化時需要外部連線或讀取檔案的服務
    * 具有龐大物件圖的服務
    * 提供僅於 CLI 使用之指令的提供者
  </Accordion>

  <Accordion title="不適合延遲化的服務">
    * 註冊路由的提供者（如 `loadRoutesFrom` 等）
    * 註冊常時運作的中介層或例外處理器的提供者
    * 註冊 Eloquent Global Scope 或 Observer 的提供者
    * 於大多數請求都會被使用的輕量服務（延遲的額外負擔反而更大時）
  </Accordion>
</AccordionGroup>

***

## 相關頁面

<Columns cols={2}>
  <Card title="套件開發基礎" icon="box" href="/zh-TW/advanced/package-development">
    解說以服務提供者為核心的 Laravel 套件開發方式。
  </Card>

  <Card title="套件的版本相容性管理" icon="layers" href="/zh-TW/advanced/package-versioning">
    解說對應 Laravel 與 PHP 主版本更新的套件維護策略。
  </Card>
</Columns>


## Related topics

- [Laravel 套件開發](/zh-TW/advanced/package-development.md)
- [請求生命週期](/zh-TW/lifecycle.md)
- [GeneratorCommand — 自訂 make: 指令的實作](/zh-TW/advanced/generator-command.md)
- [Lottery 類別](/zh-TW/advanced/lottery.md)
- [Laravel 11 以後的新應用程式結構 FAQ](/zh-TW/advanced/app-structure-faq.md)
