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

概念概述

NOTE

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

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

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

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

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

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

为方便起见,你可以使用模型的 toResource 方法,该方法将使用框架约定自动发现模型的底层资源:

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

当调用 toResource 方法时,Laravel 将尝试在模型命名空间最近的 Http\Resources 命名空间中定位一个与模型名称匹配并可选择以 Resource 结尾的资源。

如果你的资源类不遵循此命名约定或位于不同的命名空间中,你可以使用 UseResource 属性为模型指定默认资源:

php
<?php

namespace App\Models;

use App\Http\Resources\CustomUserResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResource;

#[UseResource(CustomUserResource::class)]
class User extends Model
{
    // ...
}

或者,你可以通过将资源类传递给 toResource 方法来指定:

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

资源集合

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

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

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

或者,为方便起见,你可以使用 Eloquent 集合的 toResourceCollection 方法,该方法将使用框架约定自动发现模型的底层资源集合:

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

当调用 toResourceCollection 方法时,Laravel 将尝试在模型命名空间最近的 Http\Resources 命名空间中定位一个与模型名称匹配并以 Collection 结尾的资源集合。

如果你的资源集合类不遵循此命名约定或位于不同的命名空间中,你可以使用 UseResourceCollection 属性为模型指定默认资源集合:

php
<?php

namespace App\Models;

use App\Http\Resources\CustomUserCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;

#[UseResourceCollection(CustomUserCollection::class)]
class User extends Model
{
    // ...
}

或者,你可以通过将资源集合类传递给 toResourceCollection 方法来指定:

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

自定义资源集合

默认情况下,资源集合不允许添加任何可能随集合返回的自定义元数据。如果你想自定义资源集合响应,你可以创建一个专门的资源来表示集合:

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
{
    /**
     * 将资源集合转换为数组。
     *
     * @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 方法,该方法将使用框架约定自动发现模型的底层资源集合:

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

当调用 toResourceCollection 方法时,Laravel 将尝试在模型命名空间最近的 Http\Resources 命名空间中定位一个与模型名称匹配并以 Collection 结尾的资源集合。

保留集合键

当从路由返回资源集合时,Laravel 会重置集合的键,使它们按数字顺序排列。但是,你可以使用资源类上的 PreserveKeys 属性来指示是否应保留集合的原始键:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Attributes\PreserveKeys;
use Illuminate\Http\Resources\Json\JsonResource;

#[PreserveKeys]
class UserResource extends JsonResource
{
    // ...
}

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 结尾,也可能不以 Resource 结尾。

例如,UserCollection 将尝试将给定的用户实例映射到 UserResource 资源。要自定义此行为,你可以在资源集合上使用 Collects 属性:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Attributes\Collects;
use Illuminate\Http\Resources\Json\ResourceCollection;

#[Collects(Member::class)]
class UserCollection extends ResourceCollection
{
    // ...
}

编写资源

NOTE

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

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

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * 将资源转换为数组。
     *
     * @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;

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

如果你希望仅当关联已被加载时才包含它们,请查看关于条件关联的文档。

资源集合

虽然资源将单个模型转换为数组,但资源集合将模型集合转换为数组。但是,并非绝对需要为每个模型定义一个资源集合类,因为所有 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
{
    /**
     * 将资源集合转换为数组。
     *
     * @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": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ]
}

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

    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        JsonResource::withoutWrapping();
    }
}

WARNING

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

包裹嵌套资源

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

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

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class CommentsCollection extends ResourceCollection
{
    /**
     * 将资源集合转换为数组。
     *
     * @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": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "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": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "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
/**
 * 自定义资源的分页信息。
 *
 * @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
/**
 * 将资源转换为数组。
 *
 * @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
/**
 * 将资源转换为数组。
 *
 * @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 方法可用于有条件地加载一个关联。为了避免不必要地加载关联,此方法接受关联的名称而不是关联本身:

php
use App\Http\Resources\PostResource;

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

条件关联计数

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

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

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

php
/**
 * 将资源转换为数组。
 *
 * @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 方法接受中间表的名称作为其第一个参数。第二个参数应是一个闭包,如果中间表信息在模型上可用,则返回要返回的值:

php
/**
 * 将资源转换为数组。
 *
 * @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 方法:

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

如果你的中间表使用了 pivot 以外的访问器,你可以使用 whenPivotLoadedAs 方法:

php
/**
 * 将资源转换为数组。
 *
 * @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
/**
 * 将资源转换为数组。
 *
 * @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
{
    /**
     * 将资源集合转换为数组。
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    /**
     * 获取应与资源数组一起返回的额外数据。
     *
     * @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',
    ]]);

JSON:API 资源

Laravel 提供了 JsonApiResource,这是一个资源类,它生成符合 JSON:API 规范的响应。它扩展了标准的 JsonResource 类,并自动处理资源对象结构、关联、稀疏字段集、包含,并将 Content-Type 标头设置为 application/vnd.api+json

NOTE

Laravel 的 JSON:API 资源处理你的响应的序列化。如果你还需要解析传入的 JSON:API 查询参数(如过滤器和排序),Spatie 的 Laravel Query Builder 是一个很棒的配套包。

生成 JSON:API 资源

要生成 JSON:API 资源,请使用带有 --json-api 标志的 make:resource Artisan 命令:

shell
php artisan make:resource PostResource --json-api

生成的类将扩展 Illuminate\Http\Resources\JsonApi\JsonApiResource,并包含 $attributes$relationships 属性供你定义:

php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    /**
     * 资源的属性。
     */
    public $attributes = [
        // ...
    ];

    /**
     * 资源的关联。
     */
    public $relationships = [
        // ...
    ];
}

JSON:API 资源可以像标准资源一样从路由和控制器返回:

