API 資源
API 資源
Eloquent: API 資源
簡介
當(dāng)構(gòu)建 API 時(shí),你往往需要一個(gè)轉(zhuǎn)換層來聯(lián)結(jié)你的 Eloquent 模型和實(shí)際返回給用戶的 JSON 響應(yīng)。Laravel 的資源類能夠讓你以更直觀簡便的方式將模型和模型集合轉(zhuǎn)化成 JSON。
生成資源
你可以使用 make:resource
Artisan 命令來生成一個(gè)資源類。默認(rèn)情況下生成的資源都會(huì)被放置在應(yīng)用程序的 app/Http/Resources
文件夾下。資源繼承自 Illuminate\Http\Resources\Json\JsonResource
類:
php artisan make:resource User
資源集合
除了生成資源轉(zhuǎn)換單個(gè)模型外,你還可以生成資源集合用來轉(zhuǎn)換模型的集合。這允許你在響應(yīng)中包含與給定資源相關(guān)的鏈接與其他元信息。
你需要在生成資源時(shí)添加 --collection
標(biāo)志以生成一個(gè)資源集合?;蛘?,你也可以直接在資源的名稱中包含 Collection
向 Laravel 表示應(yīng)該生成一個(gè)資源集合。資源集合繼承自 Illuminate\Http\Resources\Json\ResourceCollection
類:
php artisan make:resource Users --collection php artisan make:resource UserCollection
概念綜述
{tip} 這是對資源和資源集合的高度概述。強(qiáng)烈建議你閱讀本文檔的其他部分,以深入了解如何更好地自定義和使用資源。
在深入了解如何定制化編寫你的資源之前,讓我們先來看看在 Laravel 中如何使用資源。一個(gè)資源類表示一個(gè)單一模型需要被轉(zhuǎn)換成 JSON 格式。例如,現(xiàn)在我們有一個(gè)簡單的 User
資源類:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 將資源轉(zhuǎn)換成數(shù)組。 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; } }
每一個(gè)資源類都定義了一個(gè) toArray
方法,在發(fā)送響應(yīng)時(shí)它會(huì)返回應(yīng)該被轉(zhuǎn)化成 JSON 的屬性數(shù)組。注意在這里我們可以直接使用 $this
變量來訪問模型屬性。這是因?yàn)橘Y源類將自動(dòng)代理屬性和方法到底層模型以方便訪問。你可以在路由或控制器中返回已定義的資源:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });
資源集合
你可以在路由或者控制器中使用 collection
方法來創(chuàng)建資源實(shí)例,以返回多個(gè)資源的集合或分頁響應(yīng):
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()); });
當(dāng)然了,使用如上方法你將不能添加任何附加的元數(shù)據(jù)和集合一起返回。如果你需要自定義資源集合響應(yīng),你需要?jiǎng)?chuàng)建一個(gè)專用的資源來表示集合:
php artisan make:resource UserCollection
你可以輕松的在已生成的資源集合類中定義任何你想在響應(yīng)中返回的元數(shù)據(jù):
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 將資源集合轉(zhuǎn)換成數(shù)組。 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; } }
你可以在路由或者控制器中返回已定義的資源集合:
use App\User; use App\Http\Resources\UserCollection; Route::get('/users', function () { return new UserCollection(User::all()); });
保護(hù)集合的鍵
當(dāng)從路由返回資源集合時(shí),Laravel 將重置集合的鍵,使它們以簡單的數(shù)字順序。但是,可以將 preserveKeys
屬性添加到資源類中,指示是否應(yīng)保留集合鍵:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 指示是否應(yīng)保留資源的集合鍵。 * * @var bool */ public $preserveKeys = true; }
當(dāng) preserveKeys
屬性被設(shè)置為 true
, 集合的鍵將會(huì)被保護(hù):
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()->keyBy->id); });
自定義基礎(chǔ)資源類
通常,資源集合的 $this->collection
屬性會(huì)自動(dòng)填充,結(jié)果是將集合的每個(gè)項(xiàng)映射到其單個(gè)資源類。假定單一資源類是集合的類名,但結(jié)尾沒有 Collection
字符串。
例如,UserCollection
將給定的用戶實(shí)例映射到 User
資源中。若要自定義此行為,你可以重寫資源集合的 $collects
屬性:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * collects 屬性定義了資源類。 * * @var string */ public $collects = 'App\Http\Resources\Member'; }
編寫資源
{tip} 如果你還沒有閱讀 概念綜述,那么在繼續(xù)閱讀本文檔前,強(qiáng)烈建議你去閱讀一下。
從本質(zhì)上來說,資源的作用很簡單。它們只需要將一個(gè)給定的模型轉(zhuǎn)換成一個(gè)數(shù)組。所以每一個(gè)資源都包含一個(gè) toArray
方法用來將你的模型屬性轉(zhuǎn)換成一個(gè)可以返回給用戶的 API 友好數(shù)組:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 將資源轉(zhuǎn)換成數(shù)組。 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; } }
你可以在路由或者控制器中返回已經(jīng)定義的資源:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });
關(guān)聯(lián)
如果你希望在響應(yīng)中包含關(guān)聯(lián)資源,你只需要將它們添加到 toArray
方法返回的數(shù)組中。在下面這個(gè)例子里,我們將使用 Post
資源的 collection
方法將用戶的文章添加到資源響應(yīng)中:
/** * 將資源轉(zhuǎn)換成數(shù)組。 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ 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, ]; }
{tip} 如果你只想在關(guān)聯(lián)已經(jīng)加載時(shí)才添加關(guān)聯(lián)資源,請查看文檔 條件關(guān)聯(lián) 。
資源集合
資源是將單個(gè)模型轉(zhuǎn)換成數(shù)組,而資源集合是將多個(gè)模型的集合轉(zhuǎn)換成數(shù)組。所有的資源都提供了一個(gè) collection
方法來生成一個(gè) 「臨時(shí)」 資源集合,所以你沒有必要為每一個(gè)模型類型都編寫一個(gè)資源集合類:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()); });
不過,如果你需要自定義返回集合的元數(shù)據(jù),則仍需要定義一個(gè)資源集合:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 將資源集合轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; } }
和單個(gè)資源一樣,你可以在路由或控制器中直接返回資源集合:
use App\User; use App\Http\Resources\UserCollection; Route::get('/users', function () { return new UserCollection(User::all()); });
數(shù)據(jù)包裹
默認(rèn)情況下,當(dāng)資源響應(yīng)被轉(zhuǎn)換成 JSON 時(shí),頂層資源將會(huì)被包裹在 data
鍵中。因此一個(gè)典型的資源集合響應(yīng)如下所示:
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", "email": "therese28@example.com", }, { "id": 2, "name": "Liliana Mayert", "email": "evandervort@example.com", } ] }
你可以使用資源基類的 withoutWrapping
方法來禁用頂層資源的包裹。通常,你應(yīng)該在 AppServiceProvider
或其他在程序每一個(gè)請求中都會(huì)被加載的 服務(wù)提供者 中調(diào)用此方法:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Http\Resources\Json\Resource; class AppServiceProvider extends ServiceProvider{ /** * 在注冊后進(jìn)行服務(wù)的啟動(dòng) * * @return void */ public function boot() { Resource::withoutWrapping(); } /** * 在容器中注冊綁定 * * @return void */ public function register() { // } }
{note}
withoutWrappin
方法只會(huì)禁用頂層資源的包裹,不會(huì)刪除你手動(dòng)添加到資源集合中的data
鍵。
包裹嵌套資源
你可以完全自由地決定資源關(guān)聯(lián)如何被包裹。如果你希望無論怎樣嵌套,都將所有資源集合包裹在 data
鍵中,那么你需要為每個(gè)資源都定義一個(gè)資源集合類,并將返回的集合包裹在 data
鍵中。
當(dāng)然,你可能會(huì)擔(dān)心這樣頂層資源將會(huì)被包裹在兩個(gè) data
鍵中。請放心, Laravel 將永遠(yuǎn)不會(huì)讓你的資源被雙層包裹,因此你不必?fù)?dān)心被轉(zhuǎn)換的資源集合會(huì)被多重嵌套:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class CommentsCollection extends ResourceCollection{ /** * 將資源集合轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return ['data' => $this->collection]; } }
數(shù)據(jù)包裹和分頁
當(dāng)在資源響應(yīng)中返回分頁集合時(shí),即使你調(diào)用了 withoutWrapping
方法, Laravel 也會(huì)將你的資源數(shù)據(jù)包裹在 data
鍵中。這是因?yàn)榉猪擁憫?yīng)中總會(huì)有 meta
和 links
鍵包含著分頁狀態(tài)信息:
{ "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/pagination?page=1", "last": "http://example.com/pagination?page=1", "prev": null, "next": null }, "meta":{ "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/pagination", "per_page": 15, "to": 10, "total": 10 } }
分頁
你可以將分頁實(shí)例傳遞給資源的 collection
方法或者自定義的資源集合:
use App\User; use App\Http\Resources\UserCollection; Route::get('/users', function () { return new UserCollection(User::paginate()); });
分頁響應(yīng)中總有 meta
和 links
鍵包含著分頁狀態(tài)信息:
{ "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/pagination?page=1", "last": "http://example.com/pagination?page=1", "prev": null, "next": null }, "meta":{ "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/pagination", "per_page": 15, "to": 10, "total": 10 } }
條件屬性
有些時(shí)候,你可能希望在給定條件滿足時(shí)添加屬性到資源響應(yīng)里。例如,你可能希望如果當(dāng)前用戶是 「管理員」 時(shí)添加某個(gè)值到資源響應(yīng)中。在這種情況下 Laravel 提供了一些輔助方法來幫助你解決問題。 when
方法可以被用來有條件地向資源響應(yīng)添加屬性:
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'secret' => $this->when(Auth::user()->isAdmin(), 'secret-value'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }
在上面這個(gè)例子中,只有當(dāng) isAdmin
方法返回 true
時(shí), secret
鍵才會(huì)最終在資源響應(yīng)中被返回。如果該方法返回 false
,則 secret
鍵將會(huì)在資源響應(yīng)被發(fā)送給客戶端之前被刪除。 when
方法可以使你避免使用條件語句拼接數(shù)組,轉(zhuǎn)而用更優(yōu)雅的方式來編寫你的資源。
when
方法也接受閉包作為其第二個(gè)參數(shù),只有在給定條件為 true
時(shí),才從閉包中計(jì)算返回的值:
'secret' => $this->when(Auth::user()->isAdmin(), function () { return 'secret-value'; }),
有條件的合并數(shù)據(jù)
有些時(shí)候,你可能希望在給定條件滿足時(shí)添加多個(gè)屬性到資源響應(yīng)里。在這種情況下,你可以使用 mergeWhen
方法在給定的條件為 true
時(shí)將多個(gè)屬性添加到響應(yīng)中:
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, $this->mergeWhen(Auth::user()->isAdmin(), [ 'first-secret' => 'value', 'second-secret' => 'value', ]), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }
同理,如果給定的條件為 false
時(shí),則這些屬性將會(huì)在資源響應(yīng)被發(fā)送給客戶端之前被移除。
{note}
mergeWhen
方法不應(yīng)該被使用在混合字符串和數(shù)字鍵的數(shù)組中。此外,它也不應(yīng)該被使用在不按順序排列的數(shù)字鍵的數(shù)組中。
條件關(guān)聯(lián)
除了有條件地添加屬性之外,你還可以根據(jù)模型關(guān)聯(lián)是否已加載來有條件地在你的資源響應(yīng)中包含關(guān)聯(lián)。這允許你在控制器中決定加載哪些模型關(guān)聯(lián),這樣你的資源可以在模型關(guān)聯(lián)被加載后才添加它們。
這樣做可以避免在你的資源中出現(xiàn) 「N+1」 查詢問題。你應(yīng)該使用 whenLoaded
方法來有條件的加載關(guān)聯(lián)。為了避免加載不必要的關(guān)聯(lián),此方法接受關(guān)聯(lián)的名稱而不是關(guān)聯(lián)本身作為其參數(shù):
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ 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, ]; }
在上面這個(gè)例子中,如果關(guān)聯(lián)沒有被加載,則 posts
鍵將會(huì)在資源響應(yīng)被發(fā)送給客戶端之前被刪除。
條件中間表信息
除了在你的資源響應(yīng)中有條件地包含關(guān)聯(lián)外,你還可以使用 whenPivotLoaded
方法有條件地從多對多關(guān)聯(lián)的中間表中添加數(shù)據(jù)。 whenPivotLoaded
方法接受的第一個(gè)參數(shù)為中間表的名稱。第二個(gè)參數(shù)是一個(gè)閉包,它定義了在模型上如果中間表信息可用時(shí)要返回的值:
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoaded('role_user', function () { return $this->pivot->expires_at; }), ]; }
如果你的中間表使用的是 pivot
以外的訪問器,你可以使用 whenPivotLoadedAs
方法:
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () { return $this->subscription->expires_at; }), ]; }
添加元數(shù)據(jù)
一些 JSON API 標(biāo)準(zhǔn)需要你在資源和資源集合響應(yīng)中添加元數(shù)據(jù)。這通常包括資源或相關(guān)資源的 links
,或一些關(guān)于資源本身的元數(shù)據(jù)。如果你需要返回有關(guān)資源的其他元數(shù)據(jù),只需要將它們包含在 toArray
方法中即可。例如在轉(zhuǎn)換資源集合時(shí)你可能需要添加 links
信息:
/** * 將資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }
當(dāng)添加額外的元數(shù)據(jù)到你的資源中時(shí),你不必?fù)?dān)心會(huì)覆蓋 Laravel 在返回分頁響應(yīng)時(shí)自動(dòng)添加的 links
或 meta
鍵。你添加的任何其他 links
會(huì)與分頁響應(yīng)添加的 links
相合并。
頂層元數(shù)據(jù)
有時(shí)候你可能希望當(dāng)資源被作為頂層資源返回時(shí)添加某些元數(shù)據(jù)到資源響應(yīng)中。這通常包括整個(gè)響應(yīng)的元信息。你可以在資源類中添加 with
方法來定義元數(shù)據(jù)。此方法應(yīng)返回一個(gè)元數(shù)據(jù)數(shù)組,當(dāng)資源被作為頂層資源渲染時(shí),這個(gè)數(shù)組將會(huì)被包含在資源響應(yīng)中:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 將資源集合轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return parent::toArray($request); } /** * 返回應(yīng)該和資源一起返回的其他數(shù)據(jù)數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function with($request) { return [ 'meta' => [ 'key' => 'value', ], ]; } }
構(gòu)造資源時(shí)添加元數(shù)據(jù)
你還可以在路由或者控制器中構(gòu)造資源實(shí)例時(shí)添加頂層數(shù)據(jù)。所有資源都可以使用 additional
方法來接受應(yīng)該被添加到資源響應(yīng)中的數(shù)據(jù)數(shù)組:
return (new UserCollection(User::all()->load('roles'))) ->additional(['meta' => [ 'key' => 'value', ] ]);
響應(yīng)資源
就像你知道的那樣, 資源可以直接在路由和控制器中被返回:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });
但有些時(shí)候,在發(fā)送給客戶端前你可能需要自定義 HTTP 響應(yīng)。 你有兩種辦法。第一,你可以在鏈?zhǔn)缴险{(diào)用 response
方法。此方法將會(huì)返回 Illuminate\Http\Response
實(shí)例,允許你自定義響應(yīng)頭信息:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return (new UserResource(User::find(1))) ->response() ->header('X-Value', 'True'); });
另外,你還可以在資源中定義一個(gè) withResponse
方法。此方法將會(huì)在資源被作為頂層資源在響應(yīng)時(shí)被調(diào)用:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 資源轉(zhuǎn)換成數(shù)組 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, ]; } /** * 自定義響應(yīng) * * @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Response $response * @return void */ public function withResponse($request, $response) { $response->header('X-Value', 'True'); } }