response()->acmeFailure(...) のような生成APIを追加できます。
このページは、Laravel公式ドキュメントのデフォルトブランチ 13.x と、2026年11月8日に確認したFrameworkの最新リリース v13.40.0 の公式実装を参照しています。レスポンスとMacroableトレイトの基本を踏まえ、配布パッケージの公開APIとして保守する際の境界を扱います。
登録先はレスポンスではなくFactory
Response という名前には、生成を依頼するファサードと、生成されたHTTPレスポンスのクラスがあります。公式ドキュメントの Response::macro() が拡張するのは、ファサードの解決先である ResponseFactory です。
引数なしの
response() はFactoryを返します。一方、response('本文') や response(null) は make() で生成したレスポンスを返すため、同じ呼び出し先ではありません。
エラーJSONの生成APIを公開する
Service Providerで登録する
src/HttpServiceProvider.php の boot() に、パッケージ接頭辞付きのMacroを登録します。プロバイダーの読み込みはパッケージ自動検出を参照してください。
この例の公開契約は、error.code・error.message を持つJSONと、400〜599のHTTPステータスです。このステータス制限はLaravelのMacro機構の制約ではなく、エラー専用APIとしてパッケージが定めるものです。
runningInConsole() の否定条件の中に置かないでください。HTTP機能のテストもCLIから実行されます。
この登録だけを担うプロバイダーを、サービス解決待ちの遅延プロバイダーにする必要もありません。遅延登録の仕組みは遅延サービスプロバイダーを参照してください。
コントローラーから呼び出す
業務処理で競合を判定した後、決めたエラーコードと利用者向けのメッセージを渡します。409、JSONは次の形になります。通常の json() と同様に Content-Type は application/json です。
Macroは、このメソッドを呼んだレスポンスだけを生成します。バリデーション例外や認証エラーを自動的にこの形式へ変更するものではありません。また、
Accept ヘッダーに応じてHTMLとJSONを切り替える処理も追加していません。Closureの $this と呼び出し経路
Factoryは Macroable を使っています。インスタンスの未定義メソッドを呼ぶと __call() が登録済みMacroを探し、通常のClosureを呼び出し先のインスタンスへ再バインドします。
登録コードがService Providerのメソッド内にあっても、実行時の $this はService ProviderではなくFactoryです。そのため、例の $this->json() はFactoryの通常のメソッドを呼びます。
$thisを使うMacroは、通常のfunction (...) { ... }で定義します。static functionはインスタンスの$thisを持ちません。- invokableオブジェクトを登録した場合、Closureのような再バインドは行われません。必要な依存は、そのオブジェクト自身のコンストラクターなどで受け取ります。
- Macroの引数は、呼び出し側が渡した引数です。
Requestなどを型宣言しても、Macroableがコンテナから自動注入するわけではありません。
Response::acmeFailure() は、見た目が静的でも内部でFactoryのインスタンスへ転送されます。一方、具象クラスの Illuminate\Routing\ResponseFactory::acmeFailure() は本当の静的呼び出しになり、__callStatic() はClosureをインスタンスなしでバインドします。今回の $this->json() を使うMacroは、この具象クラスの静的呼び出しには対応しません。
名前衝突を互換性の問題として扱う
macro() は、名前をキーにした静的配列へ代入するだけです。既存名を登録しても警告は出ません。
failure のような汎用名より、acmeFailure のようにパッケージ名を識別できる名前を選びます。Laravelの更新時には method_exists(ResponseFactory::class, 'acmeFailure') でも実在メソッドとの衝突を確認します。
hasMacro() を見て無条件に登録をスキップすると、別のパッケージの実装を自分のAPIだと思って使う可能性があります。「アプリ側の事前登録で上書きできる」ことを仕様にする場合だけ、その優先順位と必要な引数・返り値を公開してください。上の例は、プロバイダーが起動するたびにパッケージ自身の定義を登録する方針です。
登録状態とレスポンスの寿命を分ける
Macroの登録表はFactoryの静的プロパティです。Factoryを作り直したり、ファサードの解決済みインスタンスを消したりするだけでは、この表は消えません。同じPHPプロセス内でアプリケーションを再生成するテストでも、登録状態が残り得ます。 一方、例のMacroは呼び出すたびにjson() が新しい JsonResponse を生成します。共有するのは生成処理であり、レスポンスの本文やヘッダーそのものではありません。
長寿命ワーカーで守るべき境界は次のとおりです。
- 登録時に現在のRequest・認証ユーザー・テナントを
use (...)で捕捉しない。これらの値は呼び出し時の引数で渡す。 - 登録時に生成したレスポンスを再利用しない。毎回新しいレスポンスを返す。
- 設定を登録時に値として捕捉する場合、その値がプロセス内で保持されることを仕様に含める。
- パッケージ更新後は、常駐プロセスを再起動して新しい登録コードを反映する。
HTTP契約を実際の返り値でテストする
パッケージをComposerで読み込めるLaravel 13の検証用アプリに、次のtests/Feature/ResponseMacroTest.php を置きます。Tests\TestCase とHTTPテストの基本はHTTPテスト、パッケージ単体の環境構築はパッケージのテストを参照してください。
Response::shouldReceive('acmeFailure') のように生成APIそのものをモックすると、Macroの登録・Closureのバインド・JSON生成が実行されません。呼び出し回数の検証とは分けて、上のように実際のHTTPレスポンスを検証します。
加えて、利用者が受け取る本文・ステータス・ヘッダーを互換性の対象として扱います。
error.codeは機械判定用として安定させ、表示メッセージの変更と区別する。- 引数名は名前付き引数で使われるため、既定値や順序と同様に変更の影響を確認する。
204のような本文なしレスポンスは、エラーJSONではなくnoContent()など別のAPIで返す。Content-Typeを独自ヘッダーで変更する拡張を許す場合も、本文形式と一致させる。- データのシリアライズやページネーションが目的なら、Macroだけで再実装せずEloquent APIリソースも検討する。
長期保守のチェックリスト
- Laravel 13のサポート対象の組み合わせで、プロバイダーが起動し、ヘルパーとファサードの両方から呼べる。
- Macro名が他の登録やFactoryの実在メソッドと衝突していない。
- JSONのキー、HTTPステータス、ヘッダー、既定値、名前付き引数の互換性をテストしている。
- 連続呼び出しでもレスポンスを共有せず、Requestやユーザー情報を登録時に保持しない。
- 登録をリセットするテストを他のパッケージから分離している。
- 常駐プロセスの再起動を更新手順に含めている。