php
use App\Http\Resources\PostResource;
use App\Models\Post;

Route::get('/api/posts/{post}', function (Post $post) {
    return new PostResource($post);
});

或者,为方便起见,你可以使用模型的 toResource 方法:

php
Route::get('/api/posts/{post}', function (Post $post) {
    return $post->toResource();
});

这将产生一个符合 JSON:API 的响应:

json
{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World",
            "body": "This is my first post."
        }
    }
}

要返回 JSON:API 资源的集合,请使用 collection 方法或 toResourceCollection 便捷方法:

php
return PostResource::collection(Post::all());

return Post::all()->toResourceCollection();

定义属性

有两种方法可以定义 JSON:API 资源中包含哪些属性。

最简单的方法是在资源上定义一个 $attributes 属性。你可以将属性名称作为值列出,这些值将从底层模型直接读取:

php
public $attributes = [
    'title',
    'body',
    'created_at',
];

或者,为了完全控制资源的属性,你可以覆盖资源上的 toAttributes 方法:

php
/**
 * 获取资源的属性。
 *
 * @return array<string, mixed>
 */
public function toAttributes(Request $request): array
{
    return [
        'title' => $this->title,
        'body' => $this->body,
        'is_published' => $this->published_at !== null,
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

定义关联

JSON:API 资源支持定义遵循 JSON:API 规范的关联。仅当客户端通过 include 查询参数请求时,关联才会被序列化。

$relationships 属性

你可以通过资源上的 $relationships 属性定义资源可包含的关联:

php
public $relationships = [
    'author',
    'comments',
];

当将关联名称作为值列出时,Laravel 将解析相应的 Eloquent 关联并自动发现适当的资源类。如果你需要显式指定资源类,可以将关联定义为键/类对:

php
use App\Http\Resources\UserResource;

public $relationships = [
    'author' => UserResource::class,
    'comments',
];

或者,你可以覆盖资源上的 toRelationships 方法:

php
/**
 * 获取资源的关联。
 */
public function toRelationships(Request $request): array
{
    return [
        'author' => UserResource::class,
        'comments',
    ];
}

包含关联

客户端可以使用 include 查询参数请求相关资源:

GET /api/posts/1?include=author,comments

这将产生一个响应,在 relationships 键中包含资源标识符对象,并在顶层的 included 数组中包含完整的资源对象:

json
{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World"
        },
        "relationships": {
            "author": {
                "data": {
                    "id": "1",
                    "type": "users"
                }
            },
            "comments": {
                "data": [
                    {
                        "id": "1",
                        "type": "comments"
                    }
                ]
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "users",
            "attributes": {
                "name": "Taylor Otwell"
            }
        },
        {
            "id": "1",
            "type": "comments",
            "attributes": {
                "body": "Great post!"
            }
        }
    ]
}

可以使用点表示法包含嵌套关联:

GET /api/posts/1?include=comments.author

关联深度

默认情况下,嵌套关联包含的深度是有限制的。你可以使用 maxRelationshipDepth 方法自定义此限制,通常在你的应用程序的一个服务提供者中调用:

php
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

JsonApiResource::maxRelationshipDepth(3);

资源类型和 ID

默认情况下,资源的 type 是从资源类名派生的。例如,PostResource 产生类型 posts,而 BlogPostResource 产生 blog-posts。资源的 id 从模型的主键解析。

如果你需要自定义这些值,可以覆盖资源上的 toTypetoId 方法:

php
/**
 * 获取资源的类型。
 */
public function toType(Request $request): string
{
    return 'articles';
}

/**
 * 获取资源的 ID。
 */
public function toId(Request $request): string
{
    return (string) $this->uuid;
}

这在资源的类型应与其类名不同时特别有用,例如当 AuthorResource 包装一个 User 模型并应输出类型 authors 时。

稀疏字段集和包含

JSON:API 资源支持稀疏字段集,允许客户端使用 fields 查询参数为每种资源类型请求特定的属性:

GET /api/posts?fields[posts]=title,created_at&fields[users]=name

这将仅包含 posts 资源的 titlecreated_at 属性,以及 users 资源的 name 属性。

忽略查询字符串

如果你想为给定的资源响应禁用稀疏字段集过滤,你可以调用 ignoreFieldsAndIncludesInQueryString 方法:

php
return $post->toResource()
    ->ignoreFieldsAndIncludesInQueryString();

包含先前加载的关联

默认情况下,仅当通过 include 查询参数请求时,关联才会包含在响应中。如果你希望无论查询字符串如何,都包含所有先前预加载的关联,你可以调用 includePreviouslyLoadedRelationships 方法:

php
return $post->load('author', 'comments')
    ->toResource()
    ->includePreviouslyLoadedRelationships();

链接和元数据

你可以通过覆盖资源上的 toLinkstoMeta 方法,向 JSON:API 资源对象添加链接和元信息:

php
/**
 * 获取资源的链接。
 */
public function toLinks(Request $request): array
{
    return [
        'self' => route('api.posts.show', $this->resource),
    ];
}

/**
 * 获取资源的元信息。
 */
public function toMeta(Request $request): array
{
    return [
        'readable_created_at' => $this->created_at->diffForHumans(),
    ];
}

这将在响应的资源对象中添加 linksmeta 键:

json
{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World"
        },
        "links": {
            "self": "https://example.com/api/posts/1"
        },
        "meta": {
            "readable_created_at": "2 hours ago"
        }
    }
}

资源响应

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

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
{
    /**
     * 将资源转换为数组。
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * 自定义资源的传出响应。
     */
    public function withResponse(Request $request, JsonResponse $response): void
    {
        $response->header('X-Value', 'True');
    }
}