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

news/2024/7/4 3:04:28

介绍 (Introduction)

When creating APIs, we often need to work with database results to filter, interpret or format values that will be returned in the API response. API resource classes allow you to convert your models and model collections into JSON, working as a data transformation layer between the database and the controllers.

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

API resources provide a uniform interface that can be used anywhere in the application. Eloquent relationships are also taken care of.

API资源提供了可以在应用程序中任何位置使用的统一接口。 雄辩的关系也得到照顾。

Laravel provides two artisan commands for generating resources and collections - we’ll understand the difference between these two later on. But for both resources and collections, we have our response wrapped in a data attribute: a JSON response standard.

Laravel提供了两个artisan命令来生成资源集合 -我们稍后将了解这两者之间的区别。 但是对于资源和集合,我们都将响应包装在data属性中:JSON响应标准。

We’ll look at how to work with API resources in the next section by playing around with a demo project.

在下一节中,我们将通过演示项目来研究如何使用API​​资源。

先决条件: (Prerequisites:)

To follow along with this guide, you need to meet the following prerequisites:

要遵循本指南,您需要满足以下先决条件:

  • A working Laravel development environment. To set this up, you can follow our guide on How to Install and Configure a Laravel application on Ubuntu 18.04.

    一个可运行的Laravel开发环境。 要进行设置,您可以按照我们的指南如何在Ubuntu 18.04上安装和配置Laravel应用程序进行操作 。

获取演示代码 (Obtaining the Demo Code)

Clone this repo and follow the instructions in the README.md to get things up and running.

克隆此 README.md ,并按照README.md的说明进行操作。

With the project setup, we can now start getting our hands dirty. Also, since this is a very small project, we won’t be creating any controllers and will instead test out responses inside route closures.

通过项目设置,我们现在就可以开始动手了。 另外,由于这是一个非常小的项目,因此我们将不会创建任何控制器,而是在路由闭包内测试响应。

Let’s start by generating a SongResource class:

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

  • php artisan make:resource SongResource

    php artisan make:resource SongResource

If we peek inside the newly created resource file i.e. SongResource (Resource files usually go inside the App\Http\Resources folder), the contents looks like this:

如果我们窥视新创建的资源文件(即SongResource)(资源文件通常位于App\Http\Resources文件夹中),则内容如下所示:

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

By default, we have parent::toArray($request) inside the toArray() method. If we leave things at this, all visible model attributes will be part of our response. To tailor the response, we specify the attributes we want to be converted to JSON inside this toArray() method.

默认情况下,我们在toArray()方法中有parent::toArray($request) 。 如果我们搁浅,所有可见的模型属性将成为我们响应的一部分。 为了定制响应,我们在此toArray()方法中指定要转换为JSON的属性。

Let’s update the toArray() method to match the snippet below:

让我们更新toArray()方法以匹配下面的代码段:

public function toArray($request)
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'rating' => $this->rating,
    ];
}

As you can see, we can access the model properties directly from the $this variable because a resource class automatically allows method access down to the underlying model.

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

Let’s now update the routes/api.php with the snippet below:

现在,使用以下代码段更新routes/api.php

# routes/api.php

[...]
use App\Http\Resources\SongResource;
use App\Song;
[...]

Route::get('/songs/{song}', function(Song $song) {
    return new SongResource($song);
});

Route::get('/songs', function() {
    return new SongResource(Song::all());
});

If we visit the URL /api/songs/1, we’ll see a JSON response containing the key-value pairs we specified in the SongResource class for the song with an id of 1:

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

{
  data: {
    id: 1,
    title: "Mouse.",
    rating: 3
  }
}

However, if we try visiting the URL /api/songs, an Exception is thrown Property [id] does not exist on this collection instance.

但是,如果尝试访问URL /api/songs ,则会引发异常Property [id] does not exist on this collection instance.

This is because instantiating the SongResource class requires a resource instance be passed to the constructor and not a collection. That’s why the exception is thrown.

这是因为实例化SongResource类需要将资源实例传递给构造函数而不是集合。 这就是引发异常的原因。

