如何创建将模型转换为 JSON 的 Laravel Eloquent API 资源

介绍

在创建 API 时,我们通常需要使用数据库结果来过滤、解释或格式化 API 响应中返回的值。 API 资源类允许您将模型和模型集合转换为 JSON,作为数据库和控制器之间的数据转换层。

API 资源提供一个统一的界面,可以在应用程序中的任何地方使用。

Laravel为生成 resourcescollections 提供了两个 artisan命令 - 我们将在稍后了解这两者之间的区别。

我们将在下一节中通过玩一个演示项目来看看如何使用API资源。

前提条件

要跟随这个指南,你需要满足以下前提条件:

您可以在 如何在Ubuntu 18.04上安装和配置一个Laravel应用程序上设置此功能。

本教程是用 PHP v7.1.3 和 Laravel v5.6.35 编写的。

本教程已通过 PHP v7.3.11、Composer v.1.10.7、MySQL 5.7.0 和 Laravel v.5.6.35 进行验证。

第1步:克隆起始人

克隆 this repo并遵循在README.md中的指示,以获得东西并运行。

首先,克隆 Repo:

1git clone `[email protected]:do-community/songs-demo.git`

然后,导航到项目文件夹:

1cd songs-demo

创建一个 .env 文件,运行以下命令:

1cp .env.example .env

更新此 .env 文件中的数据库凭证。

安装包和依赖:

1composer install

<$>[注] 注: 您必须在您的 Laravel 开发环境中工作,以便此操作。 对于那些使用 Vagrant 的用户,请确保在运行composer install之前ssh到 Vagrant。

然后,生成应用程序的加密密钥:

1php artisan key:generate

运行迁移和种子数据库,使用一些样本数据:

1php artisan migrate:refresh --seed

步骤2 - 设置项目

由于这是一个小项目,我们不会创建任何控制器,而是将测试路线关闭内部的响应。

让我们从生成一个SongResource类开始:

1php artisan make:resource SongResource

资源文件通常进入应用程序\Http\资源文件夹。

让我们看看新创建的资源文件 - SongResource:

 1[label app/Http/Resources/SongResource.php]
 2[...]
 3class SongResource extends JsonResource
 4{
 5    /**
 6    * Transform the resource into an array.
 7    *
 8    *  @param  \Illuminate\Http\Request  $request
 9    *  @return array
10    **/
11    public function toArray($request)
12    {
13        return parent::toArray($request);
14    }
15}

默认情况下,我们在)。如果我们留下这些东西,所有可见的模型属性将是我们的响应的一部分。 为了定制响应,我们在这个toArray()`方法中指定要转换为JSON的属性。

让我们更新 toArray() 方法来匹配下面的片段:

 1[label app/Http/Resources/SongResource.php]
 2[...]
 3public function toArray($request)
 4{
 5    return [
 6        'id' => $this->id,
 7        'title' => $this->title,
 8        'rating' => $this->rating,
 9    ];
10}

正如您所看到的,我们可以直接从$this变量访问模型属性,因为一个资源类自动允许方法访问底层模型。

现在让我们更新routes/api.php以以下短片:

 1[label routes/api.php]
 2[...]
 3use App\Http\Resources\SongResource;
 4use App\Song;
 5[...]
 6
 7Route::get('/songs/{song}', function(Song $song) {
 8    return new SongResource($song);
 9});
10
11Route::get('/songs', function() {
12    return new SongResource(Song::all());
13});

如果我们访问URL /api/songs/1,我们会看到一个JSON响应,其中包含我们在SongResource类中指定的关键值对,该歌曲的ID为 `1’:

1{
2  data: {
3    id: 1,
4    title: "Mouse.",
5    rating: 3
6  }
7}

但是,如果我们试图访问URL /api/songs,则会投出一个例外:

1[secondary_label Output]
2Property [id] does not exist on this collection instance.

这是因为实例化 SongResource 类需要将资源实例传递给构建器,而不是集合,这就是为什么例外被扔掉的原因。

如果我们希望一个集合返回而不是一个单一的资源,则有一个静态的collection()方法,可以在集合中作为参数传递的资源类中调用。

1Route::get('/songs', function() {
2    return SongResource::collection(Song::all());
3});

再次访问/api/songs URL 将为我们提供包含所有歌曲的 JSON 响应。

 1{
 2  "data": [
 3    {
 4      "id": 1,
 5      "title": "Mouse.",
 6      "rating": 3
 7    },
 8    {
 9      "id": 2,
10      "title": "I'll.",
11      "rating": 0
12    }
13  ]
14}

资源在返回单个资源或甚至集合时运行得很好,但如果我们希望在响应中包含元数据,则有局限性。

为了生成一个收藏类,我们运行:

1php artisan make:resource SongsCollection

JSON 资源和 JSON 集合的主要区别在于,资源扩展了JsonResource类,并在实例化时期望通过单个资源,而集合扩展了ResourceCollection类,并在实例化时期望收集作为参数。

