• 介绍
  • 生成资源
  • 概念一览
  • 写资源
  • 资源返回

介绍

当构建 API 的时候,你可能需要在 Elequent 模型和传给应用应用客户的 JOSN 返回间设立一个转换层。 Laravel 的资源类可以自述性地和简易地将模型和模型集合转变为 JSON。

生成资源

你可以用 make:resource Artisan 命令来生成一个资源类。资源类将默认生成在应用的 app/Http/Resources 文件夹中。资源类继承自 Illuminate\Http\Resources\Json\Resource 类:

php artisan make:resource User
资源集合

除了为转换独立的模型而生成资源类,你也可以为转换模型集合生成模型类,它允许你的返回信息包涵一个与已有资源相关联的整个集合的链接和其它元信息。

在创建资源的时候你可以用 --collection 创建一个资源集合,或者简单地包涵一个 collection 单词在资源名称当中将会指示 Laravel 生成一个资源集合。集合拓展自 Illuminate\Http\Resource\Json\ResourceCollection 类:

php artisan make:resource Users --collection
php artisan make:resource UserCollection

概念一览

这是一个对资源和资源集合的高度概括说明。强烈建议你阅读文档的其它模块来深入理解通过资源给你带来的个性化和能力。

在深入挖掘你写资源时可用的选项前,让我们简单浏览一个资源在 Laravel 中如何使用。一个资源类仅仅代表了一个需要被转化成 JSON 结构的模型。比如这里是一个简单的 User 资源类:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class User extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\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,
        ];
    }
}

每个资源类包含了一个在发送返回时需要被转化成 JSON 的属性数组。注意我们可以直接通过 $this 来获取模型属性。这是因为为了方便一个资源类可以自动代理它下层的模型的属性和方法。一旦资源类被定义,它便可以从路由或控制器返回:

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

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

资源集合

如果你正在返回一个资源集合或是一个分页资源,你可以在路由或控制器中创建资源实例的时候用 collection方法 。

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

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

当然,这并不允诉你添加额外的返回集合中的元信息。如果你想自定义资源集合返回,你可以直接创建一个集合类:

php artisan make:resource UserCollection

创建完集合类,你就可以轻松地定义那些需要包含在返回中的元信息。

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

定义完集合资源,它可以在路由或控制器中返回:

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

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

写资源

如果你还没有阅读过概念一览,建议你在阅读这一部分之前先阅读它们

在本质上,资源是很简单的。它们只需要把一个给定的模型转换成数组。所以,每个资源包含了一个返回给用户的,转换模型属性为友好 API 数组的 toArray 方法:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class User extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\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\User;
use App\Http\Resources\User as UserResource;

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

关系

如果你想在返回内容中包含关联资源。你可以简单地把它们增加在 toArray 方法返回的数组中。在下面的例子中,我们将用 Post 资源的 collection 方法来增加用户博客文章到返回信息中:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request
 * @return array
 */
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => Post::collection($this->posts),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}
如果你只想在关联关系已经在被加载的情况下包含关联关系,查看关系关系这篇文章。

资源集合

资源转换单一模型为数组,资源集合转换模型集合为数组。没有必要为每一个模型创建资源集合,因为所有的资源都提供了 collection 方法来快速生成一个 "ad-hoc" 资源模型。

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

Route::get('/user', 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.
     *
     * @param  \Illuminate\Http\Request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

对于单一资源,资源集合将可以直接从路由和控制器中返回:

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

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

如果你想要禁止这种去中心化的包裹,你可以调用资源基类的方法withoutWraping。通常你应该在AppServiceProvider 中调用这个方法,或是别的每次访问都会加载的 service provider:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\Resource;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot()
    {
        Resource::withoutWrapping();
    }

    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
the ``withoutWrapping`` 方法只会去中心化资源造成影响,并不会移除你手动添加到资源集合中的`data`健。

你可以完全自由地定义你的资源关系是如何被包裹的。如果你想要所有的资源被 data健包裹,那就忽视他们的嵌套,你应该为每一个资源定义一个资源类并且用一个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.
     *
     * @param  \Illuminate\Http\Request
     * @return array
     */
    public function toArray($request)
    {
        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
    }
}

分页

你可以传递一个分页实例到资源的 collectin 方法中或是一个定义的资源集合:

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

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