If we wanted a collection returned instead of a single resource, there is a static collection() method that can be called on a Resource class passing in a collection as the argument. Let’s update our songs route closure to this:

如果我们想要返回一个集合而不是单个资源,则可以在传递集合作为参数的Resource类上调用静态的collection()方法。 让我们将歌曲的路由闭合更新为此:

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

Visiting the /api/songs URL again will give us a JSON response containing all the songs.

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

{
  data: [{
      id: 1,
      title: "Mouse.",
      rating: 3
    },
    {
      id: 2,
      title: "I'll.",
      rating: 0
    }
  ]
}

Resources work just fine when returning a single resource or even a collection but have limitations if we want to include metadata in the response. That’s where Collections come to our rescue.

当返回单个资源甚至一个集合时,资源工作得很好,但是如果我们想在响应中包括元数据,则资源会受到限制。 这就是Collections拯救我们的地方。

To generate a collection class, we run:

要生成一个收集类,我们运行:

php artisan make:resource SongsCollection

The main difference between a JSON resource and a JSON collection is that a resource extends the JsonResource class and expects a single resource to be passed when being instantiated while a collection extends the ResourceCollection class and expects a collection as the argument when being instantiated.

JSON资源与JSON集合之间的主要区别在于,资源扩展了JsonResource类,并期望在实例化时传递单个资源,而集合扩展了ResourceCollection类,并期望在实例化时将集合作为参数。

Back to the metadata bit. Assuming we wanted some metadata such as the total song count to be part of the response, here’s how to go about it when working with the ResourceCollection class:

回到元数据位。 假设我们希望一些元数据(例如总歌曲数)成为响应的一部分,以下是在使用ResourceCollection类时如何进行处理:

class SongsCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'meta' => ['song_count' => $this->collection->count()],
        ];
    }
}

If we update our /api/songs route closure to this:

如果我们将/api/songs路由闭包更新为此:

[...]
use App\Http\Resources\SongsCollection;
[...]
Route::get('/songs', function() {
    return new \SongsCollection(Song::all());
});

And visit the URL /api/songs, we now see all the songs inside the data attribute as well as the total count inside the meta bit:

并访问URL /api/songs ,现在我们可以看到data属性中的所有歌曲以及meta位中的总数:

{
  data: [{
      id: 1,
      title: "Mouse.",
      artist: "Carlos Streich",
      rating: 3,
      created_at: "2018-09-13 15:43:42",
      updated_at: "2018-09-13 15:43:42"
    },
    {
      id: 2,
      title: "I'll.",
      artist: "Kelton Nikolaus",
      rating: 0,
      created_at: "2018-09-13 15:43:42",
      updated_at: "2018-09-13 15:43:42"
    },
    {
      id: 3,
      title: "Gryphon.",
      artist: "Tristin Veum",
      rating: 3,
      created_at: "2018-09-13 15:43:42",
      updated_at: "2018-09-13 15:43:42"
    }
  ],
  meta: {
    song_count: 3
  }
}

But we have a problem, each song inside the data attribute is not formatted to the specification we defined earlier inside the SongResource and instead has all attributes.

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

To fix this, inside the toArray() method, set the value of data to SongResource::collection($this->collection) instead of having $this->collection.

要解决此问题,请在toArray()方法内部,将data值设置为SongResource::collection($this->collection)而不要使用$this->collection

Our toArray() method should now look like this:

我们的toArray()方法现在应如下所示:

public function toArray($request)
{
    return [
        'data' => SongResource::collection($this->collection),
       'meta' => ['song_count' => $this->collection->count()]
    ];
}

You can verify we get the correct data in the response by visiting the /api/songs URL again.

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

What if one wants to add metadata to a single resource and not a collection? Luckily, the JsonResource class comes with an additional() method which lets you specify any additional data you’d like to be part of the response when working with a resource:

如果要向单个资源而不是集合添加元数据怎么办? 幸运的是, JsonResource类带有一个JsonResource additional()方法,该方法使您可以指定在使用资源时希望作为响应一部分的任何其他数据:

