展開文件目錄

Eloquent: API 資源

簡介

在建立 API 時,您可能需要一個轉換層,位於您的 Eloquent 模型與實際返回給應用程式使用者的 JSON 回應之間。例如,您可能希望為某些用戶顯示特定屬性,而對其他用戶則不顯示,或者您可能希望始終在模型的 JSON 表示中包含某些關聯。Eloquent 的資源類別允許您表達性地且輕鬆地將您的模型和模型集合轉換為 JSON。

當然,您可以始終使用它們的 toJson 方法將 Eloquent 模型或集合轉換為 JSON;但是,Eloquent 資源提供了更細粒度和強大的控制,用於控制模型及其關聯的 JSON 序列化。

生成資源

要生成資源類別,您可以使用 make:resource Artisan 指令。默認情況下,資源將放置在應用程式的 app/Http/Resources 目錄中。資源擴展了 Illuminate\Http\Resources\Json\JsonResource 類別:

php artisan make:resource UserResource

資源集合

除了生成轉換單個模型的資源外,您還可以生成負責轉換模型集合的資源。這使您的 JSON 回應可以包含與給定資源集合相關的連結和其他元資訊。

要創建資源集合,您應在創建資源時使用 --collection 標誌。或者,在資源名稱中包含 Collection 一詞將告訴 Laravel 應創建一個集合資源。集合資源擴展了 Illuminate\Http\Resources\Json\ResourceCollection 類別:

php artisan make:resource User --collection

php artisan make:resource UserCollection

概念概述

[!NOTE]
這是有關資源和資源集合的高層級概述。強烈建議您閱讀本文檔的其他部分,以深入了解資源提供的自定義和功能。

在深入研究編寫資源時可用的所有選項之前,讓我們首先從高層次了解 Laravel 中如何使用資源。資源類別代表需要轉換為 JSON 結構的單個模型。例如,這是一個簡單的 UserResource 資源類別:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

每個資源類別都定義了一個 toArray 方法,該方法在將資源作為路由或控制器方法的回應返回時應轉換為 JSON 的屬性陣列。

請注意,我們可以直接從 $this 變數訪問模型屬性。這是因為資源類別將自動將屬性和方法訪問代理到底層模型,以便方便訪問。定義資源後,可以從路由或控制器返回資源。資源通過其建構子接受底層模型實例:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return new UserResource(User::findOrFail($id));
});

資源集合

如果要返回一組資源或分頁回應,則在路由或控制器中創建資源實例時,應使用資源類別提供的 collection 方法:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});

請注意,這不允許添加任何可能需要與集合一起返回的自定義元數據。如果要自定義資源集合回應,可以創建一個專用資源來表示該集合:

php artisan make:resource UserCollection

一旦生成了資源集合類別,您可以輕鬆定義應包含在回應中的任何元數據:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<int|string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

在定義完您的資源集合後,可以從路由或控制器返回它:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

保留集合鍵

當從路由返回資源集合時,Laravel 會重置集合的鍵,使其按照數字順序排列。但是,您可以在您的資源類別中添加一個 preserveKeys 屬性,指示是否應保留集合的原始鍵:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Indicates if the resource's collection keys should be preserved.
     *
     * @var bool
     */
    public $preserveKeys = true;
}

preserveKeys 屬性設置為 true 時,當從路由或控制器返回集合時,集合的鍵將被保留:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all()->keyBy->id);
});

自訂底層資源類別

通常,資源集合的 $this->collection 屬性會自動填充為將集合的每個項目映射到其單一資源類別的結果。假定單一資源類別是集合的類別名稱,不包含類別名稱末尾的 Collection 部分。此外,根據您的個人偏好,單一資源類別可能會或可能不會以 Resource 結尾。

例如,UserCollection 將嘗試將給定的使用者實例映射到 UserResource 資源。要自訂此行為,您可以覆蓋資源集合的 $collects 屬性:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * The resource that this resource collects.
     *
     * @var string
     */
    public $collects = Member::class;
}

撰寫資源

[!NOTE]
如果您尚未閱讀 概念概述,強烈建議在繼續閱讀本文件之前先閱讀。

資源只需要將給定的模型轉換為陣列。因此,每個資源都包含一個 toArray 方法,將您模型的屬性轉換為可以從應用程式的路由或控制器返回的 API 友好陣列:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

一旦定義了資源,就可以直接從路由或控制器返回它:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return new UserResource(User::findOrFail($id));
});

關聯

如果您想在回應中包含相關資源,您可以將它們添加到您的資源的 toArray 方法返回的陣列中。在這個例子中,我們將使用 PostResource 資源的 collection 方法將使用者的部落格文章添加到資源回應中:

use App\Http\Resources\PostResource;
use Illuminate\Http\Request;

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => PostResource::collection($this->posts),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

