跳至內容

Eloquent:API 資源

簡介

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

當然,您始終可以使用 Eloquent 模型或集合的 toJson 方法將它們轉換為 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

概念概述

lightbulb

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

在深入探討撰寫資源時可用的所有選項之前,我們先來高階地了解一下資源在 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;
}

撰寫資源

lightbulb

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

資源只需要將給定的模型轉換為陣列。因此,每個資源都包含一個 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,
];
}
lightbulb

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

資源集合

雖然資源會將單一模型轉換為陣列,但資源集合會將模型集合轉換為陣列。但是,對於每個模型都定義資源集合類別並非絕對必要,因為所有資源都提供 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": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
]
}

如果您想要停用最外層資源的包裝,您應該在基礎 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();
}
}
exclamation

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": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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 資料和 $default 資訊的陣列,該陣列包含 linksmeta 鍵。

/**
* 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 方法有條件地將屬性新增到資源回應中。

/**
* 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,
'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}

在此範例中,只有當已驗證使用者的 isAdmin 方法返回 true 時,才會在最終資源回應中返回 secret 鍵。如果該方法返回 false,則在將資源回應傳送給用戶端之前,會從資源回應中移除 secret 鍵。當建構陣列時,when 方法允許您明確地定義資源,而無需使用條件語句。

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

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

如果底層模型上實際存在某個屬性,則可以使用 whenHas 方法來包含該屬性。

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

此外,如果屬性不為 null,則可以使用 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,這些屬性會在資源回應傳送至用戶端之前從資源回應中移除。

exclamation

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 標準要求將 meta 資料新增至您的資源和資源集合回應。這通常包含諸如資源或相關資源的 links,或是關於資源本身的 meta 資料。如果您需要返回關於資源的其他 meta 資料,請將其包含在您的 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',
],
];
}

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

頂層 Meta 資料

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

<?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',
],
];
}
}

建構資源時新增 Meta 資料

您也可以在您的路由或控制器中建構資源實例時新增頂層資料。所有資源都可使用的 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 回應傳送至用戶端之前對其進行自訂。有兩種方法可以實現此目的。首先,您可以將 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');
}
}