v13.39.0 の公式ソースを確認し、courier 設定を読む独自属性を実装します。組み込みの #[Config('courier.timeout')] だけで目的を満たせる場合は、独自属性を増やす必要はありません。独自のキー規約や解決方針をパッケージのAPIとして提供する場合に検討します。
属性の役割は注入する値を決めること
Illuminate\Contracts\Container\ContextualAttribute を実装した属性は、Laravelのコンテナが引数を解決するときに読み取ります。属性オブジェクトそのものではなく、resolverが返した値が利用クラスに渡されます。
通常のバインディングと組み込み属性はサービスコンテナを参照してください。コンテキスト属性は、
Bind・Singleton・Scoped のようにクラスやインターフェースのバインディングを宣言する属性とは別の仕組みです。
パッケージ設定を読む属性を作る
デフォルト設定を登録する
まず、パッケージのconfig/courier.php にデフォルト値を用意します。
src/CourierServiceProvider.php の register() で設定をマージします。プロバイダーの自動登録はパッケージ自動検出、公開と設定キャッシュの扱いはパッケージ設定のマージとキャッシュを参照してください。
ContextualAttribute を実装する
src/Attributes/CourierConfig.php で、設定キーに courier. を付ける属性を定義します。
- 引数に付けられた属性のインスタンス。
- 依存を解決しているコンテナ。
- 解決対象を表す
ReflectionParameter。
#[CourierConfig(default: 30)] int $timeout は courier.timeout を参照します。利用側の引数名を変えても同じ設定を読みたい場合は、#[CourierConfig('timeout', default: 30)] のようにキーを明示します。
ContextualAttribute は確認時点ではメソッドを宣言しないマーカーインターフェースです。そのため、implements だけではresolverの存在やシグネチャは保証されません。上のように resolve() を用意すれば、この属性のために通常の bind() を登録する必要はありません。
コンテナから利用クラスを生成する
src/CourierOptions.php に属性を付けます。
$options->timeout は 45、$options->retries は 3 です。属性は設定を取得するだけで、正の整数か、許容する最大値か、といった業務上の検証は行いません。型宣言も設定値の検証処理の代わりにはなりません。
コンストラクターでの解決順を読む
Container::resolveDependencies() は、各引数について次の順序で値を決めます。
makeWith()などで渡された、引数名に一致する上書き値を使う。Util::getContextualAttributeFromDependency()でコンテキスト属性を探して解決する。- 属性の結果が
nullなら、通常のプリミティブ値またはクラスの解決へ進む。 - 属性の後処理を経て、決まった値をコンストラクターに渡す。
makeWith() のキーは、設定キーではなく利用クラスの引数名です。
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 を読む例です。
new CourierOptions(timeout: 5, retries: 1) も通常のPHPによる生成です。属性を付けただけでは、直接の new やメソッド呼び出しに依存注入は追加されません。
コンストラクターとcallableの両方で同じデフォルト動作が必要なら、引数のデフォルト値だけに頼らず
#[CourierConfig('timeout', default: 30)] のように属性側のデフォルトを定義します。この default は設定キーが存在しない場合に使われ、存在するキーの値が null の場合は置換しません。null 自体を意味のある値として扱うAPIでは、2つの解決経路を別々に検証してください。値の再注入とサービスの寿命を分ける
属性は、引数を解決する時点の設定を読みます。すでに生成済みのCourierOptions のプロパティを監視・更新する機能ではありません。
例えば CourierOptions をsingletonにすると、最初に注入された値がそのインスタンスに保持されます。その後で設定を変更して通常の make() を呼んでも、同じインスタンスの値は変わりません。
リクエストごとの情報を読む属性を作る場合にも、属性自身がsingletonやscopedの寿命を決めるわけではありません。注入された値やオブジェクトを保持するサービスの寿命をパッケージサービスのライフサイクル設計に沿って選びます。設定キャッシュの再生成と、長寿命プロセスや生成済みサービスの更新も分けて考えます。
解決経路をテストする
属性単体のテストは、公式テストと同様に新しいContainer と Illuminate\Config\Repository を使って書けます。次のPHPUnit例は、上で定義した CourierConfig・CourierOptions・NullableTimeoutOptions を読み込んだテスト環境を前提にしています。
7 にする例は、優先順位のテスト専用です。このコンテナ内では retries を含めた同じ属性のすべての解決に影響します。
この単体テストだけでは、パッケージのプロバイダー登録や mergeConfigFrom()、設定キャッシュとの統合までは確認できません。パッケージテストのアプリケーション環境でも、公開前・公開済み・設定キャッシュありの状態を検証します。
リリース前の確認項目
- 属性のクラス名、コンストラクター引数名、設定キー、戻り値の型を公開APIとして扱う。名前付き引数で使われる
key・defaultの変更にも注意する。 - キー省略時に引数名を使う仕様では、利用クラスの引数名変更が参照する設定を変えないか確認する。
- 属性なしの既存バインディング、属性付きの解決、引数名での上書きをそれぞれテストする。
null、設定不足、不適切な型をmake()とcall()の両方でテストする。- singletonや長寿命ワーカーに古い値を保持させない設計を確認する。
- Laravelの対応バージョン更新時は、resolverの引数、優先順位、属性の後処理を公式ソースとテストで再確認する。
関連ページ
パッケージ設定のマージとキャッシュ
デフォルト設定、公開済み設定、キャッシュの更新境界を確認します。
パッケージサービスのライフサイクル設計
注入した値を保持するサービスの寿命を選びます。
パッケージのコンテナタグとプラグイン集約
組み込みのTag属性と、登録済み実装の集約を確認します。
PHP Reflection
属性と引数のメタデータを読み取る仕組みを確認します。
参照した一次情報
- Laravel公式ドキュメント: Defining custom attributes
- Laravel公式ドキュメント: パッケージ設定
- Container: resolveDependencies()・resolveFromAttribute()・whenHasAttribute()・属性の後処理
- BoundMethod: callableの引数解決
- Util: ContextualAttributeのReflectionによる検出
- ContextualAttribute: マーカーインターフェース
- Config: 組み込み設定属性
- Config Repository: get()と設定値の取得
- Arr: get()のデフォルト値と存在するnullの扱い
- 公式テスト: 属性のresolver・登録handler・ReflectionParameter・callでの解決