Laravel Eloquent:API资源

  • Post author:
  • Post category:其他



介绍


(


Introduction


)

When creating API’s, we sometimes specify the data we want back in the various controller actions:

创建API时,有时我们会在各种控制器操作中指定要返回的数据:

public function show(Book $book)
{
    return response()->json([
        'data' => [
            'title' => $book->title,
            'description' => $book->description,
            'author' => $book->author->name
        ]
    ]);
}

Notice we omitted the attributes

created_at

and

updated_at

when formatting the response?

注意在格式化响应时我们省略了

created_at



updated_at

属性吗?

Take another scenario where we want to update a book and expect a response back.

在另一种情况下,我们想要更新一本书并期望得到回复。

public function update(Request $request, Book $book)
{
    $book = $book->update($reques->all());
    return response()->json([
        'data' => [
            'title' => $book->title,
            'description' => $book->description,
        ]
    ]);
}

We still have to format the response for the

store()

method and probably return the created book as part of the response.

我们仍然必须格式化

store()

方法的响应,并可能将创建的书作为响应的一部分返回。

If we want the same

$book

attributes returned in all responses that involve a book resource, there is a high likelihood of forgetting some attribute, especially when working with many attributes. Again having to keep track of these attributes in every controller action that involves a book resource is a hassle. Assuming we have a book component on the frontend that is reused in all the occasions that involve a book resource. We are going to run into a problem when one of the attributes is missing.

如果我们希望在涉及书籍资源的所有响应中返回相同的

$book

属性,则很可能会忘记某些属性,尤其是在处理许多属性时。 同样,在涉及书籍资源的每个控制器操作中都必须跟踪这些属性,这很麻烦。 假设我们在前端有一个书本组件,该组件在涉及书本资源的所有场合都可以重用。 当缺少其中一个属性时,我们将遇到一个问题。

To handle inconsistencies in API resource responses, we may update the model with what should be returned when we call a model instance.

为了处理API资源响应中的不一致,我们可以使用调用模型实例时应返回的内容来更新模型。

Still working with the book example:

仍在处理书籍示例:

// Book Model
protected $hidden = ['created_at', 'updated_at'];

This means the

created_at

and

updated_at

attributes won’t be part of the response everytime we call on book resource.

这意味着,每次我们调用书本资源时,

created_at



updated_at

属性都不会成为响应的一部分。

But then again, how do we go about adding custom attributes that are not part of the original model:

但是再说一次,我们该如何添加不属于原始模型的自定义属性:

protected $appends = ['date_stored'];

public function getDateStored()
{
    return (string) $this->created_at->diffForHumans();
}

And that’s just one custom attribute. Had we wanted to include many attributes to be part of the model’s response, trust me we are going to end up with one bloated model. It’s a lot easier to create a dedicated resource to respond with exactly the data one needs.

那只是一个自定义属性。 如果我们想包括许多属性作为模型响应的一部分,请相信我,我们将最终得到一个one肿的模型。 创建专用资源以准确响应一个所需的数据要容易得多。


分形


(


Fractal


)

Before Laravel 5.5, Fractal, a third party package was the tool most developers used to format API responses. Fractal provides a presentation and transformation layer for complex data output, the likes found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc. Fractal encourages good API design, and responses will be consistent across the API.

在Laravel 5.5 Fractal之前,第三方软件包是大多数开发人员用来格式化API响应的工具。 Fractal为复杂数据输出(如在RESTful API中找到的类似对象)提供了表示和转换层,并且与JSON配合得很好。 可以将其视为JSON / YAML / etc的视图层。 Fractal鼓励良好的API设计,并且响应将在整个API中保持一致。


引入Laravel API资源


(


Introducing Laravel API resources


)

As of Laravel 5.5, Laravel has the capabilities Fractal offered with very little configuration. Setting up Fractal was a bit of a process – require the package, register service providers, create transform classes and so forth.

