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

# パッケージのレスポンスMacroとHTTP契約

> Laravel 13のResponseFactoryとMacroableを読み解き、パッケージ独自のJSONレスポンス、名前衝突、登録状態の寿命、HTTP契約のテストと保守を解説します。

パッケージの利用者が、複数のコントローラーから同じ形式のエラーJSONを返せるようにしたい場合があります。レスポンスMacroを公開すると、コントローラーの継承を要求せずに `response()->acmeFailure(...)` のような生成APIを追加できます。

このページは、Laravel公式ドキュメントのデフォルトブランチ `13.x` と、2026年11月8日に確認したFrameworkの最新リリース `v13.40.0` の公式実装を参照しています。[レスポンス](https://kawax.biz/jp/responses.md)と[Macroableトレイト](https://kawax.biz/jp/advanced/macroable.md)の基本を踏まえ、配布パッケージの公開APIとして保守する際の境界を扱います。

## 登録先はレスポンスではなくFactory

`Response` という名前には、生成を依頼するファサードと、生成されたHTTPレスポンスのクラスがあります。公式ドキュメントの `Response::macro()` が拡張するのは、ファサードの解決先である `ResponseFactory` です。

| 対象 | 役割 | このページでの扱い |
| - | - | - |
| `Illuminate\Support\Facades\Response` | `ResponseFactory` の契約を解決して呼び出すファサード | Macroの登録と呼び出し |
| `Illuminate\Routing\ResponseFactory` | `json()`・`view()`・`noContent()` などの生成API | Macroを保持するクラス |
| `Illuminate\Http\Response` | 生成済みのHTTPレスポンス | Factoryとは別のMacro登録先 |
| `Illuminate\Http\JsonResponse` | JSONレスポンス | 今回のMacroの返り値 |

引数なしの `response()` はFactoryを返します。一方、`response('本文')` や `response(null)` は `make()` で生成したレスポンスを返すため、同じ呼び出し先ではありません。

> [!WARNING]
>
> `Illuminate\Http\Response::macro()` に登録しても、`response()->acmeFailure()` のMacroにはなりません。ファサードとレスポンス本体の同名クラスを、`use` 文で取り違えないでください。

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

### Service Providerで登録する

`src/HttpServiceProvider.php` の `boot()` に、パッケージ接頭辞付きのMacroを登録します。プロバイダーの読み込みは[パッケージ自動検出](https://kawax.biz/jp/advanced/package-discovery.md)を参照してください。

この例の公開契約は、`error.code`・`error.message` を持つJSONと、400〜599のHTTPステータスです。このステータス制限はLaravelのMacro機構の制約ではなく、エラー専用APIとしてパッケージが定めるものです。

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

namespace Acme\Http;

use Illuminate\Http\JsonResponse;
use Illuminate\Routing\ResponseFactory;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;

final class HttpServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Response::macro('acmeFailure', function (
            string $code,
            string $message,
            int $status = 422,
            array $headers = [],
        ): JsonResponse {
            if ($status < 400 || $status > 599) {
                throw new InvalidArgumentException(
                    'acmeFailure requires a status between 400 and 599.'
                );
            }

            /** @var ResponseFactory $this */
            return $this->json([
                'error' => [
                    'code' => $code,
                    'message' => $message,
                ],
            ], $status, $headers);
        });
    }
}
```

Macroを使う前にこのプロバイダーが起動する必要があります。登録をHTTPルートの読み込みファイルや、`runningInConsole()` の否定条件の中に置かないでください。HTTP機能のテストもCLIから実行されます。

この登録だけを担うプロバイダーを、サービス解決待ちの遅延プロバイダーにする必要もありません。遅延登録の仕組みは[遅延サービスプロバイダー](https://kawax.biz/jp/advanced/deferred-provider.md)を参照してください。

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

業務処理で競合を判定した後、決めたエラーコードと利用者向けのメッセージを渡します。

```php theme={null}
return response()->acmeFailure(
    code: 'order_conflict',
    message: '注文を確定できませんでした。',
    status: 409,
    headers: ['X-Acme-Error' => 'order_conflict'],
);
```

HTTPステータスは `409`、JSONは次の形になります。通常の `json()` と同様に `Content-Type` は `application/json` です。

```json theme={null}
{
  "error": {
    "code": "order_conflict",
    "message": "注文を確定できませんでした。"
  }
}
```

ファサード経由でも、同じFactoryのMacroを呼び出せます。

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

return Response::acmeFailure(
    'order_conflict', '注文を確定できませんでした。', 409
);
```

