Eloquent:API 资源

Introduction

在构建 API 时,您可能需要一个位于 Eloquent 模型和实际返回给应用程序用户的 JSON 响应之间的转换层。例如,您可能希望为一部分用户而不是其他用户显示某些属性,或者您可能希望始终在模型的 JSON 表示中包含某些关系。 Eloquent 的资源类让您能够轻松而有表现力地将您的模型和模型集合转换为 JSON。

当然,你总是可以使用它们将 Eloquent 模型或集合转换为 JSONtoJson 方法;然而,Eloquent 资源提供了对模型及其关系的 JSON 序列化的更精细和更强大的控制。

生成资源

要生成资源类,您可以使用make:resource 工匠命令。默认情况下,资源将放置在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

概念概述

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

在深入了解编写资源时可用的所有选项之前,让我们首先从高层次看一下 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;
}

写作资源

Note
如果您还没有阅读概念概述,强烈建议您在继续阅读本文档之前这样做。

本质上,资源很简单。他们只需要将给定的模型转换为数组。因此,每个资源都包含一个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));
});

Relationships

如果您想在响应中包含相关资源,您可以将它们添加到资源返回的数组中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,
    ];
}

Note
如果您只想在它们已经加载时包含关系,请查看文档条件关系.

资源集合

资源将单个模型转换为数组,而资源集合将模型集合转换为数组。但是,并非绝对有必要为每个模型定义一个资源集合类,因为所有资源都提供了一个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\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());
});

数据包装

默认情况下,您的最外层资源包装在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

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * The "data" wrapper that should be applied.
     *
     * @var string|null
     */
    public static $wrap = 'user';
}

如果你想禁用最外层资源的包装,你应该调用withoutWrapping 基础上的方法Illuminate\Http\Resources\Json\JsonResource 班级。通常,您应该从您的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();
    }
}

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

包装嵌套资源

您可以完全自由地决定资源关系的包装方式。如果您希望将所有资源集合包装在一个data key,无论它们嵌套如何,都应该为每个资源定义一个资源集合类,并在一个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];
    }
}

数据包装和分页

当通过资源响应返回分页集合时,Laravel 会将你的资源数据包装在一个data 关键即使withoutWrapping 方法已被调用。这是因为分页响应总是包含metalinks 包含有关分页器状态信息的键:

{
    "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
    }
}

Pagination

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

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

在这个例子中,secret 只有经过身份验证的用户的密钥才会在最终资源响应中返回isAdmin 方法返回true.如果方法返回false, 这secret 密钥将在发送到客户端之前从资源响应中删除。这when 方法允许您在构建数组时无需借助条件语句即可明确定义资源。

when 方法还接受一个闭包作为它的第二个参数,允许您仅在给定条件为true:

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

whenHas 如果属性实际存在于底层模型中,则方法可用于包含属性:

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

此外,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,这些属性将在发送到客户端之前从资源响应中删除。

Warning
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 密钥将在发送到客户端之前从资源响应中删除。

条件枢轴信息

除了在资源响应中有条件地包含关系信息之外,您还可以有条件地包含来自多对多关系中间表的数据,使用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 标准要求将元数据添加到您的资源和资源集合响应中。这通常包括诸如links 资源或相关资源,或有关资源本身的元数据。如果您需要返回有关资源的其他元数据,请将其包含在您的toArray 方法。例如,您可以包括link 转换资源集合时的信息:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'data' => $this->collection,
        'links' => [
            'self' => 'link-value',
        ],
    ];
}

当从你的资源中返回额外的元数据时,你永远不必担心不小心覆盖了links或者meta 在返回分页响应时由 Laravel 自动添加的键。任何额外的links 您定义的将与分页器提供的链接合并。

顶级元数据

有时,如果资源是返回的最外层资源,您可能希望只在资源响应中包含某些元数据。通常,这包括有关整个响应的元信息。要定义此元数据,请添加with 方法到您的资源类。仅当资源是要转换的最外层资源时,此方法才应返回要包含在资源响应中的元数据数组:

<?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 在所有资源上都可用的方法接受应添加到资源响应中的数据数组:

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');
    }
}
豫ICP备18041297号-2