跳到內容

Eloquent:API 資源

簡介

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

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

產生資源

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

1php artisan make:resource UserResource

資源集合

除了產生轉換個別模型的資源外,您還可以產生負責轉換模型集合的資源。這允許您的 JSON 回應包含連結和其他與給定資源的整個集合相關的 Meta 資訊。

要建立資源集合,您應該在建立資源時使用 --collection 標誌。或者,在資源名稱中包含單字 Collection 將向 Laravel 指示它應該建立一個集合資源。集合資源擴展了 Illuminate\Http\Resources\Json\ResourceCollection 類別

1php artisan make:resource User --collection
2 
3php artisan make:resource UserCollection

概念概述

這是資源和資源集合的高階概述。強烈建議您閱讀本文件的其他章節,以更深入地了解資源為您提供的自訂和功能。

在深入探討編寫資源時可用的所有選項之前,我們先來高階地看一下資源如何在 Laravel 中使用。資源類別代表需要轉換為 JSON 結構的單一模型。例如,這是一個簡單的 UserResource 資源類別

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7 
8class UserResource extends JsonResource
9{
10 /**
11 * Transform the resource into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'id' => $this->id,
19 'name' => $this->name,
20 'email' => $this->email,
21 'created_at' => $this->created_at,
22 'updated_at' => $this->updated_at,
23 ];
24 }
25}

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

請注意,我們可以從 $this 變數直接存取模型屬性。這是因為資源類別將自動將屬性和方法存取代理到基礎模型,以便於存取。一旦定義了資源,就可以從路由或控制器返回它。資源透過其建構子接受基礎模型實例

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user/{id}', function (string $id) {
5 return new UserResource(User::findOrFail($id));
6});

資源集合

如果您要返回資源集合或分頁回應,您應該在路由或控制器中建立資源實例時使用資源類別提供的 collection 方法

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return UserResource::collection(User::all());
6});

請注意,這不允許新增任何可能需要與您的集合一起返回的自訂 Meta 資料。如果您想自訂資源集合回應,您可以建立一個專用資源來表示該集合

1php artisan make:resource UserCollection

一旦產生了資源集合類別,您就可以輕鬆定義任何應包含在回應中的 Meta 資料

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\ResourceCollection;
7 
8class UserCollection extends ResourceCollection
9{
10 /**
11 * Transform the resource collection into an array.
12 *
13 * @return array<int|string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'data' => $this->collection,
19 'links' => [
20 'self' => 'link-value',
21 ],
22 ];
23 }
24}

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

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::all());
6});

保留集合鍵

從路由返回資源集合時,Laravel 會重設集合的鍵,使其按數字順序排列。但是,您可以將 preserveKeys 屬性新增到您的資源類別,以指示是否應保留集合的原始鍵

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\JsonResource;
6 
7class UserResource extends JsonResource
8{
9 /**
10 * Indicates if the resource's collection keys should be preserved.
11 *
12 * @var bool
13 */
14 public $preserveKeys = true;
15}

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

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return UserResource::collection(User::all()->keyBy->id);
6});

自訂基礎資源類別

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

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

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class UserCollection extends ResourceCollection
8{
9 /**
10 * The resource that this resource collects.
11 *
12 * @var string
13 */
14 public $collects = Member::class;
15}

編寫資源

如果您尚未閱讀概念概述,強烈建議您在繼續閱讀本文檔之前先閱讀它。

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

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7 
8class UserResource extends JsonResource
9{
10 /**
11 * Transform the resource into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'id' => $this->id,
19 'name' => $this->name,
20 'email' => $this->email,
21 'created_at' => $this->created_at,
22 'updated_at' => $this->updated_at,
23 ];
24 }
25}

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

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user/{id}', function (string $id) {
5 return new UserResource(User::findOrFail($id));
6});

關聯

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

1use App\Http\Resources\PostResource;
2use Illuminate\Http\Request;
3 
4/**
5 * Transform the resource into an array.
6 *
7 * @return array<string, mixed>
8 */
9public function toArray(Request $request): array
10{
11 return [
12 'id' => $this->id,
13 'name' => $this->name,
14 'email' => $this->email,
15 'posts' => PostResource::collection($this->posts),
16 'created_at' => $this->created_at,
17 'updated_at' => $this->updated_at,
18 ];
19}

如果您只想在已載入關聯時才包含它們,請查看關於條件關聯的文件。

資源集合

雖然資源將單一模型轉換為陣列,但資源集合將模型集合轉換為陣列。但是,並非絕對需要為您的每個模型定義資源集合類別,因為所有資源都提供了一個 collection 方法來動態產生「臨時」資源集合

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return UserResource::collection(User::all());
6});

