メインコンテンツへスキップ
パッケージの利用者が、複数のコントローラーから同じ形式のエラーJSONを返せるようにしたい場合があります。レスポンスMacroを公開すると、コントローラーの継承を要求せずに 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() で生成したレスポンスを返すため、同じ呼び出し先ではありません。
Illuminate\Http\Response::macro() に登録しても、response()->acmeFailure() のMacroにはなりません。ファサードとレスポンス本体の同名クラスを、use 文で取り違えないでください。

エラーJSONの生成APIを公開する

Service Providerで登録する

src/HttpServiceProvider.php の boot() に、パッケージ接頭辞付きのMacroを登録します。プロバイダーの読み込みはパッケージ自動検出を参照してください。 この例の公開契約は、error.code・error.message を持つJSONと、400〜599のHTTPステータスです。このステータス制限はLaravelのMacro機構の制約ではなく、エラー専用APIとしてパッケージが定めるものです。
Macroを使う前にこのプロバイダーが起動する必要があります。登録をHTTPルートの読み込みファイルや、runningInConsole() の否定条件の中に置かないでください。HTTP機能のテストもCLIから実行されます。 この登録だけを担うプロバイダーを、サービス解決待ちの遅延プロバイダーにする必要もありません。遅延登録の仕組みは遅延サービスプロバイダーを参照してください。

コントローラーから呼び出す

業務処理で競合を判定した後、決めたエラーコードと利用者向けのメッセージを渡します。
HTTPステータスは 409、JSONは次の形になります。通常の json() と同様に Content-Type は application/json です。
ファサード経由でも、同じFactoryのMacroを呼び出せます。
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 を生成します。共有するのは生成処理であり、レスポンスの本文やヘッダーそのものではありません。 長寿命ワーカーで守るべき境界は次のとおりです。
  1. 登録時に現在のRequest・認証ユーザー・テナントを use (...) で捕捉しない。これらの値は呼び出し時の引数で渡す。
  2. 登録時に生成したレスポンスを再利用しない。毎回新しいレスポンスを返す。
  3. 設定を登録時に値として捕捉する場合、その値がプロセス内で保持されることを仕様に含める。
  4. パッケージ更新後は、常駐プロセスを再起動して新しい登録コードを反映する。
パッケージサービスのライフサイクル設計では、コンテナの共有状態とリクエスト単位の状態をさらに詳しく扱っています。
Response::flushMacros() はFactoryに登録されたすべてのMacroを削除します。他のパッケージの登録も失われるので、本番のリクエスト処理や、共有テスト環境の安易な後処理には使わないでください。衝突・リセットの検証は、分離したテストプロセスで行います。

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やユーザー情報を登録時に保持しない。
  • 登録をリセットするテストを他のパッケージから分離している。
  • 常駐プロセスの再起動を更新手順に含めている。

参照した一次情報

最終更新日 2026年11月8日