> ## 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/zh-TW/responses.md) 與 [Macroable trait](https://kawax.biz/zh-TW/advanced/macroable.md) 的基礎上，解說將其作為發佈套件的公開 API 維護時需要注意的邊界。

## 註冊目標是 Factory 而非回應

`Response` 這個名稱既指請求產生回應的 Facade，也指產生後的 HTTP 回應類別。官方文件中的 `Response::macro()` 擴充的是 Facade 解析到的 `ResponseFactory`。

| 對象 | 作用 | 本頁中的用法 |
| - | - | - |
| `Illuminate\Support\Facades\Response` | 解析並呼叫 `ResponseFactory` 契約的 Facade | 註冊與呼叫 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` 陳述式中混淆 Facade 與同名的回應類別。

## 公開錯誤 JSON 的產生 API

### 在 Service Provider 中註冊

在 `src/HttpServiceProvider.php` 的 `boot()` 中註冊帶有套件前綴的 Macro。關於服務提供者的載入，請參閱 [套件自動探索](https://kawax.biz/zh-TW/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/zh-TW/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": "注文を確定できませんでした。"
  }
}
```

透過 Facade 也可以呼叫同一個 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` 也是 Factory，而不是 Service Provider。因此範例中的 `$this->json()` 呼叫的是 Factory 的一般方法。

* 使用 `$this` 的 Macro 要用一般的 `function (...) { ... }` 定義。`static function` 沒有實例的 `$this`。
* 註冊 invokable 物件時，不會像 Closure 那樣重新綁定。所需的相依性應由該物件自己透過建構子等方式接收。
* Macro 的參數是呼叫端傳入的參數。即使宣告了 `Request` 等型別，Macroable 也不會從容器自動注入。

Facade 的 `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 或清除 Facade 已解析的實例，並不會清除這張表。在同一個 PHP 行程中重新建立應用程式的測試裡，註冊狀態也可能殘留。

另一方面，範例中的 Macro 每次呼叫時都由 `json()` 產生新的 `JsonResponse`。共享的是產生邏輯，而不是回應的內文或標頭本身。

在長時間執行的 worker 中應遵守的邊界如下。

1. 註冊時不要用 `use (...)` 捕捉目前的 Request、認證使用者或租戶。這些值應在呼叫時作為參數傳入。
2. 不要重複使用註冊時產生的回應。每次都回傳新的回應。
3. 如果在註冊時將設定作為值捕捉，要在規格中說明該值會在行程內保留。
4. 更新套件後，重新啟動常駐行程以讓新的註冊程式碼生效。

[套件服務的生命週期設計](https://kawax.biz/zh-TW/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/zh-TW/http-tests.md)；關於套件單獨的環境建置，請參閱 [套件測試](https://kawax.biz/zh-TW/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/zh-TW/eloquent-resources.md)。

## 長期維護檢查清單

* [ ] 在 Laravel 13 支援的組合中，服務提供者能夠啟動，並且可以從輔助函式與 Facade 兩種方式呼叫。
* [ ] 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 Facade 及其解析目標（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 trait](https://kawax.biz/zh-TW/advanced/macroable.md)
- [進階主題](https://kawax.biz/zh-TW/advanced/index.md)
- [套件的自訂快取儲存與契約](https://kawax.biz/zh-TW/advanced/package-cache-drivers.md)
- [套件的自訂 Session 驅動](https://kawax.biz/zh-TW/advanced/package-session-drivers.md)
- [套件的 HTTP 用戶端與測試](https://kawax.biz/zh-TW/advanced/package-http-client.md)


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