Skip to content

Eloquent: API 资源

介绍

在构建 API 时,您可能需要一个转换层,位于 Eloquent 模型和实际返回给应用程序用户的 JSON 响应之间。例如,您可能希望为某些用户显示某些属性,而不显示给其他用户,或者您可能希望在模型的 JSON 表示中始终包含某些关系。Eloquent 的资源类允许您以表达性和简单的方式将模型和模型集合转换为 JSON。

当然,您始终可以使用 toJson 方法将 Eloquent 模型或集合转换为 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

概念概述

lightbulb

这是资源和资源集合的高级概述。强烈建议您阅读本文档的其他部分,以更深入地了解资源提供的自定义和功能。

在深入了解编写资源时可用的所有选项之前,让我们首先从高层次上了解资源在 Laravel 中的使用方式。资源类表示需要转换为 JSON 结构的单个模型。例如,这里是一个简单的 UserResource 资源类:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource 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 变量访问模型属性。这是因为资源类会自动将属性和方法访问代理到底层模型,以便于访问。一旦定义了资源,它可以从路由或控制器返回。资源通过其构造函数接受底层模型实例:

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

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

资源集合

如果您要返回资源集合或分页响应,则应在路由或控制器中创建资源实例时使用资源类提供的 collection 方法:

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

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

请注意,这不允许添加可能需要与您的集合一起返回的自定义元数据。如果您想自定义资源集合响应,可以创建一个专门的资源来表示集合:

shell
php artisan make:resource UserCollection

一旦生成了资源集合类,您可以轻松定义应该包含在响应中的任何元数据:

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

定义资源集合后,它可以从路由或控制器返回:

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

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

保留集合键

从路由返回资源集合时,Laravel 会重置集合的键,使其按数字顺序排列。但是,您可以在资源类中添加一个 preserveKeys 属性,指示是否应保留集合的原始键:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * 指示是否应保留资源的集合键。
     *
     * @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
{
    /**
     * 此资源收集的资源。
     *
     * @var string
     */
    public $collects = Member::class;
}

编写资源

lightbulb

如果您尚未阅读概念概述,强烈建议您在继续阅读本文档之前先阅读。

本质上,资源很简单。它们只需要将给定的模型转换为数组。因此,每个资源都包含一个 toArray 方法,该方法将模型的属性转换为可以从应用程序的路由或控制器返回的 API 友好数组:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

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

一旦定义了资源,它可以直接从路由或控制器返回:

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

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

关系

如果您希望在响应中包含相关资源,可以将它们添加到资源的 toArray 方法返回的数组中。在此示例中,我们将使用 PostResource 资源的 collection 方法将用户的博客文章添加到资源响应中:

php
use App\Http\Resources\PostResource;

/**
 * 将资源转换为数组。
 *
 * @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,
    ];
}
lightbulb

如果您希望仅在关系已加载时才包含关系,请查看有关条件关系的文档。

资源集合

虽然资源将单个模型转换为数组,但资源集合将模型集合转换为数组。然而,您不必为每个模型定义资源集合类,因为所有资源都提供一个 collection 方法,可以即时生成“临时”资源集合:

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

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

但是,如果您需要自定义与集合一起返回的元数据,则需要定义自己的资源集合:

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

与单个资源一样,资源集合可以直接从路由或控制器返回:

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

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

数据包装

默认情况下,当资源响应转换为 JSON 时,最外层的资源会被包装在 data 键中。因此,典型的资源集合响应如下所示:

json
{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ]
}

如果您希望使用自定义键而不是 data,可以在资源类上定义一个 $wrap 属性:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * 应用的“数据”包装器。
     *
     * @var string|null
     */
    public static $wrap = 'user';
}

如果您希望禁用最外层资源的包装,您应该在基础 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
{
    /**
     * 注册任何应用程序服务。
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * 启动任何应用程序服务。
     *
     * @return void
     */
    public function boot()
    {
        JsonResource::withoutWrapping();
    }
}
exclamation

withoutWrapping 方法仅影响最外层的响应,不会删除您手动添加到自己的资源集合中的 data 键。

包装嵌套资源

您可以完全自由地决定资源的关系如何包装。如果您希望所有资源集合都包装在 data 键中,无论其嵌套如何,您应该为每个资源定义一个资源集合类,并在 data 键中返回集合。

您可能会担心这会导致最外层的资源被包装在两个 data 键中。别担心,Laravel 永远不会让您的资源意外地双重包装,因此您不必担心要转换的资源集合的嵌套级别:

php
<?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 键中。这是因为分页响应始终包含有关分页器状态的 metalinks 键:

json
{
    "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 分页器实例传递给资源的 collection 方法或自定义资源集合:

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

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

分页响应始终包含有关分页器状态的 metalinks 键:

json
{
    "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 方法可用于有条件地将属性添加到资源响应中:

php
/**
 * 将资源转换为数组。
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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,
    ];
}

在此示例中,secret 键仅在经过身份验证的用户的 isAdmin 方法返回 true 时才会在最终资源响应中返回。如果方法返回 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),

合并条件属性

有时,您可能有几个属性仅在给定条件为 true 时才应包含在资源响应中。在这种情况下,您可以使用 mergeWhen 方法仅在给定条件为 true 时将属性包含在响应中:

php
/**
 * 将资源转换为数组。
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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 方法可用于有条件地加载关系。为了避免不必要地加载关系,此方法接受关系的名称而不是关系本身:

php
use App\Http\Resources\PostResource;

/**
 * 将资源转换为数组。
 *
 * @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 键将从资源响应中删除。

条件关系计数

除了有条件地包含关系外,您还可以根据关系的计数是否已加载到模型上来有条件地在资源响应中包含关系“计数”:

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

whenCounted 方法可用于有条件地在资源响应中包含关系的计数。此方法避免在关系的计数不存在时不必要地包含属性:

php
/**
 * 将资源转换为数组。
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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 键将从资源响应中删除。

条件枢轴信息

除了在资源响应中有条件地包含关系信息外,您还可以使用 whenPivotLoaded 方法有条件地包含多对多关系的中间表中的数据。whenPivotLoaded 方法接受枢轴表的名称作为其第一个参数。第二个参数应为一个闭包,该闭包返回在模型上可用枢轴信息时要返回的值:

php
/**
 * 将资源转换为数组。
 *
 * @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;
        }),
    ];
}

如果您的关系使用自定义中间表模型,您可以将中间表模型的实例作为第一个参数传递给 whenPivotLoaded 方法:

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

如果您的中间表使用的访问器不是 pivot,您可以使用 whenPivotLoadedAs 方法:

php
/**
 * 将资源转换为数组。
 *
 * @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 方法中。例如,您可以在转换资源集合时包含 link 信息:

php
/**
 * 将资源转换为数组。
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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
{
    /**
     * 将资源集合转换为数组。
     *
     * @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 方法接受一个应添加到资源响应的数据数组:

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

资源响应

如您所读,资源可以直接从路由和控制器返回:

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

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

然而,有时您可能需要在将响应发送到客户端之前自定义传出的 HTTP 响应。有两种方法可以实现这一点。首先,您可以在资源上链接 response 方法。此方法将返回一个 Illuminate\Http\JsonResponse 实例,给予您对响应头的完全控制:

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

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource 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');
    }
}