假设我们希望某些元数据,如歌曲总数,成为响应的一部分,这里是如何在使用ResourceCollection类工作时处理此问题:

 1[label app/Http/Resources/SongsCollection.php]
 2[...]
 3class SongsCollection extends ResourceCollection
 4{
 5    public function toArray($request)
 6    {
 7        return [
 8            'data' => $this->collection,
 9            'meta' => ['song_count' => $this->collection->count()],
10        ];
11    }
12}

如果我们更新我们的/api/songs路线关闭到这个:

1[label routes/api.php]
2[...]
3use App\Http\Resources\SongsCollection;
4[...]
5Route::get('/songs', function() {
6    return new SongsCollection(Song::all());
7});

然后访问URL /api/songs,我们现在看到数据属性内的所有歌曲以及元位内的总数:

 1{
 2  "data": [
 3    {
 4      "id": 1,
 5      "title": "Mouse.",
 6      "artist": "Carlos Streich",
 7      "rating": 3,
 8      "created_at": "2018-09-13 15:43:42",
 9      "updated_at": "2018-09-13 15:43:42"
10    },
11    {
12      "id": 2,
13      "title": "I'll.",
14      "artist": "Kelton Nikolaus",
15      "rating": 0,
16      "created_at": "2018-09-13 15:43:42",
17      "updated_at": "2018-09-13 15:43:42"
18    },
19    {
20      "id": 3,
21      "title": "Gryphon.",
22      "artist": "Tristin Veum",
23      "rating": 3,
24      "created_at": "2018-09-13 15:43:42",
25      "updated_at": "2018-09-13 15:43:42"
26    }
27  ],
28  "meta": {
29    "song_count": 3
30  }
31}

但是我们有一个问题,数据属性中的每个歌曲都没有被格式化为我们之前在SongResource中定义的规格,而是具有所有属性。

要修复此问题,在)而不是$this->collection`。

我们的toArray()方法现在将看起来像这样:

1[label app/Http/Resources/SongsCollection.php]
2[...]
3public function toArray($request)
4{
5    return [
6        'data' => SongResource::collection($this->collection),
7        'meta' => ['song_count' => $this->collection->count()]
8    ];
9}

您可以通过再次访问/api/songs URL 来验证我们在回复中获得了正确的数据。

如果要将元数据添加到单个资源而不是集合中,怎么办?幸运的是,)`方法,允许您指定您希望在处理资源时加入响应的任何额外数据:

1[label routes/api.php]
2[...]
3Route::get('/songs/{song}', function(Song $song) {
4    return (new SongResource(Song::find(1)))->additional([
5        'meta' => [
6            'anything' => 'Some Value'
7        ]
8    ]);
9});

在这种情况下,答案看起来有点像这个:

 1{
 2  "data": {
 3    "id": 1,
 4    "title": "Mouse.",
 5    "rating": 3
 6  },
 7  "meta": {
 8    "anything": "Some Value"
 9  }
10}

步骤3 - 创建模型关系

在这个项目中,我们只有两个模型,‘Album’和‘Song’。当前的关系是‘一对多’的关系,这意味着一张专辑有许多歌曲,一首歌曲属于一个专辑。

现在我们将更新 SongResource 类内的 toArray() 方法,以便它引用该专辑:

 1[label app/Http/Resources/SongResource.php]
 2[...]
 3class SongResource extends JsonResource
 4{
 5    public function toArray($request)
 6    {
 7        return [
 8            [...]
 9            // other attributes
10            'album' => $this->album
11        ];
12    }
13}

如果我们想更具体地了解哪些专辑属性会出现在响应中,我们可以创建一个类似于我们对歌曲的 AlbumResource。

要创建AlbumResource,运行:

1php artisan make:resource AlbumResource

一旦资源类被创建,我们就会指定我们想要在响应中包含的属性。

 1[label app/Http/Resources/AlbumResource.php]
 2[...]
 3class AlbumResource extends JsonResource
 4{
 5    public function toArray($request)
 6    {
 7        return [
 8            'title' => $this->title
 9        ];
10    }
11}

现在在SongResource类内,而不是做album => $this->album,我们可以使用我们刚刚创建的AlbumResource类。

 1[label app/Http/Resources/SongResource.php]
 2[...]
 3class SongResource extends JsonResource
 4{
 5    public function toArray($request)
 6    {
 7        return [
 8            [...]
 9            // other attributes
10            'album' => new AlbumResource($this->album)
11        ];
12    }
13}

如果我们再次访问/api/songs URL,你会注意到一张专辑将是响应的一部分,这种方法唯一的问题是它带来了N + 1查询问题。

为了演示目的,请在routes/api.php文件中添加以下片段:

1[label routes/api.php]
2[...]
3DB::listen(function($query) {
4    var_dump($query->sql);
5});