但是,如果您需要自訂與集合一起返回的 Meta 資料,則必須定義您自己的資源集合

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\ResourceCollection;
7 
8class UserCollection extends ResourceCollection
9{
10 /**
11 * Transform the resource collection into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'data' => $this->collection,
19 'links' => [
20 'self' => 'link-value',
21 ],
22 ];
23 }
24}

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

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::all());
6});

資料包裝

預設情況下,當資源回應轉換為 JSON 時,您最外層的資源會包裝在 data 鍵中。因此,例如,典型的資源集合回應如下所示

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "[email protected]"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "[email protected]"
12 }
13 ]
14}

如果您想停用最外層資源的包裝,您應該在基礎 Illuminate\Http\Resources\Json\JsonResource 類別上調用 withoutWrapping 方法。通常,您應該從您的 AppServiceProvider 或另一個在每次請求時載入到您的應用程式的服務提供者中調用此方法

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Http\Resources\Json\JsonResource;
6use Illuminate\Support\ServiceProvider;
7 
8class AppServiceProvider extends ServiceProvider
9{
10 /**
11 * Register any application services.
12 */
13 public function register(): void
14 {
15 // ...
16 }
17 
18 /**
19 * Bootstrap any application services.
20 */
21 public function boot(): void
22 {
23 JsonResource::withoutWrapping();
24 }
25}

withoutWrapping 方法僅影響最外層的回應,並且不會移除您手動新增到自己的資源集合的 data 鍵。

包裝巢狀資源

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

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

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class CommentsCollection extends ResourceCollection
8{
9 /**
10 * Transform the resource collection into an array.
11 *
12 * @return array<string, mixed>
13 */
14 public function toArray(Request $request): array
15 {
16 return ['data' => $this->collection];
17 }
18}

資料包裝和分頁

透過資源回應返回分頁集合時,即使已調用 withoutWrapping 方法,Laravel 也會將您的資源資料包裝在 data 鍵中。這是因為分頁回應始終包含 metalinks 鍵,其中包含有關分頁器狀態的資訊

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "[email protected]"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "[email protected]"
12 }
13 ],
14 "links":{
15 "first": "http://example.com/users?page=1",
16 "last": "http://example.com/users?page=1",
17 "prev": null,
18 "next": null
19 },
20 "meta":{
21 "current_page": 1,
22 "from": 1,
23 "last_page": 1,
24 "path": "http://example.com/users",
25 "per_page": 15,
26 "to": 10,
27 "total": 10
28 }
29}

分頁

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

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::paginate());
6});

分頁回應始終包含 metalinks 鍵,其中包含有關分頁器狀態的資訊

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "[email protected]"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "[email protected]"
12 }
13 ],
14 "links":{
15 "first": "http://example.com/users?page=1",
16 "last": "http://example.com/users?page=1",
17 "prev": null,
18 "next": null
19 },
20 "meta":{
21 "current_page": 1,
22 "from": 1,
23 "last_page": 1,
24 "path": "http://example.com/users",
25 "per_page": 15,
26 "to": 10,
27 "total": 10
28 }
29}

自訂分頁資訊

如果您想自訂分頁回應的 linksmeta 鍵中包含的資訊,您可以在資源上定義 paginationInformation 方法。此方法將接收 $paginated 資料和 $default 資訊的陣列,$default 資訊是一個包含 linksmeta 鍵的陣列

1/**
2 * Customize the pagination information for the resource.
3 *
4 * @param \Illuminate\Http\Request $request
5 * @param array $paginated
6 * @param array $default
7 * @return array
8 */
9public function paginationInformation($request, $paginated, $default)
10{
11 $default['links']['custom'] = 'https://example.com';
12 
13 return $default;
14}

條件屬性

有時,您可能希望僅在滿足給定條件時才在資源回應中包含屬性。例如,您可能希望僅在目前使用者是「管理員」時才包含值。Laravel 提供了各種輔助方法來協助您處理這種情況。when 方法可用於有條件地將屬性新增到資源回應中

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
13 'created_at' => $this->created_at,
14 'updated_at' => $this->updated_at,
15 ];
16}

在此範例中,只有在經過身份驗證的使用者的 isAdmin 方法返回 true 時,secret 鍵才會在最終資源回應中返回。如果該方法返回 false,則在將 secret 鍵發送到客戶端之前,將從資源回應中移除它。when 方法讓您可以表達性地定義您的資源,而無需在建構陣列時訴諸條件陳述式。

when 方法也接受閉包作為其第二個參數,允許您僅在給定條件為 true 時才計算結果值

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

如果屬性實際存在於基礎模型上,則可以使用 whenHas 方法來包含屬性

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

此外,如果屬性不是 null,則可以使用 whenNotNull 方法在資源回應中包含屬性

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

合併條件屬性

