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

# パッケージのコンテキスト属性と依存解決

> Laravel 13のContextualAttributeを読み解き、パッケージ設定を注入する属性、解決の優先順位、makeとcallの違い、長寿命サービスのテストと保守を解説します。

パッケージの利用者が、設定キーをサービスプロバイダーに並べずに必要な値を注入できるようにしたい場合があります。コンテキスト属性を公開すると、引数に付けたメタデータからパッケージ固有の設定やサービスを解決できます。

このページではLaravel Framework `v13.39.0` の公式ソースを確認し、`courier` 設定を読む独自属性を実装します。組み込みの `#[Config('courier.timeout')]` だけで目的を満たせる場合は、独自属性を増やす必要はありません。独自のキー規約や解決方針をパッケージのAPIとして提供する場合に検討します。

## 属性の役割は注入する値を決めること

`Illuminate\Contracts\Container\ContextualAttribute` を実装した属性は、Laravelのコンテナが引数を解決するときに読み取ります。属性オブジェクトそのものではなく、resolverが返した値が利用クラスに渡されます。

| 方法 | 設定する場所 | 向いている用途 |
| - | - | - |
| `when()->needs()->give()` | サービスプロバイダー | 利用クラスを変更せず、アプリ側で依存関係を切り替える |
| 組み込みの `Config`・`Storage`・`Tag` 属性 | 利用クラスの引数 | Laravelが用意した解決方法を宣言する |
| パッケージ独自のコンテキスト属性 | 利用クラスの引数と属性クラス | パッケージ固有の規約で値を解決する |

