Skip to content

Eloquent: API 資源

介紹

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

當然,您隨時可以使用 Eloquent 模型或集合的 toJson 方法將其轉換為 JSON;然而,Eloquent 資源為您的模型及其關聯的 JSON 序列化提供了更細緻且強大的控制。

產生資源

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

shell
php artisan make:resource UserResource

資源集合

除了產生轉換個別模型的資源之外,您還可以產生負責轉換模型集合的資源。這讓您的 JSON 回應能夠包含與給定資源的整個集合相關的連結及其他中繼資訊。

若要建立資源集合,您在建立資源時應使用 --collection 標誌。或者,在資源名稱中包含 Collection 這個字將指示 Laravel 應建立集合資源。集合資源會擴充 Illuminate\Http\Resources\Json\ResourceCollection 類別:

shell
php artisan make:resource User --collection

php artisan make:resource UserCollection

概念概覽

📌 備註

這是資源與資源集合的高層次概覽。強烈建議您閱讀本文檔的其他章節,以便更深入地理解資源所提供的自訂與強大功能。

在深入探討撰寫資源時的所有可用選項之前,讓我們先從高層次角度了解資源在 Laravel 中的使用方式。一個資源類別代表一個需要轉換為 JSON 結構的單一 model。例如,這是一個簡單的 UserResource 資源類別:

php
<?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 變數存取 model 屬性。這是因為資源類別會自動將屬性與方法的存取代理到底層 model,以方便存取。一旦定義了資源,就可以從路由或控制器返回它。該資源透過其建構函式接受底層 model 實例:

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

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

為方便起見,您可以使用 model 的 toResource 方法,該方法將利用框架慣例自動發現 model 的底層資源:

php
return User::findOrFail($id)->toResource();

當調用 toResource 方法時,Laravel 會嘗試在最接近 model 命名空間的 Http\Resources 命名空間中,定位與 model 名稱相符且可選地以 Resource 為後綴的資源。

資源集合

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

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

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

或者,為方便起見,您可以使用 Eloquent 集合的 toResourceCollection 方法,該方法將利用框架慣例自動發現 model 的底層資源集合:

php
return User::all()->toResourceCollection();

當調用 toResourceCollection 方法時,Laravel 會嘗試在最接近 model 命名空間的 Http\Resources 命名空間中,定位與 model 名稱相符且以 Collection 為後綴的資源集合。

自訂資源集合

依預設,資源集合不允許添加任何可能需要與您的集合一起返回的自訂中繼資料。如果您想自訂資源集合回應,可以建立一個專用資源來代表該集合:

shell
php artisan make:resource UserCollection

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

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

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

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

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

或者,為方便起見,您可以使用 Eloquent 集合的 toResourceCollection 方法,該方法將利用框架慣例自動發現 model 的底層資源集合:

php
return User::all()->toResourceCollection();

當調用 toResourceCollection 方法時,Laravel 會嘗試在最接近 model 命名空間的 Http\Resources 命名空間中,定位與 model 名稱相符且以 Collection 為後綴的資源集合。

保留集合鍵值

當從路由返回資源集合時,Laravel 會重設集合的鍵值,使它們按數字順序排列。然而,您可以在資源類別中添加 preserveKeys 屬性,以指出是否應保留集合的原始鍵值:

php
<?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 時,當集合從路由或控制器返回時,集合鍵值將被保留:

php
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
<?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;
}

撰寫資源

📌 備註

如果您尚未閱讀 概念概覽 章節,強烈建議您在繼續閱讀本文件前先行閱讀。

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

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

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

php
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return User::findOrFail($id)->toUserResource();
});

關聯

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

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

📌 備註

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

資源集合

儘管資源將單一模型轉換為陣列,但資源集合則將模型集合轉換為陣列。然而,對於您每個模型而言,定義一個資源集合類別並非絕對必要,因為所有 Eloquent 模型集合都提供了一個 toResourceCollection 方法,可以即時生成一個「臨時」資源集合:

php
use App\Models\User;

Route::get('/users', function () {
    return User::all()->toResourceCollection();
});

然而,如果您需要自訂隨集合返回的中繼資料,則有必要定義您自己的資源集合:

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

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

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

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

或者,為了方便起見,您可以使用 Eloquent 集合的 toResourceCollection 方法,該方法將遵循框架慣例自動發現模型底層的資源集合:

php
return User::all()->toResourceCollection();

當調用 toResourceCollection 方法時,Laravel 將嘗試在最接近模型命名空間的 Http\Resources 命名空間中,定位與模型名稱匹配且以 Collection 結尾的資源集合。

資料包裝

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

json
{
    "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
<?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();
    }
}

⚠️ 警告

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

包裝巢狀資源

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

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

php
<?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 鍵,其中包含關於分頁器狀態的資訊:

json
{
    "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 方法,或傳遞給自訂資源集合:

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

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

或者,為了方便起見,您可以使用分頁器的 toResourceCollection 方法,該方法將利用框架慣例自動發現分頁模型所依據的資源集合:

php
return User::paginate()->toResourceCollection();

分頁回應始終包含帶有分頁器狀態資訊的 metalinks 鍵:

json
{
    "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 鍵的陣列:

php
/**
 * 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 方法可用於條件式地將屬性新增到資源回應中:

php
/**
 * 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 時才計算結果值:

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

whenHas 方法可用於在屬性確實存在於基礎模型上時才將其包含進來:

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

此外,whenNotNull 方法可用於在屬性不是 null 時將其包含在資源回應中:

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

合併條件式屬性

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

php
/**
 * 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,這些屬性將在傳送給客戶端之前從資源回應中移除。

⚠️ 警告

mergeWhen 方法不應在混合字串和數字鍵的陣列中使用。此外,它也不應在數字鍵未按順序排列的陣列中使用。

條件式關聯

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

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

php
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 鍵會在傳送給用戶端前,從資源回應中移除。

條件式關聯計數

除了條件式地包含關聯外,您還可以根據關聯的計數是否已載入 Model 上,來條件式地將關聯「計數」包含在您的資源回應中:

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

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

php
/**
 * 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 方法進行條件式載入:

php
'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 方法接受樞紐表格的名稱作為第一個引數。第二個引數應為一個閉包,該閉包在 Model 上有樞紐資訊時回傳該值:

php
/**
 * 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;
        }),
    ];
}

如果您的關聯正在使用 自訂中介表格 Model,您可以將中介表格 Model 的實例作為第一個引數傳遞給 whenPivotLoaded 方法:

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

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

php
/**
 * 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 資訊:

php
/**
 * 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
<?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 方法接受一個應新增至資源回應的資料陣列:

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

資源回應

如您所知,資源可以直接從路由和控制器中回傳:

php
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return User::findOrFail($id)->toResource();
});

然而,有時您可能需要在將 HTTP 回應送達客戶端之前,客製化該回應。有兩種方式可以達成此目的。首先,您可以將 response 方法串接到資源上。此方法會回傳一個 Illuminate\Http\JsonResponse 實例,讓您可以完全控制回應的標頭:

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

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

另一種方式是,您可以在資源本身內定義一個 withResponse 方法。當資源作為回應中最外層的資源回傳時,此方法將會被呼叫:

php
<?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');
    }
}