有時,您可能有多個屬性應僅在滿足相同條件的情況下才包含在資源回應中。在這種情況下,您可以使用 mergeWhen 方法僅在給定條件為 true 時才在回應中包含這些屬性

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 $this->mergeWhen($request->user()->isAdmin(), [
13 'first-secret' => 'value',
14 'second-secret' => 'value',
15 ]),
16 'created_at' => $this->created_at,
17 'updated_at' => $this->updated_at,
18 ];
19}

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

mergeWhen 方法不應在混合字串和數字鍵的陣列中使用。此外,它不應在具有非順序數字鍵的陣列中使用。

條件關聯

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

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

1use App\Http\Resources\PostResource;
2 
3/**
4 * Transform the resource into an array.
5 *
6 * @return array<string, mixed>
7 */
8public function toArray(Request $request): array
9{
10 return [
11 'id' => $this->id,
12 'name' => $this->name,
13 'email' => $this->email,
14 'posts' => PostResource::collection($this->whenLoaded('posts')),
15 'created_at' => $this->created_at,
16 'updated_at' => $this->updated_at,
17 ];
18}

在此範例中,如果關聯尚未載入,則在將 posts 鍵發送到客戶端之前,將從資源回應中移除它。

條件關聯計數

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

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

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

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 'posts_count' => $this->whenCounted('posts'),
13 'created_at' => $this->created_at,
14 'updated_at' => $this->updated_at,
15 ];
16}

在此範例中,如果 posts 關聯的計數尚未載入,則在將 posts_count 鍵發送到客戶端之前,將從資源回應中移除它。

其他類型的聚合,例如 avgsumminmax 也可以使用 whenAggregated 方法有條件地載入

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

條件樞紐資訊

除了有條件地在資源回應中包含關聯資訊外,您還可以有條件地使用 whenPivotLoaded 方法包含來自多對多關聯的中間表資料。whenPivotLoaded 方法接受樞紐表的名稱作為其第一個參數。第二個參數應該是一個閉包,如果模型上提供了樞紐資訊,則返回要返回的值

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'expires_at' => $this->whenPivotLoaded('role_user', function () {
12 return $this->pivot->expires_at;
13 }),
14 ];
15}

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

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

如果您的中間表使用 pivot 以外的存取器,您可以使用 whenPivotLoadedAs 方法

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
12 return $this->subscription->expires_at;
13 }),
14 ];
15}

新增 Meta 資料

某些 JSON API 標準要求將 Meta 資料新增到您的資源和資源集合回應中。這通常包括資源或相關資源的 links 之類的東西,或關於資源本身的 Meta 資料。如果您需要返回關於資源的其他 Meta 資料,請將其包含在您的 toArray 方法中。例如,您可以在轉換資源集合時包含 links 資訊

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'data' => $this->collection,
10 'links' => [
11 'self' => 'link-value',
12 ],
13 ];
14}

當從您的資源返回其他 Meta 資料時,您永遠不必擔心意外覆蓋 Laravel 在返回分頁回應時自動新增的 linksmeta 鍵。您定義的任何其他 links 都將與分頁器提供的連結合併。

頂層 Meta 資料

有時,您可能希望僅在資源是最外層返回的資源時才包含某些 Meta 資料。通常,這包括關於整個回應的 Meta 資訊。要定義此 Meta 資料,請將 with 方法新增到您的資源類別。此方法應返回一個 Meta 資料陣列,僅當資源是最外層正在轉換的資源時才包含在資源回應中

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class UserCollection extends ResourceCollection
8{
9 /**
10 * Transform the resource collection into an array.
11 *
12 * @return array<string, mixed>
13 */
14 public function toArray(Request $request): array
15 {
16 return parent::toArray($request);
17 }
18 
19 /**
20 * Get additional data that should be returned with the resource array.
21 *
22 * @return array<string, mixed>
23 */
24 public function with(Request $request): array
25 {
26 return [
27 'meta' => [
28 'key' => 'value',
29 ],
30 ];
31 }
32}

在建構資源時新增 Meta 資料

您也可以在路由或控制器中建構資源實例時新增頂層資料。additional 方法在所有資源上都可用,它接受一個應新增到資源回應的資料陣列

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

資源回應

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

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user/{id}', function (string $id) {
5 return new UserResource(User::findOrFail($id));
6});

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

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user', function () {
5 return (new UserResource(User::find(1)))
6 ->response()
7 ->header('X-Value', 'True');
8});

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

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\JsonResponse;
6use Illuminate\Http\Request;
7use Illuminate\Http\Resources\Json\JsonResource;
8 
9class UserResource extends JsonResource
10{
11 /**
12 * Transform the resource into an array.
13 *
14 * @return array<string, mixed>
15 */
16 public function toArray(Request $request): array
17 {
18 return [
19 'id' => $this->id,
20 ];
21 }
22 
23 /**
24 * Customize the outgoing response for the resource.
25 */
26 public function withResponse(Request $request, JsonResponse $response): void
27 {
28 $response->header('X-Value', 'True');
29 }
30}