[!NOTE]
如果您只想在已經加載了關聯時包含關聯,請查看有關 條件關聯 的文件。

資源集合

雖然資源將單個模型轉換為陣列,資源集合將一組模型轉換為陣列。但是,並不絕對需要為您的每個模型定義一個資源集合類,因為所有資源都提供一個 collection 方法來動態生成一個“臨時”資源集合:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});

但是,如果您需要自定義與集合一起返回的元數據,則需要定義自己的資源集合:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

與單數資源一樣,資源集合可以直接從路由或控制器返回:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

資料包裝

默認情況下,當資源回應轉換為 JSON 時,您最外層的資源將包裝在 data 金鑰中。因此,例如,典型的資源集合回應如下所示:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ]
}

如果您想要禁用最外層資源的包裝,您應該在基本的 Illuminate\Http\Resources\Json\JsonResource 類上調用 withoutWrapping 方法。通常,您應該從您的 AppServiceProvider 或另一個服務提供者中的每個請求加載的服務提供者中調用此方法:

<?php

namespace App\Providers;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        JsonResource::withoutWrapping();
    }
}

[!WARNING]
withoutWrapping 方法僅影響最外層回應,不會刪除您手動添加到自己的資源集合中的 data 金鑰。

包裹巢狀資源

您可以完全自由地決定如何包裹您資源的關聯。如果您希望所有資源集合都被包裹在一個 data 鍵中,無論其巢狀程度如何,您應該為每個資源定義一個資源集合類別,並在 data 鍵中返回該集合。

您可能會想知道這是否會導致您最外層的資源被包裹在兩個 data 鍵中。別擔心,Laravel 永遠不會讓您的資源被意外地雙重包裹,因此您不必擔心您正在轉換的資源集合的巢狀層級:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class CommentsCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return ['data' => $this->collection];
    }
}

資料包裹和分頁

當通過資源回應返回分頁集合時,即使調用了 withoutWrapping 方法,Laravel 也會將您的資源數據包裹在一個 data 鍵中。這是因為分頁響應始終包含有關分頁器狀態的 metalinks 鍵:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "links":{
        "first": "http://example.com/users?page=1",
        "last": "http://example.com/users?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/users",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

分頁

您可以將 Laravel 分頁器實例傳遞給資源的 collection 方法或自定義資源集合:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::paginate());
});

分頁響應始終包含有關分頁器狀態的 metalinks 鍵:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "links":{
        "first": "http://example.com/users?page=1",
        "last": "http://example.com/users?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/users",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

自定義分頁信息

如果您想自定義分頁回應的 linksmeta 鍵中包含的信息,您可以在資源上定義一個 paginationInformation 方法。該方法將接收 $paginated 數據和包含 linksmeta 鍵的 $default 信息數組:

/**
 * Customize the pagination information for the resource.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  array $paginated
 * @param  array $default
 * @return array
 */
public function paginationInformation($request, $paginated, $default)
{
    $default['links']['custom'] = 'https://example.com';

    return $default;
}

條件屬性

有時,您可能希望僅在符合特定條件時才在資源回應中包含屬性。例如,如果當前用戶是 "管理員",則可能只希望包含某個值。Laravel 提供了各種輔助方法來幫助您應對這種情況。when 方法可用於有條件地將屬性添加到資源回應中:

在這個範例中,如果驗證使用者的 isAdmin 方法返回 true,則 secret 金鑰只會在最終資源回應中返回。如果該方法返回 false,則在將資源回應發送給客戶端之前,secret 金鑰將從中刪除。when 方法允許您在構建陣列時表達式地定義資源,而無需使用條件語句。

when 方法還接受閉包作為其第二個引數,只有在給定條件為 true 時才計算結果值:

'secret' => $this->when($request->user()->isAdmin(), function () {
    return 'secret-value';
}),

whenHas 方法可用於僅在底層模型上實際存在屬性時包含該屬性:

'name' => $this->whenHas('name'),

此外,whenNotNull 方法可用於在資源回應中包含屬性,如果該屬性不為空:

'name' => $this->whenNotNull($this->name),

合併條件屬性

有時您可能有幾個屬性應僅基於相同條件包含在資源回應中。在這種情況下,您可以使用 mergeWhen 方法僅在給定條件為 true 時將屬性包含在回應中:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        $this->mergeWhen($request->user()->isAdmin(), [
            'first-secret' => 'value',
            'second-secret' => 'value',
        ]),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

同樣,如果給定條件為 false,則這些屬性將在將資源回應發送給客戶端之前從中刪除。

[!WARNING]
mergeWhen 方法不應在混合字符串和數字鍵的陣列中使用。此外,不應在具有非按順序排列的數字鍵的陣列中使用。

條件關聯

除了有條件地加載屬性之外,您還可以根據模型上已經加載的關聯有條件地在資源回應中包含關聯。這使得您的控制器可以決定應該在模型上加載哪些關聯,而您的資源只有在實際加載時才能輕鬆地包含它們。最終,這使得更容易避免資源中的 "N+1" 查詢問題。

whenLoaded 方法可用於條件性地加載關聯。為了避免不必要地加載關聯,此方法接受關聯的名稱而不是關聯本身:

use App\Http\Resources\PostResource;

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => PostResource::collection($this->whenLoaded('posts')),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

在這個例子中,如果關聯尚未被加載,則在將資源回應發送給客戶端之前,posts 鍵將從中刪除。

條件性關聯計數

除了條件性地包含關聯外,您還可以根據模型上的關聯計數是否已加載,在資源回應中條件性地包含關聯的「計數」:

new UserResource($user->loadCount('posts'));

whenCounted 方法可用於在資源回應中條件性地包含關聯的計數。如果關聯的計數不存在,此方法將避免不必要地包含該屬性:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts_count' => $this->whenCounted('posts'),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

在這個例子中,如果 posts 關聯的計數尚未被加載,則在將資源回應發送給客戶端之前,posts_count 鍵將從中刪除。

其他類型的聚合,如 avgsumminmax,也可以使用 whenAggregated 方法進行條件性加載:

'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
'words_min' => $this->whenAggregated('posts', 'words', 'min'),
'words_max' => $this->whenAggregated('posts', 'words', 'max'),

條件性樞紐資訊

除了在資源回應中條件性地包含關聯資訊外,您還可以使用 whenPivotLoaded 方法條件性地包含多對多關係的中介表中的資料。whenPivotLoaded 方法將中介表的名稱作為第一個參數。第二個參數應該是一個返回值的閉包,如果中介資訊在模型上可用,則返回該值:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'expires_at' => $this->whenPivotLoaded('role_user', function () {
            return $this->pivot->expires_at;
        }),
    ];
}