从Laravel 5.5开始,Laravel只需很少的配置就可以提供Fractal的功能。 设置Fractal只是一个过程-需要包,注册服务提供者,创建转换类等。

With API resources, developers can easily specify the data they want to be returned per model basis without having to update models or even specifying the attributes they want to be part of the response in the various controller methods.

借助API资源,开发人员可以轻松地按模型指定要返回的数据,而不必更新模型,甚至不必指定希望作为各种控制器方法中响应一部分的属性。

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

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

Laravel provides two artisan commands for generating

resources

and

collections

– don’t worry about the difference between the two yet, we’ll get there in a bit. But for both resources and collections, we have our response wrapped in a data attribute; a JSON response standard.

Laravel提供了两个用于生成

resources



collections

手Craft.io命令-不用担心两者之间的区别,我们将在稍后介绍。 但是对于资源和集合,我们的响应都包装在data属性中。 JSON响应标准。

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

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


先决条件:


(


Prerequisites:


)

To follow along in this artilce, you need to have the following prerequisites

要遵循本条款,您需要具备以下先决条件

  • Basic Laravel knowledge

    Laravel基础知识

  • A working Laravel development environment. Note, the project is built on Laravel 5.6 which requires

    PHP >= 7.1.3

    .

    一个有效的Laravel开发环境。 注意,该项目基于Laravel 5.6构建,需要

    PHP >= 7.1.3


歌曲API演示


(


Songs API Demo


)

Clone this

repo

and follow the instructions in the

README.md

to get things up and running.

克隆这个

回购协议

,并按照指示

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

If we peek inside the newly created resource file i.e. SongResource (

Resouce files usually go inside the App

\

Http

\

Resources folder

), the contents look like this:

如果我们窥视新创建的资源文件(即SongResource)(Resouce

文件通常位于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"
  }
}

###

###


包括关系


(


Including 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.

在这个项目中,我们只有两个模型

Album



Song

。 当前关系是

one-to-many

关系,这意味着专辑中有很多歌曲,而歌曲属于专辑。

Making an album be part of a song’s response is pretty straightforward. Let’s update the

toArray()

method inside the

SongResource

to take note of 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

we run:

要创建

AlbumResource

我们运行:

php artisan make:resource AlbumResource

Once the resource class has been created, we then specify the attributes we want to be included 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

代码段,因为我们不再需要它了。


使用资源时的条件


(


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 class

内部需要一个

ConditionallyLoadsAttributes trait

,它具有一些处理条件的方法。 仅举几例,我们有

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 eager 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 eager 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 learnt how to transform our responses, how do we actually verify that the response we get back is what we specified in our resource classes?

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

Here, we’ll 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

Notice I passed the

--unit

flag when generating the test to tell Laravel 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 ideally.

在这里,我们首先创建一个歌曲资源,然后在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);

I only matched against two attributes and their corresponding values to keep things simple but you can list as many attributes as you would like.

我只针对两个属性及其对应的值进行了匹配,以使事情保持简单,但是您可以根据需要列出尽可能多的属性。

What about making sure our model relationships are preserved even after converting our models to resources?

在将模型转换为资源后,如何确保保留模型关系又如何呢?

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_id



1

的歌曲,然后将其传递给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. I hope this helps.

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


回顾


(


Recap


)

Congratulations if you have managed to get to this point. We’ve looked at what Laravel API resources are, how to create them as well as how to test out various JSON responses. If you are the curious type, you can peep inside the

JsonResource

class and see all the methods that are available to us.

恭喜您成功达到目标。 我们已经研究了Laravel API资源是什么,如何创建它们以及如何测试各种JSON响应。 如果您是一个好奇的类型,则可以在

JsonResource

类内部进行窥视,并查看所有可用的方法。

Do check the

official docs

to learn more about API resources. The complete code for this tutorial is available on

GitHub

.

请检查

官方文档

以了解有关API资源的更多信息。 本教程的完整代码可在

GitHub



找到

翻译自:

https://scotch.io/tutorials/laravel-eloquent-api-resources