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

# Service Container

> 說明使用 Laravel service container 進行依賴注入的機制與 binding 的基礎。

## 什麼是 Service Container

Laravel 的 service container 是管理類別依賴並進行依賴注入的機制。所謂依賴注入，是指透過建構子（或有時透過 setter 方法）將類別所需的依賴「注入」到類別中。

看以下例子：

```php theme={null}
<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;
use Illuminate\View\View;

class PodcastController extends Controller
{
    /**
     * 建立新的 controller 實例
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * 顯示指定 podcast 的資訊
     */
    public function show(string $id): View
    {
        return view('podcasts.show', [
            'podcast' => $this->apple->findPodcast($id)
        ]);
    }
}
```

此例中，`PodcastController` 需要從 Apple Music 等資料來源取得 Podcast。因此我們**注入**能取得 Podcast 的服務。透過注入服務，測試時能輕易換成 `AppleMusic` 服務的 mock（假實作）。

<Info>
  深入理解 service container 對於建構大型 Laravel 應用不可或缺，也對貢獻 Laravel 核心有所幫助。
</Info>

```mermaid theme={null}
flowchart TD
    A["Service Provider<br>register()"] --> B["Service Container<br>登記 binding"]
    B --> C{"解析請求<br>make() / 自動注入"}
    C -- "具體類別" --> D["以 reflection<br>自動解析"]
    C -- "介面" --> E["解析已登記的<br>實作類別"]
    D --> F["建立實例<br>注入到建構子"]
    E --> F
```

## 零設定解析

若類別只依賴其他具體類別（非介面），無需告訴 container 如何解析。例如在 `routes/web.php` 寫下：

```php theme={null}
<?php

class Service
{
    // ...
}

Route::get('/', function (Service $service) {
    dd($service::class);
});
```

<Info>
  此例在路由檔中定義類別僅為示範。實際應用中，service 類別應定義於 `app/Services` 目錄。
</Info>

存取此路由時，Laravel 會自動解析 `Service` 類別並注入到路由 handler。無需設定檔即可享受依賴注入。

controller、event listener、middleware 等 Laravel 應用中撰寫的多數類別，其依賴都會透過 container 自動注入。

## Binding

### 基本 binding

多數 binding 會在[service provider](/zh-TW/service-providers)中登記。在 service provider 內可透過 `$this->app` 屬性存取 container。

#### bind

用 `bind` 方法傳入類別或介面名稱與 closure 來登記 binding。

```php theme={null}
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;

$this->app->bind(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
```

Closure 的引數會收到 container 本身。可用它解析子依賴。

若要在 service provider 外操作 container，使用 `App` facade。

```php theme={null}
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;

App::bind(Transistor::class, function (Application $app) {
    // ...
});
```

<Info>
  不依賴介面的類別不需要 bind 到 container。container 會以 reflection 自動解析這些物件。
</Info>

#### singleton

`singleton` 方法會將類別或介面 bind 為只解析一次。一旦解析出的 singleton 之後每次向 container 呼叫都會回傳同一個實例。

```php theme={null}
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;

$this->app->singleton(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
```

使用 `singletonIf` 方法可以只在該型別尚未登記 binding 時才登記 singleton binding。

```php theme={null}
$this->app->singletonIf(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
```

#### Singleton 屬性

也可以在類別或介面加上 `#[Singleton]` 屬性，指示 container 只解析一次。

```php theme={null}
<?php

namespace App\Services;

use Illuminate\Container\Attributes\Singleton;

#[Singleton]
class Transistor
{
    // ...
}
```

#### Scoped singleton binding

`scoped` 方法將類別或介面 bind 為在 Laravel 請求／job 生命週期內只解析一次。與 `singleton` 類似，但以 `scoped` 登記的實例會在 Laravel 應用開始新的「生命週期」時被銷毀，例如 [Laravel Octane](/zh-TW/octane) worker 處理新請求，或 [queue worker](/zh-TW/queues) 處理新 job 時。

```php theme={null}
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;

$this->app->scoped(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
```

使用 `scopedIf` 方法可以只在該型別尚未登記 binding 時才登記 scoped binding。

```php theme={null}
$this->app->scopedIf(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
```

#### Scoped 屬性

也可以在類別或介面加上 `#[Scoped]` 屬性，指示 container 在請求／job 生命週期內只解析一次。

```php theme={null}
<?php

namespace App\Services;

use Illuminate\Container\Attributes\Scoped;

#[Scoped]
class Transistor
{
    // ...
}
```

#### instance

也可用 `instance` 方法將既有物件實例 bind 到 container。之後每次呼叫 container 都會回傳該實例。

```php theme={null}
use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);
```

### 將介面 bind 到實作

service container 強大功能之一是可以將介面 bind 到特定實作。例如有 `EventPusher` 介面與 `RedisEventPusher` 實作：

```php theme={null}
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);
```

如此，container 會為需要 `EventPusher` 實作的類別注入 `RedisEventPusher`。之後只需在建構子 type-hint `EventPusher` 介面即可。

```php theme={null}
use App\Contracts\EventPusher;

/**
 * 建立新的類別實例
 */
public function __construct(
    protected EventPusher $pusher,
) {}
```

