序章

APIを作成するとき、API応答で返される値をフィルタリング、解釈、またはフォーマットするために、データベースの結果を操作する必要があることがよくあります。 APIリソースクラスを使用すると、モデルとモデルコレクションをJSONに変換し、データベースとコントローラー間のデータ変換レイヤーとして機能させることができます。

APIリソースは、アプリケーションのどこでも使用できる統一されたインターフェイスを提供します。 雄弁な関係も世話をされます。

Laravel は、resourcescollectionsを生成するための2つのartisanコマンドを提供します。これら2つの違いは後で理解します。 ただし、リソースとコレクションの両方について、応答をデータ属性(JSON応答標準)でラップしています。

次のセクションでは、デモプロジェクトを試して、APIリソースを操作する方法を見ていきます。

前提条件

このガイドに従うには、次の前提条件を満たしている必要があります。

  • 動作するLaravel開発環境。 これを設定するには、 Ubuntu18.04にLaravelアプリケーションをインストールして構成する方法に関するガイドに従ってください。

このチュートリアルは、PHPv7.1.3とLaravelv5.6.35で作成されました。

このチュートリアルは、PHP v7.3.11、Composer v.1.10.7、MySQL 5.7.0、およびLaravelv.5.6.35で検証されました。

ステップ1—スターターのクローンを作成する

このリポジトリのクローンを作成し、 README.md 物事を稼働させるために。

まず、リポジトリのクローンを作成します。

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

次に、プロジェクトフォルダに移動します。

  1. cd songs-demo

作成する .env 次のコマンドを実行してファイルを作成します。

  1. cp .env.example .env

この中のデータベースクレデンシャルを更新します .env ファイル。

パッケージと依存関係をインストールします。

  1. composer install

注:これを機能させるには、Laravel開発環境内にいる必要があります。 Vagrantを使用している場合は、次のことを確認してください ssh 実行する前にVagrantに composer install.

次に、アプリの暗号化キーを生成します。

  1. php artisan key:generate

いくつかのサンプルデータを使用して、移行とシードデータベースを実行します。

  1. php artisan migrate:refresh --seed

ステップ2—プロジェクトの設定

プロジェクトのセットアップで、手を汚し始めることができます。 また、これは小さなプロジェクトであるため、コントローラーを作成せず、代わりにルートクロージャー内の応答をテストします。

を生成することから始めましょう SongResource クラス:

  1. php artisan make:resource SongResource

リソースファイルは通常、 App\Http\Resources フォルダ。

新しく作成されたリソースファイルの内部を覗いてみましょう-SongResource:

app / Http / Resources / SongResource.php
[...]
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);
    }
}

デフォルトでは、 parent::toArray($request)toArray() 方法。 これのままにしておくと、表示されているすべてのモデル属性が応答の一部になります。 応答を調整するために、この中でJSONに変換する属性を指定します toArray() 方法。

更新しましょう toArray() 次のスニペットに一致するメソッド:

app / Http / Resources / SongResource.php
[...]
public function toArray($request)
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'rating' => $this->rating,
    ];
}

ご覧のとおり、モデルのプロパティには、 $this リソースクラスは、基になるモデルへのメソッドアクセスを自動的に許可するため、変数です。

更新しましょう routes/api.php 次のスニペットを使用:

ルート/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());
});

URLにアクセスすると /api/songs/1、で指定したキーと値のペアを含むJSON応答が表示されます SongResource IDがの曲のクラス 1:

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

ただし、URLにアクセスしてみると /api/songs、例外がスローされます:

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

これは、SongResourceクラスをインスタンス化するには、コレクションではなくコンストラクターにリソースインスタンスを渡す必要があるためです。 そのため、例外がスローされます。

単一のリソースではなくコレクションを返したい場合は、静的なものがあります collection() コレクションを引数として渡すResourceクラスで呼び出すことができるメソッド。 更新しましょう /songs これへのルート閉鎖:

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

訪問 /api/songs URLは、すべての曲を含むJSON応答を返します。

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

リソースは、単一のリソースまたはコレクションを返す場合でも問題なく機能しますが、応答にメタデータを含める場合は制限があります。 それはどこです Collections 私たちの救助に来てください。

コレクションクラスを生成するには、次のコマンドを実行します。

php artisan make:resource SongsCollection

JSONリソースとJSONコレクションの主な違いは、リソースが JsonResource クラスであり、コレクションが拡張している間にインスタンス化されるときに単一のリソースが渡されることを期待します ResourceCollection クラスであり、インスタンス化されるときに引数としてコレクションを期待します。

メタデータビットに戻ります。 合計曲数などのメタデータを応答の一部にしたいと仮定して、 ResourceCollection クラス:

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

更新すると /api/songs これへのルート閉鎖:

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

そして、URLにアクセスします /api/songs、データ属性内のすべての曲とメタビット内の合計カウントが表示されます。

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

ただし、問題があります。データ属性内の各曲は、SongResource内で以前に定義した仕様にフォーマットされておらず、代わりにすべての属性を持っています。

これを修正するには、 toArray() メソッド、の値を設定します dataSongResource::collection($this->collection) 持っている代わりに $this->collection.

私たちの toArray() メソッドは次のようになります。

app / Http / Resources / SongsCollection.php
[...]
public function toArray($request)
{
    return [
        'data' => SongResource::collection($this->collection),
        'meta' => ['song_count' => $this->collection->count()]
    ];
}

にアクセスして、応答で正しいデータを取得したことを確認できます。 /api/songs もう一度URL。

コレクションではなく単一のリソースにメタデータを追加したい場合はどうなりますか? 幸いなことに、 JsonResource クラスには additional() リソースを操作するときに応答の一部にしたい追加データを指定できるメソッド:

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