Route::get('/songs/{song}', function(Song $song) {
    return (new SongResource(Song::find(1)))->additional([
        'meta' => [
            'anything' => 'Some Value'
        ]
    ]);
})

In this case, the response would look somewhat like this:

在这种情况下,响应看起来像这样:

{
  data: {
    id: 1,
    title: "Mouse.",
    rating: 3
  },
  meta: {
    anything: "Some Value"
  }
}

创建模型关系 (Creating Model Relationships)

In this project, we only have two models, Album and Song. The current relationship is a one-to-many relationship, meaning an album has many songs and a song belongs to an album.

在这个项目中,我们只有两个模型AlbumSong 。 当前关系是one-to-many关系,这意味着专辑中有很多歌曲,而歌曲属于专辑。

We’ll now update the toArray() method inside the SongResource class so that it references the album:

现在,我们将更新SongResource类中的toArray()方法,使其引用专辑:

class SongResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            // other attributes
            'album' => $this->album
        ];
    }
}

If we want to be more specific in terms of what album attributes should be present in the response, we can create an AlbumResource similar to what we did with songs.

如果我们想更具体地说明响应中应包含哪些专辑属性,则可以创建一个与歌曲相似的AlbumResource。

To create the AlbumResource, run:

要创建AlbumResource ,请运行:

  • php artisan make:resource AlbumResource

    php artisan make:resource专辑资源

Once the resource class has been created, we then specify the attributes we want to be included in the response.

创建资源类后,我们然后指定要包含在响应中的属性。

class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'title' => $this->title
        ];
    }
}

And now inside the SongResource class, instead of doing 'album' => $this->album, we can make use of the AlbumResource class we just created.

现在,在SongResource类中,我们无需使用'album' => $this->album ,而可以使用刚刚创建的AlbumResource类。

class SongResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            // other attributes
            'album' => new AlbumResource($this->album)
        ];
    }
}

If we visit the /api/songs URL again, you’ll notice an album will be part of the response. The only problem with this approach is that it brings up the N + 1 query problem.

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

For demonstration purposes, add the snippet below inside the api/routes file:

出于演示目的,请在api/routes文件内添加以下代码段:

# routes/api.php

[...]
\DB::listen(function($query) {
    var_dump($query->sql);
});

Visit the /api/songs URL again. Notice that for each song, we make an extra query to retrieve the album’s details? This can be avoided by eager loading relationships. In our case, update the code inside the /api/songs route closure to:

再次访问/api/songs URL。 注意,对于每首歌曲,我们都会进行一个额外的查询来检索专辑的详细信息吗? 可以通过渴望加载关系来避免这种情况。 在我们的情况下,将/api/songs路由闭包内的代码更新为:

return new SongsCollection(Song::with('album')->get());

Reload the page again and you’ll notice the number of queries has reduced. Comment out the \DB::listen snippet since we don’t need that anymore.

再次重新加载页面,您会发现查询数量减少了。 注释掉\DB::listen代码段,因为我们不再需要它了。

使用资源时使用条件 (Using Conditionals When Working with Resources)

Every now and then, we might have a conditional determining the type of response that should be returned.

我们可能时不时地有条件地确定应返回的响应类型。

One approach we could take is introducing if statements inside our toArray() method. The good news is we don’t have to do that as there is a ConditionallyLoadsAttributes trait required inside the JsonResource class that has a handful of methods for handling conditionals. Just to mention a few, we have the when(), whenLoaded() and mergeWhen() methods.

我们可以采用的一种方法是在toArray()方法中引入if语句。 好消息是我们不必这样做,因为JsonResource类内部需要一个ConditionallyLoadsAttributes特性,它具有一些处理条件的方法。 仅举几例,我们有when()whenLoaded()mergeWhen()方法。

We’ll only brush through a few of these methods, but the documentation is quite comprehensive.

我们将只介绍其中一些方法,但是文档非常全面。

whenLoaded方法 (The whenLoaded method)

