介绍
在创建 API 时,我们通常需要使用数据库结果来过滤、解释或格式化 API 响应中返回的值。 API 资源类允许您将模型和模型集合转换为 JSON,作为数据库和控制器之间的数据转换层。
API 资源提供一个统一的界面,可以在应用程序中的任何地方使用。
Laravel为生成 resources 和 collections 提供了两个 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/songs
URL。请注意,对于每首歌曲,我们会进行额外的查询以获取专辑的详细信息吗?这可以通过热切的关系来避免。
1[label routes/api.php]
2[...]
3return new SongsCollection(Song::with('album')->get());
重新加载页面,你会注意到查询数量减少。
评论DB::Listen
片段,因为我们不再需要它。
步骤4 – 使用资源时使用条件
偶尔,我们可能会有条件决定将返回的响应类型。
好消息是,我们不必这样做,因为在JsonResource类别中需要一个条件负载属性
特征,该类别有几种方法来处理条件。
我们只会讨论whenLoaded
和mergeWhen
的方法,但文档是全面的。
当加载
方法
此方法可防止在检索相关模型时加载未加载的数据,从而防止出现(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}
在这里,我们创建一个具有1
的album_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 资源的信息,请参阅 官方文档。