この場合、応答は次のようになります。

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

ステップ3—モデル関係の作成

このプロジェクトでは、2つのモデルしかありません。 AlbumSong. 現在の関係は one-to-many 関係、つまりアルバムには多くの曲があり、曲はアルバムに属します。

更新します toArray() アルバムを参照するようにSongResourceクラス内のメソッド:

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

応答に表示されるアルバム属性に関してより具体的にしたい場合は、曲で行ったのと同様のAlbumResourceを作成できます。

を作成するには AlbumResource、 走る:

  1. php artisan make:resource AlbumResource

リソースクラスが作成されたら、応答に含める属性を指定します。

app / Http / Resources / AlbumResource.php
[...]
class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'title' => $this->title
        ];
    }
}

そして今、 SongResource する代わりにクラス 'album' => $this->album、私たちは利用することができます AlbumResource 作成したばかりのクラス。

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

私たちが訪問した場合 /api/songs URLをもう一度入力すると、アルバムが応答の一部になることがわかります。 このアプローチの唯一の問題は、 N + 1 クエリの問題。

デモンストレーションの目的で、次のスニペットを routes/api.php ファイル:

ルート/api.php
[...]
DB::listen(function($query) {
    var_dump($query->sql);
});

訪問 /api/songs もう一度URL。 曲ごとに、アルバムの詳細を取得するための追加のクエリを作成することに注意してください。 これは、熱心な読み込みの関係によって回避できます。 この場合、内部のコードを更新します /api/songs ルート閉鎖:

ルート/api.php
[...]
return new SongsCollection(Song::with('album')->get());

ページを再度リロードすると、クエリの数が減ったことがわかります。

コメントアウト DB::listen スニペットはもう必要ないので。

ステップ4—リソースを操作するときに条件を使用する

時々、返される応答のタイプを決定する条件があるかもしれません。

私たちが取ることができる1つのアプローチは、私たちの内部にifステートメントを導入することです toArray() 方法。 良いニュースは、あるのでそれをする必要がないということです ConditionallyLoadsAttributes 条件を処理するためのいくつかのメソッドを持つJsonResourceクラス内で必要な特性。

のみ説明します whenLoadedmergeWhen メソッドが、ドキュメントは包括的です。

The whenLoaded 方法

このメソッドは、関連するモデルを取得するときに、熱心にロードされていないデータがロードされるのを防ぎ、それによって (N+1) クエリの問題。

参照ポイントとしてアルバムリソースを引き続き使用します(アルバムには多くの曲があります)。

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

アルバムを取得するときに曲を熱心にロードしていない場合は、空の曲コレクションになってしまいます。

The mergeWhen 方法

一部の属性とその値が応答の一部になるかどうかを指示するifステートメントを使用する代わりに、 mergeWhen() 最初の引数として評価する条件と、条件がtrueと評価された場合に応答の一部となることを意図したキーと値のペアを含む配列を受け取るメソッド:

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

これは、ifステートメントがreturnブロック全体をラップする代わりに、よりクリーンでエレガントに見えます。

ステップ5—ユニットテストAPIリソース

応答を変換する方法を学習したので、返される応答がリソースクラスで指定したものであることをどのように確認しますか?

次に、応答に正しいデータが含まれていることを確認し、雄弁な関係が維持されていることを確認するテストを作成します。

テストを作成しましょう:

  1. php artisan make:test SongResourceTest --unit

に注意してください --unit テストを生成するときにフラグを立てます。これにより、これが単体テストになることがLaravelに通知されます。

注:検証中にエラーが発生しました Test already exists! 実行時に観察された make:test 指図。 の内容 SongResourceTest.php いくつかの古いテストが含まれているようです。 このファイルの内容を、このチュートリアル用に提供されているコードに置き換えます。

テストを書いて、からの応答を確認することから始めましょう。 SongResource クラスには正しいデータが含まれています:

tests / Unit / SongResourceTest.php
[...]
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();
    }
}

ここでは、最初に曲のリソースを作成してから、 jsonSerialize() SongResourceでリソースをJSON形式に変換します。これは、フロントエンドに送信されるものです。

また、応答の一部となる曲の属性がすでにわかっているので、次のようにアサーションを作成できます。

tests / Unit / SongResourceTest.php
[...]
$this->assertArraySubset([
    'title' => $song->title,
    'rating' => $song->rating
], $resource);

この例では、2つの属性を照合しました。 titlerating. 複数の属性をリストできます。

モデルをリソースに変換した後でもモデルの関係が維持されるようにする場合は、次を使用できます。

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

ここでは、 album_id1 次に、曲をSongResourceクラスに渡してから、最終的にリソースをJSON形式に変換します。

曲とアルバムの関係が引き続き維持されていることを確認するために、 $resource 作成したばかりです。 そのようです:

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

ただし、 $this->assertInstanceOf(Album::class, $resource["album"]) アルバムインスタンスをSongResourceクラス内のリソースに変換しているため、テストは失敗します。

注:検証中に、次のコマンドでこれらのテストを実行できることが確認されました。

  1. vendor/bin/phpunit

要約として、最初にモデルインスタンスを作成し、インスタンスをリソースクラスに渡し、リソースをJSON形式に変換してから、最終的にアサーションを作成します。

結論

Laravel APIリソースとは何か、それらを作成する方法、およびJSON応答をテストする方法を見てきました。 お気軽に JsonResource クラスを作成し、使用可能なすべてのメソッドを確認します。

Laravel APIリソースの詳細については、公式ドキュメントを確認してください。