请再次访问/api/songsURL。请注意,对于每首歌曲,我们会进行额外的查询以获取专辑的详细信息吗?这可以通过热切的关系来避免。

1[label routes/api.php]
2[...]
3return new SongsCollection(Song::with('album')->get());

重新加载页面,你会注意到查询数量减少。

评论DB::Listen片段,因为我们不再需要它。

步骤4 – 使用资源时使用条件

偶尔,我们可能会有条件决定将返回的响应类型。

好消息是,我们不必这样做,因为在JsonResource类别中需要一个条件负载属性特征,该类别有几种方法来处理条件。

我们只会讨论whenLoadedmergeWhen的方法,但文档是全面的。

当加载方法

此方法可防止在检索相关模型时加载未加载的数据,从而防止出现(N+1)查询问题。

仍然使用Album资源作为参考点(一张专辑有许多歌曲):

1[label app/Http/Resources/AlbumResource.php]
2public function toArray($request)
3{
4    return [
5        [...]
6        // other attributes
7        'songs' => SongResource::collection($this->whenLoaded($this->songs))
8    ];
9}

如果我们在检索专辑时不急于加载歌曲,我们将最终获得一个空的歌曲收藏。

The mergeWhen 方法

相反,要有一个 if 语句,决定某个属性及其值是否会是响应的一部分,我们可以使用将条件作为第一个参数进行评估的 mergeWhen() 方法,以及包含关键值对的数组,如果条件被评估为 true,则该数组应该是响应的一部分:

 1[label app/Http/Resources/AlbumResource.php]
 2public function toArray($request)
 3{
 4    return [
 5        [...]
 6        // other attributes
 7        'songs' => SongResource::collection($this->whenLoaded($this->songs)),
 8        $this->mergeWhen($this->songs->count > 10, ['new_attribute' => 'attribute value'])
 9    ];
10}

这看起来更干净和更优雅,而不是如果声明包裹整个回报块。

第5步:单元测试API资源

现在我们已经学会了如何转换我们的响应,我们如何验证我们返回的响应是我们在资源类中指定的?

现在我们将撰写测试,验证答案包含正确的数据,并确保仍然保持着雄辩的关系。

让我们创建测试:

1php artisan make:test SongResourceTest --unit

在生成测试时注意 - 单位标志:这将告诉Laravel这将是一个单位测试。

注意:在验证过程中,在运行)SongResourceTest.php的内容似乎包含一些较旧的测试。

让我们开始写测试,以确保我们从SongResource类的答案包含正确的数据:

 1[label tests/Unit/SongResourceTest.php]
 2[...]
 3use App\Http\Resources\SongResource;
 4use App\Http\Resources\AlbumResource;
 5[...]
 6class SongResourceTest extends TestCase
 7{
 8    use RefreshDatabase;
 9    public function testCorrectDataIsReturnedInResponse()
10    {
11        $resource = (new SongResource($song = factory('App\Song')->create()))->jsonSerialize();
12    }
13}

在这里,我们首先创建一个歌曲资源,然后在SongResource上呼叫jsonSerialize(),将该资源转换为JSON格式,因为这将被发送到我们的前端。

既然我们已经知道将成为响应的一部分的歌曲属性,我们现在可以做出我们的说法:

1[label tests/Unit/SongResourceTest.php]
2[...]
3$this->assertArraySubset([
4    'title' => $song->title,
5    'rating' => $song->rating
6], $resource);

在本示例中,我们匹配了两个属性:标题评级

如果您想要确保模型关系即使在将模型转换为资源后仍然存在,则可以使用:

1[label tests/Unit/SongResourceTest.php]
2[...]
3public function testSongHasAlbumRelationship()
4{
5    $resource = (new SongResource($song = factory('App\Song')->create(["album_id" => factory('App\Album')->create(['id' => 1])])))->jsonSerialize();
6}

在这里,我们创建一个具有1album_id的歌曲,然后将歌曲传递到SongResource类,然后最终将资源转换为JSON格式。

为了验证歌曲与专辑的关系是否仍然存在,我们对我们刚刚创建的$resource专辑属性作出声明:

1[label tests/Unit/SongResourceTest.php]
2[...]
3$this->assertInstanceOf(AlbumResource::class, $resource["album"]);

但是,请注意,如果我们做了 $this->assertInstanceOf(Album::class, $resource["album"]),我们的测试将失败,因为我们正在将专辑实例转化为SongResource类内的资源。

注意:在验证过程中,我们确定我们可以使用以下命令运行这些测试:

1vendor/bin/phpunit

美元

作为回复,我们首先创建一个模型实例,将该实例传递到资源类,将资源转换为JSON格式,然后最终做出声明。

结论

我们已经研究了Laravel API资源是什么,如何创建它们以及如何测试JSON响应。

如果您想了解更多关于 Laravel API 资源的信息,请参阅 官方文档

Published At
Categories with 技术
comments powered by Disqus