API 資源
Eloquent: API 資源
資源回應
##「介紹」##簡介
當建構API 時,你往往需要一個轉換層來聯結你的Eloquent 模型和實際回傳給使用者的JSON 回應。 Laravel 的資源類別能讓你以更直覺簡單的方式將模型和模型集合轉換成 JSON。
產生資源你可以使用
make:resource
Artisan 指令來產生一個資源類別。預設產生的資源都會被放置在應用程式的 app/Http/Resources
資料夾下。資源繼承自
類別:php artisan make:resource User
資源集合
除了產生資源轉換單一模型外,你還可以產生資源集合用來轉換模型的集合。這允許你在回應中包含與給定資源相關的連結與其他元資訊。
你需要在產生資源時加上Collection--collection
標誌以產生一個資源集合。或者,你也可以直接在資源的名稱中包含
向 Laravel 表示應該產生一個資源集合。資源集合繼承自Illuminate\Http\Resources\Json\ResourceCollection
類別:
php artisan make:resource Users --collection php artisan make:resource UserCollection
概念綜述
在深入了解如何客製化撰寫你的資源之前,讓我們先來看看在 Laravel 中如何使用資源。一個資源類別表示一個單一模型需要被轉換成 JSON 格式。例如,現在我們有一個簡單的
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 将资源转换成数组。 * * @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, ]; } }###每一個資源類別都定義了一個###toArray### 方法,在發送回應時它會回傳應該被轉換成JSON 的屬性數組。注意在這裡我們可以直接使用 ###$this### 變數來存取模型屬性。這是因為資源類別將自動代理屬性和方法到底層模型以方便存取。你可以在路由或控制器中傳回已定義的資源:###
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });###資源集合######你可以在路由或控制器中使用###collection### 方法來建立資源實例,以傳回多個資源的集合或分頁回應:###
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()); });
當然了,使用如上方法你將不能添加任何附加的元資料和集合一起返回。如果你需要自訂資源集合回應,你需要建立一個專用的資源來表示集合:
php artisan make:resource UserCollection
你可以輕鬆的在已產生的資源集合類別中定義任何你想在回應中傳回的元數據:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 将资源集合转换成数组。 * * @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()); });
保護集合的鍵
當從路由傳回資源集合時,Laravel 將重置集合的鍵,使它們以簡單的數字順序。但是,可以將preserveKeys
屬性加入到資源類別中,指示是否要保留集合鍵:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 指示是否应保留资源的集合键。 * * @var bool */ public $preserveKeys = true; }
當preserveKeys
屬性被設定為true
, 集合的鍵將會被保護:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()->keyBy->id); });
自訂基礎資源類別
#通常,資源集合的$this->collection
屬性會自動填充,結果是將集合的每個項目對應到其單一資源類別。假定單一資源類別是集合的類別名,但結尾沒有 Collection
字串。
例如,UserCollection
將給定的使用者實例對應到 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} 如果你還沒有閱讀概念綜述,那麼在繼續閱讀本文檔之前,強烈建議你去閱讀一下。
從本質上來說,資源的作用很簡單。它們只需要將一個給定的模型轉換成一個陣列。所以每一個資源都包含一個toArray
方法用來將你的模型屬性轉換成一個可以傳回給使用者的API 友善數組:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 将资源转换成数组。 * * @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, ]; } }
你可以在路由或控制器中返回已經定義的資源:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });
關聯
如果你希望在回應中包含關聯資源,你只需要將它們加入到toArray
方法傳回的陣列中。在下面這個範例裡,我們將使用Post
資源的collection
方法將使用者的文章加入資源回應:
/** * 将资源转换成数组。 * * @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} 如果你只想在關聯已經載入時才新增關聯資源,請查看文件條件關聯。
資源集合
資源是將單一模型轉換成數組,而資源集合是將多個模型的集合轉換成陣列。所有的資源都提供了一個collection
方法來產生一個「臨時」 資源集合,所以你沒有必要為每個模型類型都寫一個資源集合類別:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return UserResource::collection(User::all()); });
不過,如果你需要自訂傳回集合的元數據,則仍需要定義一個資源集合:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 将资源集合转换成数组 * * @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()); });#
資料包裹
預設情況下,當資源回應被轉換成 JSON 時,頂層資源將會被包裹在 data
鍵中。因此一個典型的資源集合回應如下所示:
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", "email": "therese28@example.com", }, { "id": 2, "name": "Liliana Mayert", "email": "evandervort@example.com", } ] }
你可以使用資源基底類別的 withoutWrapping
方法來停用頂層資源的包裹。通常,你應該在 AppServiceProvider
或其他在程式每個請求中都會被載入的服務提供者中呼叫此方法:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Http\Resources\Json\Resource; class AppServiceProvider extends ServiceProvider{ /** * 在注册后进行服务的启动 * * @return void */ public function boot() { Resource::withoutWrapping(); } /** * 在容器中注册绑定 * * @return void */ public function register() { // } }
{note}
withoutWrappin
方法只會停用頂層資源的包裹,不會刪除你手動新增到資源集合中的data
鍵。
包裹嵌套資源
你可以完全自由地決定資源關聯如何被包裹。如果你希望無論怎樣嵌套,都將所有資源集合包裹在data
鍵中,那麼你需要為每個資源定義一個資源集合類,並將返回的集合包裹在data
鍵中。
當然,你可能會擔心這樣頂層資源將會被包裹在兩個 data
鍵中。請放心, Laravel 將永遠不會讓你的資源被雙層包裹,因此你不必擔心被轉換的資源集合會被多重嵌套:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class CommentsCollection extends ResourceCollection{ /** * 将资源集合转换成数组 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return ['data' => $this->collection]; } }
資料包和分頁
當在資源回應中傳回分頁集合時,即使你呼叫了withoutWrapping
方法, Laravel 也會將你的資源資料包裹在data
鍵中。這是因為分頁回應中總會有meta
和links
鍵包含著分頁狀態資訊:
{ "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 } }
分頁
你可以將分頁實例傳遞給資源的collection
方法或自訂的資源集合:
use App\User; use App\Http\Resources\UserCollection; Route::get('/users', function () { return new UserCollection(User::paginate()); });
分頁回應中總有meta
和links
鍵包含著分頁狀態資訊:
{ "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 } }
#條件屬性
有些時候,你可能希望在給定條件滿足時加入屬性到資源回應裡。例如,你可能希望如果目前使用者是 “管理員” 時將某個值新增到資源回應中。在這種情況下 Laravel 提供了一些輔助方法來幫助你解決問題。 when
方法可以用來有條件地向資源回應新增屬性:
/** * 将资源转换成数组 * * @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, ]; }
在上面這個範例中,只有當isAdmin
方法回傳true
時, secret
鍵才會最終在資源回應中被傳回。如果該方法傳回 false
,則 secret
鍵將會在資源回應傳送給客戶端之前被刪除。 when
方法可以讓你避免使用條件語句拼接數組,轉而用更優雅的方式來寫你的資源。
when
方法也接受閉包作為其第二個參數,只有在給定條件為true
時,才從閉包中計算傳回的值:
'secret' => $this->when(Auth::user()->isAdmin(), function () { return 'secret-value'; }),
有條件的合併資料
有些時候,你可能希望在給定條件滿足時添加多個屬性到資源回應。在這種情況下,你可以使用mergeWhen
方法在給定的條件為true
時將多個屬性加入回應:
/** * 将资源转换成数组 * * @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
時,則這些屬性將會在資源回應傳送給客戶端之前移除。
{note}
mergeWhen
方法不應該被使用在混合字串和數字鍵的陣列中。此外,它也不應該被使用在不按順序排列的數字鍵的數組中。
條件關聯
除了有條件地新增屬性之外,你還可以根據模型關聯是否已載入來有條件地在你的資源回應中包含關聯。這允許你在控制器中決定要載入哪些模型關聯,這樣你的資源可以在模型關聯被載入後才加入它們。
這樣做可以避免在你的資源中出現 “N 1” 查詢問題。你應該使用 whenLoaded
方法來有條件的載入關聯。為了避免載入不必要的關聯,此方法接受關聯的名稱而不是關聯本身作為其參數:
/** * 将资源转换成数组 * * @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, ]; }
在上面這個例子中,如果關聯沒有被載入,則posts
鍵將會在資源回應被傳送給客戶端之前被刪除。
條件中間表格資訊
除了在你的資源回應中有條件地包含關聯外,你還可以使用whenPivotLoaded
方法有條件地從多對多關聯的中間表中新增資料。 whenPivotLoaded
方法接受的第一個參數為中間表的名稱。第二個參數是一個閉包,它定義了在模型上如果中間表資訊可用時要傳回的值:
/** * 将资源转换成数组 * * @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
方法:
/** * 将资源转换成数组 * * @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; }), ]; }##添加元資料一些JSON API 標準需要你在資源和資源集合回應中新增元資料。這通常包括資源或相關資源的
links ,或一些關於資源本身的元資料。如果你需要傳回有關資源的其他元數據,只需要將它們包含在
toArray 方法中即可。例如在轉換資源集合時你可能需要添加
links 資訊:
/** * 将资源转换成数组 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request){ return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }當添加額外的元資料到你的資源時,你不必擔心會覆蓋Laravel 在返回分頁回應時自動新增的
links 或
meta 鍵。你新增的任何其他
links 會與分頁回應新增的
links 相合併。
頂層元資料
有時候你可能會希望當資源被當作頂層資源回傳時加入某些元資料到資源回應中。這通常包括整個回應的元資訊。你可以在資源類別中加入 with
方法來定義元資料。此方法應傳回一個元資料數組,當資源被當作頂層資源渲染時,這個數組將會被包含在資源回應中:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * 将资源集合转换成数组 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return parent::toArray($request); } /** * 返回应该和资源一起返回的其他数据数组 * * @param \Illuminate\Http\Request $request * @return array */ public function with($request) { return [ 'meta' => [ 'key' => 'value', ], ]; } }
建構資源時加入元資料
你還可以在路由或控制器中建構資源實例時新增頂層資料。所有資源都可以使用additional
方法來接受應該被加入到資源回應中的資料數組:
return (new UserCollection(User::all()->load('roles'))) ->additional(['meta' => [ 'key' => 'value', ] ]);
回應資源
#就像你知道的那樣, 資源可以直接在路由和控制器中被回傳:
use App\User; use App\Http\Resources\User as UserResource; Route::get('/user', function () { return new UserResource(User::find(1)); });
但有些時候,在發送給客戶端之前你可能需要自訂HTTP 回應。你有兩種辦法。第一,你可以在鍊式上呼叫 response
方法。此方法將會傳回 Illuminate\Http\Response
實例,讓你自訂回應頭資訊:
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'); });
另外,你也可以在資源中定義一個withResponse
方法。此方法將會在資源被作為頂層資源在回應時被呼叫:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class User extends JsonResource{ /** * 资源转换成数组 * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, ]; } /** * 自定义响应 * * @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Response $response * @return void */ public function withResponse($request, $response) { $response->header('X-Value', 'True'); } }