モデルをJSONに変換するLaravelEloquentAPIリソースを作成する方法
序章
APIを作成するとき、API応答で返される値をフィルタリング、解釈、またはフォーマットするために、データベースの結果を操作する必要があることがよくあります。 APIリソースクラスを使用すると、モデルとモデルコレクションをJSONに変換し、データベースとコントローラー間のデータ変換レイヤーとして機能させることができます。
APIリソースは、アプリケーションのどこでも使用できる統一されたインターフェイスを提供します。 雄弁な関係も世話をされます。
Laravel は、resourcesとcollectionsを生成するための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の指示に従って起動して実行します。
まず、リポジトリのクローンを作成します。
git clone `[email protected]:do-community/songs-demo.git`
次に、プロジェクトフォルダに移動します。
cd songs-demo
次のコマンドを実行して、.envファイルを作成します。
cp .env.example .env
この.envファイル内のデータベースクレデンシャルを更新します。
パッケージと依存関係をインストールします。
composer install
注:これを機能させるには、Laravel開発環境内にいる必要があります。 Vagrantを使用している場合は、composer installを実行する前に、必ずsshをVagrantに入れてください。
次に、アプリの暗号化キーを生成します。
php artisan key:generate
いくつかのサンプルデータを使用して、移行とシードデータベースを実行します。
php artisan migrate:refresh --seed
ステップ2—プロジェクトの設定
プロジェクトのセットアップで、手を汚し始めることができます。 また、これは小さなプロジェクトであるため、コントローラーを作成せず、代わりにルートクロージャー内の応答をテストします。
SongResourceクラスを生成することから始めましょう。
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);
}
}
デフォルトでは、toArray()メソッド内にparent::toArray($request)があります。 これのままにしておくと、表示されているすべてのモデル属性が応答の一部になります。 応答を調整するために、このtoArray()メソッド内でJSONに変換する属性を指定します。
次のスニペットに一致するように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にアクセスすると、IDが1の曲のSongResourceクラスで指定したキーと値のペアを含むJSON応答が表示されます。
{
data: {
id: 1,
title: "Mouse.",
rating: 3
}
}
ただし、URL /api/songsにアクセスしようとすると、例外がスローされます。
OutputProperty [id] does not exist on this collection instance.
これは、SongResourceクラスをインスタンス化するには、コレクションではなくコンストラクターにリソースインスタンスを渡す必要があるためです。 そのため、例外がスローされます。
単一のリソースの代わりにコレクションを返す必要がある場合は、引数としてコレクションを渡すResourceクラスで呼び出すことができる静的なcollection()メソッドがあります。 /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()メソッド内で、$this->collectionではなくdataの値をSongResource::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—モデル関係の作成
このプロジェクトでは、AlbumとSongの2つのモデルしかありません。 現在の関係はone-to-manyの関係です。つまり、アルバムには多くの曲があり、曲はアルバムに属しています。
次に、SongResourceクラス内のtoArray()メソッドを更新して、アルバムを参照するようにします。
app / Http / Resources / SongResource.php
[...]
class SongResource extends JsonResource
{
public function toArray($request)
{
return [
[...]
// other attributes
'album' => $this->album
];
}
}
応答に表示されるアルバム属性に関してより具体的にしたい場合は、曲で行ったのと同様のAlbumResourceを作成できます。
AlbumResourceを作成するには、次のコマンドを実行します。
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/songsURLにアクセスしてください。 曲ごとに、アルバムの詳細を取得するための追加のクエリを作成することに注意してください。 これは、熱心な読み込みの関係によって回避できます。 この場合、/api/songsルートクロージャー内のコードを次のように更新します。
ルート/api.php
[...]
return new SongsCollection(Song::with('album')->get());
ページを再度リロードすると、クエリの数が減ったことがわかります。
DB::listenスニペットはもう必要ないので、コメントアウトしてください。
ステップ4—リソースを操作するときに条件を使用する
時々、返される応答のタイプを決定する条件があるかもしれません。
採用できるアプローチの1つは、toArray()メソッド内にifステートメントを導入することです。 良いニュースは、条件を処理するためのいくつかのメソッドを持つJsonResourceクラス内に必要なConditionallyLoadsAttributes特性があるため、これを行う必要がないことです。
whenLoadedとmergeWhenの方法についてのみ説明しますが、ドキュメントは包括的です。
whenLoadedメソッド
このメソッドは、関連するモデルを取得するときに、熱心にロードされていないデータがロードされるのを防ぎ、それによって(N+1)クエリの問題を防ぎます。
参照ポイントとしてアルバムリソースを引き続き使用します(アルバムには多くの曲があります)。
app / Http / Resources / AlbumResource.php
public function toArray($request)
{
return [
[...]
// other attributes
'songs' => SongResource::collection($this->whenLoaded($this->songs))
];
}
アルバムを取得するときに曲を熱心にロードしていない場合は、空の曲コレクションになってしまいます。
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リソース
応答を変換する方法を学習したので、返される応答がリソースクラスで指定したものであることをどのように確認しますか?
次に、応答に正しいデータが含まれていることを確認し、雄弁な関係が維持されていることを確認するテストを作成します。
テストを作成しましょう:
php artisan make:test SongResourceTest --unit
テストを生成するときの--unitフラグに注意してください。これにより、これが単体テストになることがLaravelに通知されます。
注:検証中に、make:testコマンドを実行すると、エラーTest already exists!が観察されました。 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();
}
}
ここでは、最初に曲のリソースを作成し、次にSongResourceでjsonSerialize()を呼び出して、リソースをJSON形式に変換します。これがフロントエンドに送信されます。
また、応答の一部となる曲の属性がすでにわかっているので、次のようにアサーションを作成できます。
tests / Unit / SongResourceTest.php
[...]
$this->assertArraySubset([
'title' => $song->title,
'rating' => $song->rating
], $resource);
この例では、titleとratingの2つの属性を照合しました。 複数の属性をリストできます。
モデルをリソースに変換した後でもモデルの関係が維持されるようにする場合は、次を使用できます。
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_idが1の曲を作成し、その曲をSongResourceクラスに渡してから、最終的にリソースをJSON形式に変換します。
曲とアルバムの関係が引き続き維持されていることを確認するために、作成した$resourceのアルバム属性をアサーションします。 そのようです:
tests / Unit / SongResourceTest.php
[...] $this->assertInstanceOf(AlbumResource::class, $resource["album"]);
ただし、$this->assertInstanceOf(Album::class, $resource["album"])を実行した場合、アルバムインスタンスをSongResourceクラス内のリソースに変換しているため、テストは失敗することに注意してください。
注:検証中に、次のコマンドでこれらのテストを実行できることが確認されました。
vendor/bin/phpunit
要約として、最初にモデルインスタンスを作成し、インスタンスをリソースクラスに渡し、リソースをJSON形式に変換してから、最終的にアサーションを作成します。
結論
Laravel APIリソースとは何か、それらを作成する方法、およびJSON応答をテストする方法を見てきました。 JsonResourceクラスを自由に調べて、使用可能なすべてのメソッドを確認してください。
Laravel APIリソースの詳細については、公式ドキュメントを確認してください。