mogody / hyperf-resource

API资源类允许您以表达性和简洁的方式将您的模型和模型集合转换为JSON。

v2.1.0 2021-01-29 01:41 UTC

This package is auto-updated.

Last update: 2024-09-29 05:29:12 UTC


README

介绍

当构建API时,您可能需要一个位于您的Eloquent模型和实际返回给应用程序用户的JSON响应之间的转换层。Hyperf资源类允许您以表达性和简洁的方式将您的模型和模型集合转换为JSON。

概念概述

{提示} 这是资源及其集合的高级概述。强烈建议您阅读此文档的其他部分,以深入了解资源提供的定制和功能。

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

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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\Http\Resources\User as UserResource;
use App\User;

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

资源集合

如果您正在返回资源集合或分页响应,您可以在路由或控制器中创建资源实例时使用collection方法

use App\Http\Resources\User as UserResource;
use App\User;

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

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

php artisan make:resource UserCollection

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

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

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

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

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

保留集合键

当从路由返回资源集合时,Laravel会重置集合的键,以便它们以简单的数字顺序排列。但是,您可以在资源类中添加一个preserveKeys属性,以指示是否应保留集合键

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Indicates if the resource's collection keys should be preserved.
     *
     * @var bool
     */
    public $preserveKeys = true;
}

preserveKeys属性设置为true时,将保留集合键

use App\Http\Resources\User as UserResource;
use App\User;

Route::get('/user', function () {
    return UserResource::collection(User::all()->keyBy->id);
});

自定义底层资源类

通常,资源集合的$this->collection属性会自动用映射集合中的每个项目到其单个资源类的结果填充。单个资源类假定是集合类名,但不带尾随的Collection字符串。

例如,UserCollection会尝试将给定的用户实例映射到User资源。要自定义此行为,您可以覆盖资源集合的$collects属性

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * The resource that this resource collects.
     *
     * @var string
     */
    public $collects = 'App\Http\Resources\Member';
}

编写资源

{提示} 如果您还没有阅读概念概述,强烈建议在继续阅读此文档之前阅读它。

本质上,资源很简单。它们只需要将给定的模型转换成一个数组。因此,每个资源都包含一个 toArray 方法,该方法将您的模型属性转换成API友好的数组,以便返回给用户。

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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\Http\Resources\User as UserResource;
use App\User;

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

关系

如果您想将相关资源包含在响应中,可以将其添加到您 toArray 方法返回的数组中。在这个例子中,我们将使用 Post 资源的 collection 方法将用户的博客文章添加到资源响应中。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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,
    ];
}

{提示} 如果您只想在有相关数据已加载时包含关系,请查看有关 条件关系 的文档。

资源集合

虽然资源将单个模型转换为数组,但资源集合将模型集合转换为数组。对于您的模型类型,并不绝对需要为每个类型定义资源集合类,因为所有资源都提供了一个 collection 方法来动态生成“临时”资源集合。

use App\Http\Resources\User as UserResource;
use App\User;

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

但是,如果您需要自定义随集合返回的元数据,则必须定义资源集合。

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

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

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

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",
        }
    ]
}

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

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\JsonResource;

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

如果您想禁用最外层资源的包装,可以使用基础资源类的 withoutWrapping 方法。通常,您应该从您的 AppServiceProvider 或其他 服务提供者 中调用此方法,该提供者会在每次请求您的应用程序时加载。

<?php

namespace App\Providers;

use Mogody\Resource\Json\JsonResource;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        JsonResource::withoutWrapping();
    }
}

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

包装嵌套资源

您完全自由地确定资源关系如何被包装。如果您想将所有资源集合包装在 data 键中,无论它们的嵌套程度如何,您应该为每个资源定义一个资源集合类,并在 data 键中返回集合。

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

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\ResourceCollection;

class CommentsCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @return array
     */
    public function toArray($request)
    {
        return ['data' => $this->collection];
    }
}

数据包装和分页

当在资源响应中返回分页集合时,即使调用了 withoutWrapping 方法,Laravel 也会将您的资源数据包装在 data 键中。这是因为分页响应总是包含有关分页器状态的 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
    }
}

分页

您始终可以将分页器实例传递给资源或自定义资源集合的 collection 方法。

use App\Http\Resources\UserCollection;
use App\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.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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 时将属性包含在响应中。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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 方法有条件地加载关系。为了避免不必要地加载关系,此方法接受关系的名称而不是关系本身。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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 方法接受其第一个参数为连接表的名称。第二个参数应是一个闭包,该闭包定义了如果模型上存在连接信息,则应返回的值。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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 方法。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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 信息。

/**
 * Transform the resource into an array.
 *
 * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
 * @return array
 */
public function toArray($request)
{
    return [
        'data' => $this->collection,
        'links' => [
            'self' => 'link-value',
        ],
    ];
}

从资源返回其他元数据时,你永远不必担心意外覆盖 Laravel 在返回分页响应时自动添加的 linksmeta 键。你定义的任何其他 links 都将与分页器提供的链接合并。

顶级元数据

有时,您可能希望仅在资源是返回的最外层资源时,将某些元数据包含在资源响应中。通常,这包括关于整个响应的元信息。要定义这些元数据,请在您的资源类中添加一个with方法。此方法应返回一个数组,其中包含仅当资源是正在渲染的最外层资源时才包含在资源响应中的元数据。

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @return array
     */
    public function toArray($request)
    {
        return parent::toArray($request);
    }

    /**
     * Get additional data that should be returned with the resource array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $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\Http\Resources\User as UserResource;
use App\User;

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

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

use App\Http\Resources\User as UserResource;
use App\User;

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

或者,您可以在资源本身内定义一个withResponse方法。当资源作为响应中的最外层资源返回时,将调用此方法。

<?php

namespace App\Http\Resources;

use Mogody\Resource\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * Customize the outgoing response for the resource.
     *
     * @param  \Hyperf\HttpServer\Contract\RequestInterface  $request
     * @param  \Psr\Http\Message\ResponseInterface  $response
     * @return void
     */
    public function withResponse($request, $response)
    {
        $response->header('X-Value', 'True');
    }
}