<Tip>
  依賴介面就算換掉實作也不需改動程式碼。使測試與未來變更更容易。
</Tip>

#### Bind 屬性

Laravel 還提供更方便的 `Bind` 屬性。在介面加上此屬性，就可以告訴 Laravel 當該介面被要求時要自動注入哪個實作。使用 `Bind` 屬性時，service provider 中無需再登記。

還可以在介面放多個 `Bind` 屬性，依環境注入不同實作。

```php theme={null}
<?php

namespace App\Contracts;

use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;

#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher
{
    // ...
}
```

依任意條件的 binding，可用 `BindWhen` 屬性。closure 會收到 container，套用該 binding 時回傳 `true`。`Bind` 與 `BindWhen` 屬性會依宣告順序評估。

```php theme={null}
use App\Services\BetaEventPusher;
use Illuminate\Container\Attributes\BindWhen;
use Laravel\Pennant\Feature;

#[BindWhen(BetaEventPusher::class, static fn () => Feature::active('beta-events'))]
interface EventPusher
{
    // ...
}
```

<Info>
  使用 `BindWhen` 屬性需要 PHP 8.5 以上。
</Info>

也可與 [Singleton](#singleton-屬性) 或 [Scoped](#scoped-屬性) 屬性併用，指定該 container binding 是否只解析一次、或每個請求／job 只解析一次。

```php theme={null}
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Singleton;

#[Bind(RedisEventPusher::class)]
#[Singleton]
interface EventPusher
{
    // ...
}
```

## 自動解析（以 type-hint 進行 DI）

service container 在解析 controller、event listener、middleware 等類別時，會查看建構子的 type-hint 自動注入依賴。

```php theme={null}
<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
    /**
     * 建立新的 controller 實例
     */
    public function __construct(
        protected UserRepository $users,
    ) {}
}
```

若 `UserRepository` 不依賴介面，就不用登記到 container。只要存取路由，container 就會自動解析依賴並注入到 controller。

## 從 Container 解析

### make 方法

用 `make` 方法可從 container 解析類別實例。

```php theme={null}
use App\Services\Transistor;

$transistor = app()->make(Transistor::class);
```

若類別的依賴無法由 container 解析，也可用 `makeWith` 方法傳入額外引數。

```php theme={null}
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
```

### 自動注入

實際上，很少直接呼叫 `make`。只要在 container 會解析的類別（controller、event listener、middleware 等）建構子加上 type-hint，container 就會自動注入。

## Facade 與 Container 的關係

Laravel 的 facade 為 container 內的物件提供靜態介面。例如 `Cache::get()` 內部會從 container 取得 `Cache` 服務再呼叫。

```php theme={null}
use Illuminate\Support\Facades\Cache;

// 使用 facade 呼叫
Cache::get('key');

// 直接使用 container 的等效呼叫
app('cache')->get('key');
```

Facade 是 container 的便利包裝。測試時可將 facade 替換為 mock。

```php theme={null}
use Illuminate\Support\Facades\Cache;

Cache::shouldReceive('get')
    ->once()
    ->with('key')
    ->andReturn('value');
```

## 建構子注入的實務範例

看實際應用中的典型模式：

<Steps>
  <Step title="定義介面">
    ```php theme={null}
    <?php

    namespace App\Contracts;

    interface PaymentGateway
    {
        public function charge(int $amount, string $token): bool;
    }
    ```
  </Step>

  <Step title="建立實作類別">
    ```php theme={null}
    <?php

    namespace App\Services;

    use App\Contracts\PaymentGateway;

    class StripePaymentGateway implements PaymentGateway
    {
        public function charge(int $amount, string $token): bool
        {
            // 使用 Stripe API 的付款處理...
            return true;
        }
    }
    ```
  </Step>

  <Step title="在 Service Provider bind">
    ```php theme={null}
    use App\Contracts\PaymentGateway;
    use App\Services\StripePaymentGateway;

    $this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
    ```
  </Step>

  <Step title="在 controller 接收注入">
    ```php theme={null}
    <?php

    namespace App\Http\Controllers;

    use App\Contracts\PaymentGateway;
    use Illuminate\Http\Request;

    class OrderController extends Controller
    {
        public function __construct(
            protected PaymentGateway $payment,
        ) {}

        public function store(Request $request)
        {
            $this->payment->charge(
                $request->amount,
                $request->payment_token
            );

            // ...
        }
    }
    ```
  </Step>
</Steps>

透過此模式，即使要把付款服務從 `Stripe` 換成其他供應商，也只需改一處 binding。

## 後續步驟

<Card title="Service Provider" icon="plug" href="/zh-TW/service-providers">
  學習使用 service provider 登記 binding 的方式。
</Card>


## Related topics

- [Service Provider](/zh-TW/service-providers.md)
- [Laravel Passkeys 初步調查（passkeys-server + @laravel/passkeys）](/zh-TW/blog/passkeys-introduction.md)
- [HTTP Request](/zh-TW/requests.md)
- [開始學習 Laravel 前需要具備的知識](/zh-TW/true-tutorial.md)
- [Laravel 11 以後的應用程式結構](/zh-TW/advanced/app-structure.md)