条件属性

有时你会想在特定条件下才在资源返回中包含某个属性。比如,你可能想在当前用户是管理员时才包含某值。Larave 提供了一系列方法来帮你解决这个问题。这个when方法可以条件性的帮你添加属性到资源返回:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request
 * @return array
 */
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'secret' => $this->when($this->isAdmin(), 'secret-value'),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

在这个例子中,只有 $this->isAdmin()返回 true 的时候,secret健才会在最终返回资源中出现。 如果它反回了false,那么在资源返回给客户前secret健将会从资源中移除。这个when方法允许你自述性地定义资源,而不用在构建数组时重构条件状态。

when方法同时也接受匿名函数作为它的第二个参数,允许你在所给条件为真的情况下计算结果值:

'secret' => $this->when($this->isAdmin(), function () {
    return 'secret-value';
}),
记住,资源中调用的方法可以代理模型实例,那么,在这个例子中,`isAdmin`方法代理了那个原生赋予了资源的 Eloquent 模型。

合并条件属性

有时你可能有一些条件属性是基于同一个条件的。这时,你可以用 mergeWhen 方法来包含那写条件为真时才返回的属性们:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request
 * @return array
 */
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        $this->mergeWhen($this->isAdmin(), [
            'first-secret' => 'value',
            'second-secret' => 'value',
        ]),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

同样,如果条件为false,在资源返回给客户前这些健将会从资源中移除。

mergeWhen 不能用在数字,字母健混合的数组中,并且也不能用在非顺序排序的数字健中

条件关系

除了条件性加载属性,在模型关联属性已经被加载到模型中的条件下,你也可以条件性地关系关系到资源中。这将允许你决定哪个关联关系应该在模型中被加载。这样你的资源可以在它们实际被加载的条件下加载他们。

最终这将可以轻松避免 " N+1 " 问题。使用 whenLoaded 方法可以条件性加载关联关系。为了避免加载不必要的关联关系。这个方法接受关联关系名而不是关联关系本身。

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request
 * @return array
 */
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => Post::collection($this->whenLoaded('posts')),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

在上面的例子中,如果关联关系已经被加载,那么post健将在资源返回前全部移除。

条件中间表信息

在资源返回中除了包含关联关系,你也可以用 whenPivotLoaded方法来返回多对多关系中中间表的信息,这个方法接受中间表的名字作为它的第一个参数,第二个参数是一个回调,返回值为需要返回的中间表字段。:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request
 * @return array
 */
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'expires_at' => $this->whenPivotLoaded('role_users', function () {
            return $this->pivot->expires_at;
        }),
    ];
}

增加元信息

一些 JSON API要求在资源各资源集合中返回额外的元信息,常见的比如包含资源和相关资源的links,或资源本身的元信息。如果你想返回额外的元信息,只要简单得把它放在toArray中,比如在资源集合中加入link:

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

在返回分页资源的时候,不用担心意外覆盖了Laravel 自动提供的 linksmeta。他们会自行合并。

顶层元信息

有时,你可能只想在当前返回信息为顶层信息时才包含元信息,通常包含元信息的返回作为一个整体。在你的资源类中用with方法来定义元信息。此方法应该返回一个当前资源作为顶层返回资源才包含的元信息:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

    /**
     * Get additional data that should be returned with the resource array.
     *
     * @param \Illuminate\Http\Request  $request
     * @return array
     */
    public function with($request)
    {
        return [
            'meta' => [
                'key' => 'value',
            ],
        ];
    }
}

在构建资源时添加元信息

你也可以在控制器或路由中创建资源时,增加顶层元信息,所有资源类都可以用的addtional方法来增加元信息:

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

资源返回

你已经知道了,资源可以从控制器和路由中返回:

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

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

然而,你有时会想在响应送回前定义HTTP,有两种方式,第一种,你可以链式调用response方法,这个方法会返回Illuminate\Http\Response实例,让你完全控制返回头:

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

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

第二种,你可以在资源类中定义 withResponse方法,当前资源为顶层资源时会调用:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class User extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * Customize the outgoing response for the resource.
     *
     * @param  \Illuminate\Http\Request
     * @param  \Illuminate\Http\Response
     * @return void
     */
    public function withResponse($request, $response)
    {
        $response->header('X-Value', 'True');
    }
}