This method prevents data that has not been eagerly loaded from being loaded when retrieving related models thereby preventing the (N+1) query problem.

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

Still working with the Album resource as a point of reference (an album has many songs):

仍然使用专辑资源作为参考(专辑中有很多歌曲):

public function toArray($request)
{
    return [
        // other attributes
        'songs' => SongResource::collection($this->whenLoaded($this->songs))
    ];
}

In the case where we are not eagerly loading songs when retrieving an album, we’ll end up with an empty songs collection.

如果我们在检索专辑时不急于加载歌曲,则最终会得到一个空的歌曲集合。

mergeWhen方法 (The mergeWhen Method)

Instead of having an if statement that dictates whether some attribute and its value should be part of the response, we can use the mergeWhen() method which takes in the condition to evaluate as the first argument and an array containing key-value pair that is meant to be part of the response if the condition evaluates to true:

我们可以使用mergeWhen()方法代替该if语句来确定某个属性及其值是否应作为响应的一部分,该方法将条件值作为第一个参数,并使用一个包含键值对的数组如果条件评估为true,则表示是响应的一部分:

public function toArray($request)
{
    return [
        // other attributes
        'songs' => SongResource::collection($this->whenLoaded($this->songs)),
        this->mergeWhen($this->songs->count > 10, ['new_attribute' => 'attribute value'])
    ];
}

This looks cleaner and more elegant instead of having if statements wrapping the entire return block.

这看起来更干净,更优雅,而不是使用if语句包装整个返回块。

单元测试API资源 (Unit Testing API Resources)

Now that we’ve learned how to transform our responses, how do we actually verify that the response we get back is what we specified in our resource classes?

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

We’ll now write tests verifying the response contains the correct data as well making sure eloquent relationships are still maintained.

现在,我们将进行测试以验证响应中是否包含正确的数据,并确保仍然保持雄辩的关系。

Let’s create the test:

让我们创建测试:

  • php artisan make:test SongResourceTest --unit

    php artisan make:test SongResourceTest --unit

Notice the --unit flag when generating the test: this will tell Laravel that this should be a unit test.

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

Let’s start by writing the test to make sure our response from the SongResource class contains the correct data:

让我们从编写测试开始,以确保我们从SongResource类获得的响应包含正确的数据:

[...]
use App\Http\Resources\SongResource;
use App\Http\Resources\AlbumResource;
[...]
class SongResourceTest extends TestCase
{
    use RefreshDatabase;
    public function testCorrectDataIsReturnedInResponse()
    {
        $resource = (new SongResource($song = factory('App\Song')->create()))->jsonSerialize();
    }
}

Here, we first create a song resource then call jsonSerialize() on the SongResource to transform the resource into JSON format, as that’s what should be sent to our front-end.

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

And since we already know the song attributes that should be part of the response, we can now make our assertion:

由于我们已经知道应该作为响应一部分的歌曲属性,因此我们现在可以断言:

$this->assertArraySubset([
    'title' => $song->title,
    'rating' => $song->rating
], $resource);

In this example, we’ve matched two attributes: title and rating. You can list as many attributes as you would like.

在此示例中,我们匹配了两个属性: titlerating 。 您可以列出任意数量的属性。

If you want to make sure your model relationships are preserved even after converting models to resources, you can use:

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

public function testSongHasAlbumRelationship()
{
    $resource = (new SongResource($song = factory('App\Song')->create(["album_id" => factory('App\Album')->create(['id' => 1])])))->jsonSerialize();
}

Here, we create a song with an album_id of 1 then pass the song on to the SongResource class before finally transforming the resource into JSON format.

在这里,我们创建一首带album_id1的歌曲,然后将其传递给SongResource类,最后将资源转换为JSON格式。

To verify that the song-album relationship is still maintained, we make an assertion on the album attribute of the $resource we just created. Like so:

为了验证歌曲专辑关系是否仍然保持,我们对刚创建的$resource的专辑属性进行断言。 像这样:

$this->assertInstanceOf(AlbumResource::class, $resource["album"]);

Note, however, if we did $this->assertInstanceOf(Album::class, $resource["album"]) our test would fail since we are transforming the album instance into a resource inside the SongResource class.

但是请注意,如果我们执行$this->assertInstanceOf(Album::class, $resource["album"])由于将专辑实例转换为SongResource类中的资源,我们的测试将失败。

As a recap, we first create a model instance, pass the instance to the resource class, convert the resource into JSON format before finally making the assertions.

作为回顾,我们首先创建一个模型实例,将该实例传递给资源类,然后将资源转换为JSON格式,然后再进行断言。

结论 (Conclusion)

We’ve looked at what Laravel API resources are, how to create them as well as how to test out various JSON responses. Feel free to explore the JsonResource class and see all the methods that are available.

我们已经研究了Laravel API资源是什么,如何创建它们以及如何测试各种JSON响应。 随意探索JsonResource类,并查看所有可用的方法。

If you would like to learn more about Laravel API resources, check the official documentation.

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

翻译自: https://www.digitalocean.com/community/tutorials/how-to-create-laravel-eloquent-api-resources-to-convert-models-into-json


http://www.niftyadmin.cn/n/3649676.html

相关文章

[JavaME]手机玩点对点MSN传情动漫之补充说明

代码将陆续上传。1:发送特定端口短信的MIDlet源代码可以从http://www.cnblogs.com/Files/zhengyun_ustc/MIMESMSSender.rar下载。其实实现的技术很简单,只要下载了 这两个例子程序,跑一遍,想想就能做出来了,呵呵。希望…

C/C++ 常见误区

1. C虽然主要是以C的基础发展起来的一门新语言,但她不是C的替代品,不是C的升级,C和C是兄弟关系。没有谁比谁先进的说法,更重要 的一点是C和C各自的标准委员会是独立的,最新的C标准是C98,最新的C标准是C99.因…

[JavaME]手机也能玩转点对点的MSN传情动漫

郑昀ultrapower产品名称产品版本Keyword: Mobile 传情动漫 Wink MSN Messenger Media 媒体播放 j2me midp2.0WMA PushRegistryPopupWinksMobile0.0.1 BETA手机也能玩转MSN传情动漫! J2ME实作摘要:本文档并不是一个关于PopupWinks On Mobile 之Kjava版本…

移动端开发:ionic如何与服务端进行数据交互

一、查看数据库表内容(以新闻表为例) 二、使用postman检查服务端接口是否可以正常获取数据 1、getNewsList方法用于新闻表多个数据查询 2、getSingleNewsById方法用于新闻表单个个数据查询 三、编写移动端代码 1、建立模型类News 新建model包用于存放…

水滴石穿C语言之指针综合谈

概述Joel Spolsky认为,对指针的理解是一种aptitude,不是通过训练就可以达到的。虽然如此,我还是想谈一谈这个C/C语言中最强劲也是最容易出错的要素。鉴于指针和目前计算机内存结构的关联,很多C语言比较本质的特点都孕育在其中&…

如何使用Doctl,DigitalOcean官方命令行客户端

An earlier version of this tutorial was written by Brennen Bearnes. 本教程的早期版本由Brennen Bearnes编写 。 介绍 (Introduction) DigitalOcean’s web-based control panel provides a point-and-click interface for managing Droplets. However, you may prefer a …

前端:Angular框架和Ionic框架的生命周期详解

一、angular 生命周期钩子 ngOnInit()在Angular第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件;ngAfterViewInit()初始化完组件视图及其子视图之后调用。 ngOnInit() 钩子是我们用得最频繁的一个,在使用新建组件命令ng …

将著名的反波播客默认加入到iPodderX On Mobile的频道里

经过测试,可以将著名的反波播客默认加入到iPodderX On Mobile的频道里了。请下载toodouPodcastMidlet-deployed.rar,建议在Nokia S60系列模拟器上测试。不过,反波播客的mp3下载太慢了,所以需要服务器端预先载入并转换。资源我的IP…