如果您的關聯使用自定義中介表模型,您可以將中介表模型的實例作為 whenPivotLoaded 方法的第一個參數傳遞:

'expires_at' => $this->whenPivotLoaded(new Membership, function () {
    return $this->pivot->expires_at;
}),

如果您的中介表使用的取值器不是 pivot,您可以使用 whenPivotLoadedAs 方法:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
            return $this->subscription->expires_at;
        }),
    ];
}

添加元數據

一些 JSON API 標準要求在您的資源和資源集合回應中添加元數據。這通常包括像是 links 到資源或相關資源的東西,或者關於資源本身的元數據。如果您需要返回有關資源的額外元數據,請將其包含在您的 toArray 方法中。例如,當轉換資源集合時,您可能會包含 links 資訊:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'data' => $this->collection,
        'links' => [
            'self' => 'link-value',
        ],
    ];
}

當從您的資源返回額外元數據時,您永遠不必擔心意外覆蓋 Laravel 在返回分頁回應時自動添加的 linksmeta 關鍵字。您定義的任何額外 links 將與分頁器提供的連結合併。

頂層元數據

有時您可能希望僅在資源回應是最外層返回的資源時才包含某些元數據。通常,這包括關於整個回應的元信息。要定義這些元數據,請在您的資源類別中添加一個 with 方法。此方法應該返回一個要與資源回應一起包含的元數據陣列,僅當資源是被轉換的最外層資源時:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    /**
     * Get additional data that should be returned with the resource array.
     *
     * @return array<string, mixed>
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'key' => 'value',
            ],
        ];
    }
}

構建資源時添加元數據

您還可以在路由或控制器中構建資源實例時添加頂層資料。additional 方法可用於所有資源,接受一個要添加到資源回應中的資料陣列:

return (new UserCollection(User::all()->load('roles')))
    ->additional(['meta' => [
        'key' => 'value',
    ]]);

資源回應

正如您已經閱讀的那樣,資源可以直接從路由和控制器返回:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return new UserResource(User::findOrFail($id));
});

然而,有時您可能需要在將 HTTP 回應發送給客戶端之前自定義輸出的 HTTP 回應。有兩種方法可以實現這一點。首先,您可以將 response 方法鏈接到資源上。該方法將返回一個 Illuminate\Http\JsonResponse 實例,讓您完全控制回應的標頭:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user', function () {
    return (new UserResource(User::find(1)))
        ->response()
        ->header('X-Value', 'True');
});

或者,您可以在資源本身中定義一個 withResponse 方法。當資源作為回應中最外層的資源返回時,將調用此方法:

<?php

namespace App\Http\Resources;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * Customize the outgoing response for the resource.
     */
    public function withResponse(Request $request, JsonResponse $response): void
    {
        $response->header('X-Value', 'True');
    }
}