> [!NOTE]
>
> Macroは、このメソッドを呼んだレスポンスだけを生成します。バリデーション例外や認証エラーを自動的にこの形式へ変更するものではありません。また、`Accept` ヘッダーに応じてHTMLとJSONを切り替える処理も追加していません。

## Closureの `$this` と呼び出し経路

Factoryは `Macroable` を使っています。インスタンスの未定義メソッドを呼ぶと `__call()` が登録済みMacroを探し、通常のClosureを呼び出し先のインスタンスへ再バインドします。

```mermaid theme={null}
flowchart TD
    A["response()->acmeFailure(...)<br>または Response::acmeFailure(...)"] --> B["ResponseFactory のインスタンス"]
    B --> C["Macroable::__call()<br>Closure の $this を再バインド"]
    C --> D["ResponseFactory::json()"]
    D --> E["新しい JsonResponse"]
```

登録コードが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()` は、名前をキーにした静的配列へ代入するだけです。既存名を登録しても警告は出ません。

| 状況 | 確認した実装の挙動 |
| - | - |
| 同じ名前のMacroを再登録 | 後の登録が前のMacroを上書きする |
| `json()` など、クラスに実在する公開メソッドと同名で登録 | 通常のメソッドが呼ばれ、Macroには到達しない |
| `hasMacro($name)` が `true` | 登録済みという意味。実在メソッドとの衝突がない保証ではない |
| `mixin($object, replace: false)` | 登録済みMacroの置換を避ける。実在メソッドとの衝突検査ではない |

`failure` のような汎用名より、`acmeFailure` のようにパッケージ名を識別できる名前を選びます。Laravelの更新時には `method_exists(ResponseFactory::class, 'acmeFailure')` でも実在メソッドとの衝突を確認します。

`hasMacro()` を見て無条件に登録をスキップすると、別のパッケージの実装を自分のAPIだと思って使う可能性があります。「アプリ側の事前登録で上書きできる」ことを仕様にする場合だけ、その優先順位と必要な引数・返り値を公開してください。上の例は、プロバイダーが起動するたびにパッケージ自身の定義を登録する方針です。

## 登録状態とレスポンスの寿命を分ける

Macroの登録表はFactoryの**静的プロパティ**です。Factoryを作り直したり、ファサードの解決済みインスタンスを消したりするだけでは、この表は消えません。同じPHPプロセス内でアプリケーションを再生成するテストでも、登録状態が残り得ます。

一方、例のMacroは呼び出すたびに `json()` が新しい `JsonResponse` を生成します。共有するのは生成処理であり、レスポンスの本文やヘッダーそのものではありません。

長寿命ワーカーで守るべき境界は次のとおりです。

1. 登録時に現在のRequest・認証ユーザー・テナントを `use (...)` で捕捉しない。これらの値は呼び出し時の引数で渡す。
2. 登録時に生成したレスポンスを再利用しない。毎回新しいレスポンスを返す。
3. 設定を登録時に値として捕捉する場合、その値がプロセス内で保持されることを仕様に含める。
4. パッケージ更新後は、常駐プロセスを再起動して新しい登録コードを反映する。

[パッケージサービスのライフサイクル設計](https://kawax.biz/jp/advanced/package-service-lifetimes.md)では、コンテナの共有状態とリクエスト単位の状態をさらに詳しく扱っています。

> [!WARNING]
>
> `Response::flushMacros()` はFactoryに登録された**すべてのMacro**を削除します。他のパッケージの登録も失われるので、本番のリクエスト処理や、共有テスト環境の安易な後処理には使わないでください。衝突・リセットの検証は、分離したテストプロセスで行います。

## HTTP契約を実際の返り値でテストする

パッケージをComposerで読み込めるLaravel 13の検証用アプリに、次の `tests/Feature/ResponseMacroTest.php` を置きます。`Tests\TestCase` とHTTPテストの基本は[HTTPテスト](https://kawax.biz/jp/http-tests.md)、パッケージ単体の環境構築は[パッケージのテスト](https://kawax.biz/jp/advanced/package-testing.md)を参照してください。

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

namespace Tests\Feature;

use Acme\Http\HttpServiceProvider;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use InvalidArgumentException;
use Tests\TestCase;

class ResponseMacroTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->app->register(HttpServiceProvider::class);
    }

    public function test_http_response_keeps_the_public_contract(): void
    {
        Route::get('/_acme/failure', function (): JsonResponse {
            return response()->acmeFailure(
                'order_conflict',
                '注文を確定できませんでした。',
                409,
                ['X-Acme-Error' => 'order_conflict'],
            );
        });

        $this->getJson('/_acme/failure')
            ->assertStatus(409)
            ->assertHeader('Content-Type', 'application/json')
            ->assertHeader('X-Acme-Error', 'order_conflict')
            ->assertExactJson([
                'error' => [
                    'code' => 'order_conflict',
                    'message' => '注文を確定できませんでした。',
                ],
            ]);
    }

    public function test_facade_returns_a_fresh_json_response(): void
    {
        $first = Response::acmeFailure('first', '最初のエラー');
        $second = response()->acmeFailure('second', '次のエラー', 503);

        $this->assertInstanceOf(JsonResponse::class, $first);
        $this->assertNotSame($first, $second);
        $this->assertSame(422, $first->getStatusCode());

        $first->setData(['changed' => true]);

        $this->assertSame(503, $second->getStatusCode());
        $this->assertSame('second', $second->getData(true)['error']['code']);
    }

    public function test_success_status_is_rejected(): void
    {
        $this->expectException(InvalidArgumentException::class);

        response()->acmeFailure('invalid_status', '設定を確認してください。', 200);
    }
}
```