通常のバインディングと組み込み属性は[サービスコンテナ](https://kawax.biz/jp/service-container.md)を参照してください。コンテキスト属性は、`Bind`・`Singleton`・`Scoped` のようにクラスやインターフェースのバインディングを宣言する属性とは別の仕組みです。

## パッケージ設定を読む属性を作る

### デフォルト設定を登録する

まず、パッケージの `config/courier.php` にデフォルト値を用意します。

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

return [
    'timeout' => 30,
    'retries' => 3,
];
```

`src/CourierServiceProvider.php` の `register()` で設定をマージします。プロバイダーの自動登録は[パッケージ自動検出](https://kawax.biz/jp/advanced/package-discovery.md)、公開と設定キャッシュの扱いは[パッケージ設定のマージとキャッシュ](https://kawax.biz/jp/advanced/package-config-merging.md)を参照してください。

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

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );
    }
}
```

### `ContextualAttribute` を実装する

`src/Attributes/CourierConfig.php` で、設定キーに `courier.` を付ける属性を定義します。

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

namespace Acme\Courier\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionParameter;

#[Attribute(Attribute::TARGET_PARAMETER)]
final class CourierConfig implements ContextualAttribute
{
    public function __construct(
        public ?string $key = null,
        public mixed $default = null,
    ) {}

    public static function resolve(
        self $attribute,
        Container $container,
        ReflectionParameter $parameter,
    ): mixed {
        $key = $attribute->key ?? $parameter->getName();

        return $container->make('config')->get(
            'courier.'.$key, $attribute->default
        );
    }
}
```

確認した実装では、resolverへ次の3つの引数が渡されます。

1. 引数に付けられた属性のインスタンス。
2. 依存を解決しているコンテナ。
3. 解決対象を表す `ReflectionParameter`。

この例では、キーを省略した場合に引数名を使います。`#[CourierConfig(default: 30)] int $timeout` は `courier.timeout` を参照します。利用側の引数名を変えても同じ設定を読みたい場合は、`#[CourierConfig('timeout', default: 30)]` のようにキーを明示します。

`ContextualAttribute` は確認時点ではメソッドを宣言しないマーカーインターフェースです。そのため、`implements` だけではresolverの存在やシグネチャは保証されません。上のように `resolve()` を用意すれば、この属性のために通常の `bind()` を登録する必要はありません。

> [!WARNING]
>
> 属性のコンストラクターはメタデータの保持だけに使います。コンテナは解決時と属性の後処理で別々に `newInstance()` を呼ぶため、「一度だけ実行する初期化」の場所ではありません。外部通信や可変の状態を持たせないでください。

### コンテナから利用クラスを生成する

`src/CourierOptions.php` に属性を付けます。

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

namespace Acme\Courier;

use Acme\Courier\Attributes\CourierConfig;

final class CourierOptions
{
    public function __construct(
        #[CourierConfig('timeout', default: 30)] public int $timeout,
        #[CourierConfig('retries', default: 3)] public int $retries,
    ) {}
}
```

設定を利用アプリケーションから上書きすると、次の解決時にその値が注入されます。

```php theme={null}
use Acme\Courier\CourierOptions;

config(['courier.timeout' => 45]);

$options = app()->make(CourierOptions::class);
```

この例の `$options->timeout` は `45`、`$options->retries` は `3` です。属性は設定を取得するだけで、正の整数か、許容する最大値か、といった業務上の検証は行いません。型宣言も設定値の検証処理の代わりにはなりません。

## コンストラクターでの解決順を読む

`Container::resolveDependencies()` は、各引数について次の順序で値を決めます。

1. `makeWith()` などで渡された、引数名に一致する上書き値を使う。
2. `Util::getContextualAttributeFromDependency()` でコンテキスト属性を探して解決する。
3. 属性の結果が `null` なら、通常のプリミティブ値またはクラスの解決へ進む。
4. 属性の後処理を経て、決まった値をコンストラクターに渡す。

1の上書き経路は、その引数の属性解決と後処理をスキップします。

```mermaid theme={null}
flowchart TD
    A["コンストラクター引数"] --> B{"引数名で上書き済み?"}
    B -->|Yes| C["上書き値を使う"]
    B -->|No| D{"ContextualAttributeがある?"}
    D -->|Yes| E["属性のresolverを呼ぶ"]
    E --> F{"結果はnull?"}
    F -->|No| G["属性の値を使う"]
    F -->|Yes| H["通常の型・プリミティブ解決"]
    D -->|No| H
    G --> I["属性の後処理"]
    H --> I
    C --> J["コンストラクターに渡す"]
    I --> J
```

`makeWith()` のキーは、設定キーではなく利用クラスの引数名です。

```php theme={null}
use Acme\Courier\CourierOptions;

$options = app()->makeWith(CourierOptions::class, [
    'timeout' => 5,
]);
```

この場合、`timeout` は設定値にかかわらず `5` です。一方、属性が `null` 以外を返した場合は、通常の `when()->needs('$timeout')->give(...)` より属性の結果が優先されます。パッケージへの属性追加が、既存利用者のコンテキストバインディングを変える可能性もテストします。

`Util` は、`ContextualAttribute` を実装した属性をReflectionで取得し、その先頭の1件を解決用に選びます。複数の異なるコンテキスト属性を並べても、resolverが順番に合成されるわけではありません。

### 登録済みhandlerは `resolve()` より優先される

`Container::resolveFromAttribute()` は属性クラス名をキーに、`whenHasAttribute()` で登録されたhandlerを先に探します。なければ属性の `resolve()` を使い、どちらもなければ `BindingResolutionException` を投げます。

`whenHasAttribute()` は利用クラスごとではなく属性の種類ごとの設定です。同じ属性クラスへの再登録は前のhandlerを上書きします。パッケージが他のパッケージやLaravel組み込み属性のhandlerを無条件に変更すると、利用アプリケーション全体へ影響します。独自属性の標準動作は `resolve()` に置き、handlerによる変更は明示的な拡張点として扱います。

## `make()`・`call()`・直接呼び出しは同じではない

`app()->call()` も属性を解決しますが、通常の `BoundMethod` 経路は、属性が見つかるとその結果をそのまま引数へ追加します。コンストラクターの解決と違い、属性の結果が `null` でもデフォルト値の解決へ戻りません。

ほかのコンテキストバインディングがない状態で、存在しない `courier.missing` を読む例です。

```php theme={null}
use Acme\Courier\Attributes\CourierConfig;

final class NullableTimeoutOptions
{
    public function __construct(
        #[CourierConfig('missing')] public ?int $timeout = 99,
    ) {}
}

$readTimeout = function (
    #[CourierConfig('missing')] ?int $timeout = 99,
): ?int {
    return $timeout;
};
```

| 呼び出し | 結果 | 理由 |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | 属性が `null` を返し、プリミティブのデフォルト値へ進む |
| `app()->call($readTimeout)` | `null` | 属性の結果をそのまま渡す |
| `$readTimeout()` | `99` | PHPの通常の呼び出しであり、属性を解決しない |

`new CourierOptions(timeout: 5, retries: 1)` も通常のPHPによる生成です。属性を付けただけでは、直接の `new` やメソッド呼び出しに依存注入は追加されません。

> [!NOTE]
>
> コンストラクターとcallableの両方で同じデフォルト動作が必要なら、引数のデフォルト値だけに頼らず `#[CourierConfig('timeout', default: 30)]` のように属性側のデフォルトを定義します。この `default` は設定キーが存在しない場合に使われ、存在するキーの値が `null` の場合は置換しません。`null` 自体を意味のある値として扱うAPIでは、2つの解決経路を別々に検証してください。

## 値の再注入とサービスの寿命を分ける

属性は、引数を解決する時点の設定を読みます。すでに生成済みの `CourierOptions` のプロパティを監視・更新する機能ではありません。

例えば `CourierOptions` をsingletonにすると、最初に注入された値がそのインスタンスに保持されます。その後で設定を変更して通常の `make()` を呼んでも、同じインスタンスの値は変わりません。

リクエストごとの情報を読む属性を作る場合にも、属性自身がsingletonやscopedの寿命を決めるわけではありません。注入された値やオブジェクトを保持するサービスの寿命を[パッケージサービスのライフサイクル設計](https://kawax.biz/jp/advanced/package-service-lifetimes.md)に沿って選びます。設定キャッシュの再生成と、長寿命プロセスや生成済みサービスの更新も分けて考えます。

## 解決経路をテストする

属性単体のテストは、公式テストと同様に新しい `Container` と `Illuminate\Config\Repository` を使って書けます。次のPHPUnit例は、上で定義した `CourierConfig`・`CourierOptions`・`NullableTimeoutOptions` を読み込んだテスト環境を前提にしています。

```php theme={null}
use Acme\Courier\Attributes\CourierConfig;
use Acme\Courier\CourierOptions;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\Container as ContainerContract;
use PHPUnit\Framework\TestCase;

class CourierConfigTest extends TestCase
{
    private function container(array $settings = []): Container
    {
        $container = new Container;
        $container->instance('config', new Repository([
            'courier' => $settings,
        ]));

        return $container;
    }

    public function testConfigDefaultsAndNamedOverrides(): void
    {
        $container = $this->container();
        $this->assertSame(30, $container->make(CourierOptions::class)->timeout);

        $container->make('config')->set('courier.timeout', 45);
        $container->when(CourierOptions::class)->needs('$timeout')->give(77);
        $this->assertSame(45, $container->make(CourierOptions::class)->timeout);

        $options = $container->makeWith(CourierOptions::class, ['timeout' => 5]);
        $this->assertSame(5, $options->timeout);
        $this->assertSame(3, $options->retries);
    }

    public function testParameterNameIsAvailableToTheResolver(): void
    {
        $container = $this->container(['timeout' => 45]);

        $value = $container->call(function (
            #[CourierConfig(default: 30)] int $timeout,
        ): int {
            return $timeout;
        });

        $this->assertSame(45, $value);
    }

    public function testRegisteredHandlerOverridesResolve(): void
    {
        $container = $this->container(['timeout' => 45]);
        $container->whenHasAttribute(
            CourierConfig::class,
            static fn (CourierConfig $attribute, ContainerContract $app) => 7,
        );

        $this->assertSame(7, $container->make(CourierOptions::class)->timeout);
    }

    public function testNullDiffersBetweenMakeAndCall(): void
    {
        $container = $this->container();
        $readTimeout = function (
            #[CourierConfig('missing')] ?int $timeout = 99,
        ): ?int {
            return $timeout;
        };

        $this->assertSame(99, $container->make(NullableTimeoutOptions::class)->timeout);
        $this->assertNull($container->call($readTimeout));
        $this->assertSame(99, $readTimeout());
    }

    public function testSingletonKeepsItsResolvedValues(): void
    {
        $container = $this->container(['timeout' => 45]);
        $container->singleton(CourierOptions::class);
        $options = $container->make(CourierOptions::class);

        $container->make('config')->set('courier.timeout', 60);

        $this->assertSame($options, $container->make(CourierOptions::class));
        $this->assertSame(45, $options->timeout);
    }
}
```

handlerを常に `7` にする例は、優先順位のテスト専用です。このコンテナ内では `retries` を含めた同じ属性のすべての解決に影響します。

この単体テストだけでは、パッケージのプロバイダー登録や `mergeConfigFrom()`、設定キャッシュとの統合までは確認できません。[パッケージテスト](https://kawax.biz/jp/advanced/package-testing.md)のアプリケーション環境でも、公開前・公開済み・設定キャッシュありの状態を検証します。

### リリース前の確認項目

* 属性のクラス名、コンストラクター引数名、設定キー、戻り値の型を公開APIとして扱う。名前付き引数で使われる `key`・`default` の変更にも注意する。
* キー省略時に引数名を使う仕様では、利用クラスの引数名変更が参照する設定を変えないか確認する。
* 属性なしの既存バインディング、属性付きの解決、引数名での上書きをそれぞれテストする。
* `null`、設定不足、不適切な型を `make()` と `call()` の両方でテストする。
* singletonや長寿命ワーカーに古い値を保持させない設計を確認する。
* Laravelの対応バージョン更新時は、resolverの引数、優先順位、属性の後処理を公式ソースとテストで再確認する。

## 関連ページ

> [card] [パッケージ設定のマージとキャッシュ](https://kawax.biz/jp/advanced/package-config-merging.md)
>
> デフォルト設定、公開済み設定、キャッシュの更新境界を確認します。

> [card] [パッケージサービスのライフサイクル設計](https://kawax.biz/jp/advanced/package-service-lifetimes.md)
>
> 注入した値を保持するサービスの寿命を選びます。

> [card] [パッケージのコンテナタグとプラグイン集約](https://kawax.biz/jp/advanced/package-container-tags.md)
>
> 組み込みのTag属性と、登録済み実装の集約を確認します。

> [card] [PHP Reflection](https://kawax.biz/jp/advanced/php-reflection.md)
>
> 属性と引数のメタデータを読み取る仕組みを確認します。

## 参照した一次情報

* [Laravel公式ドキュメント: Defining custom attributes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Laravel公式ドキュメント: パッケージ設定](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container: resolveDependencies()・resolveFromAttribute()・whenHasAttribute()・属性の後処理](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod: callableの引数解決](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util: ContextualAttributeのReflectionによる検出](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Util.php)
* [ContextualAttribute: マーカーインターフェース](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Container/ContextualAttribute.php)
* [Config: 組み込み設定属性](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository: get()と設定値の取得](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr: get()のデフォルト値と存在するnullの扱い](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [公式テスト: 属性のresolver・登録handler・ReflectionParameter・callでの解決](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [サービスコンテナ](https://kawax.biz/jp/service-container.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [パッケージのArtisanコマンド登録と依存解決](https://kawax.biz/jp/advanced/package-commands.md)
- [パッケージのコンテナタグとプラグイン集約](https://kawax.biz/jp/advanced/package-container-tags.md)
- [セッションコンテキストとフィルタリング](https://kawax.biz/jp/packages/laravel-copilot-sdk/session-context.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.