`Response::shouldReceive('acmeFailure')` のように生成APIそのものをモックすると、Macroの登録・Closureのバインド・JSON生成が実行されません。呼び出し回数の検証とは分けて、上のように実際のHTTPレスポンスを検証します。

加えて、利用者が受け取る本文・ステータス・ヘッダーを互換性の対象として扱います。

* `error.code` は機械判定用として安定させ、表示メッセージの変更と区別する。
* 引数名は名前付き引数で使われるため、既定値や順序と同様に変更の影響を確認する。
* `204` のような本文なしレスポンスは、エラーJSONではなく `noContent()` など別のAPIで返す。
* `Content-Type` を独自ヘッダーで変更する拡張を許す場合も、本文形式と一致させる。
* データのシリアライズやページネーションが目的なら、Macroだけで再実装せず[Eloquent APIリソース](https://kawax.biz/jp/eloquent-resources.md)も検討する。

## 長期保守のチェックリスト

* [ ] Laravel 13のサポート対象の組み合わせで、プロバイダーが起動し、ヘルパーとファサードの両方から呼べる。
* [ ] Macro名が他の登録やFactoryの実在メソッドと衝突していない。
* [ ] JSONのキー、HTTPステータス、ヘッダー、既定値、名前付き引数の互換性をテストしている。
* [ ] 連続呼び出しでもレスポンスを共有せず、Requestやユーザー情報を登録時に保持しない。
* [ ] 登録をリセットするテストを他のパッケージから分離している。
* [ ] 常駐プロセスの再起動を更新手順に含めている。

## 参照した一次情報

* [Laravel公式ドキュメント: Response macros・JSON responses（13.x）](https://github.com/laravel/docs/blob/13.x/responses.md#response-macros)
* [Laravel公式ドキュメント: Package development（13.x）](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Responseファサードと解決先（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/Facades/Response.php)
* [Facadeのインスタンスへの転送（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/Facades/Facade.php)
* [response() ヘルパー（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Foundation/helpers.php)
* [ResponseFactoryの生成処理（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Routing/ResponseFactory.php)
* [Macroableの登録・バインド・リセット（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Macroable/Traits/Macroable.php)
* [Macroableの公式テスト（v13.40.0）](https://github.com/laravel/framework/blob/v13.40.0/tests/Support/SupportMacroableTest.php)


## Related topics

- [Macroableトレイト](https://kawax.biz/jp/advanced/macroable.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [Laravel Telescope 実践テクニック](https://kawax.biz/jp/blog/telescope-introduction.md)
- [パッケージのカスタムキャッシュストアと契約](https://kawax.biz/jp/advanced/package-cache-drivers.md)
- [send() と on()](https://kawax.biz/jp/packages/laravel-copilot-sdk/send-on.md)


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