Laravel の Resource::collection と ResourceCollection の比較

目次

ResourceCollection

こちらの記事 Laravel の API Resource の使い方(基礎編)の続きおよび関連記事となります。

前回書ききれなかった、Laravel のResourceCollectionについてまとめておきたいと思います。

もしご自身で先に公式ドキュメントをご覧になられる方は下のリンクから眺めてみてください。
Eloquent: API Resources ー 公式ドキュメントです。英語なので、英語が苦手な方は下の翻訳されたものをご覧ください。
APIリソース ー 有志が翻訳してくれているものです。

対象となる方

  • 前回記事を読まれた方
  • Laravelを学習されている方
  • APIリソースの collection メソッドに物足りなさを感じている方

 ※Laravelのバージョンは8系で書いていきます。それ以前は確認しておりません。

API リソースのおさらい

前回の記事では、JsonResourceクラスを継承し、JSONの整形を行うリソースクラスの作成を行いました。リソースクラスの使い方については、インスタンスの生成をして、そこにモデルを引数として渡してあげると望み通りのJSONデータに整形してくれるという非常に使い勝手の良いものでしたね。だた、前回の記事内でも触れたのですが、リソースクラスは基本的には1つのデータに対して整形するようになっていますので、検索によって複数のデータが取得された場合は、望み通りの形式で返却できないといったことが起こります。
そこで、今回はその「複数のデータ」を対象としたJSONデータの整形をやっていきたいと思います!

事前準備

今回作るのは、前回の一言を投稿表示するアプリの機能を追加していくという想定でいきたいと思います!
従いまして、一緒にコードも書いてくださるという方は、今一度前回の記事をなぞって頂いて、環境を整えていただければと思います。
ちなみに、前回はデータの作成部分は完全に読者の皆様に任せっきりでしたので、簡単にファクトリーやシーダーを使ってデータの作成をしておきましょうか。
databaseディレクトリの中のfactoriesにあるTweetFactory.phpファイルのdefinitionメソッドを以下のように編集してください。

    public function definition()
    {
        return [
            'tweet' => $this->faker->realText($maxNbChars = 60, $indexSize = 2),
            'user_id' => $this->faker->numberBetween($min = 1, $max = 10),
        ];
    }

次に、databaseディレクトリの中のseedersにあるDatabaseSeeder.phpファイルのrunメソッドを以下のように編集してください。

    public function run()
    {
        \App\Models\User::factory(10)->create();
        \App\Models\Tweet::factory(100)->create();
    }

これでダミーデータを作る準備ができましたので、次のコマンドをターミナル上で叩いてください。
※下記コマンドはDB上のデータを一旦空っぽにするので入力する際は消えても大丈夫かどうかご確認ください!

 php artisan migrate:refresh --seed

これでダミーデータがDB上に作られたかと思います。

ResourceCollection

環境が整いましたら、早速 ResourceCollection を作っていきましょう。以下のコマンドをターミナル上で叩いてください。

php artisan make:resource TweetCollection

するとapp/Http/Resourcesディレクトリ内に以下のようなTweetCollection.phpが作成されますので、編集していきましょう!

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

こちら一見、前回作ったTweetResource.phpと同じように見えるかもしれませんが、継承元が異なります。TweetResource.php は JsonResource クラスを継承しているのに対して TweetCollection.php は ResourceCollection クラスを継承しています。
ではCollection側を編集していきましょう! toArray()内を以下のように変えてみてください。

    public function toArray($request)
    {
        return [
            'tweet_count' => $this->count(),
            'data' => $this->collection,
            'links' => [
                'self' => $request->fullUrl(),
            ]
        ];
    }

構造としてはシンプルな感じですが、ユーザーの詳細画面に遷移した時に表示されるデータを想定しました。「該当ユーザの総ツイート数」「該当ユーザのツイートの一覧」を取得する際に使えるかと思います。

では、このコレクションを呼び出すファンクションをコントローラーに作りましょう!TweetController.phpに以下を追記してください。

<?php

namespace App\Http\Controllers;

use App\Models\Tweet;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Resources\TweetResource;
use App\Http\Resources\TweetCollection;

class TweetController extends Controller
{
    /**
     * 全ての一言を取得するFunction
     */
    public function index()
    {
        return TweetResource::collection(Tweet::all());
    }

    /**
     * 1件の一言を取得するFunction
     */
    public function show($tweet_id)
    {
        $tweet = Tweet::find($tweet_id);
        return new TweetResource($tweet);
    }

    /**
     * ユーザーに紐付く一言を取得するFunction
     */
    public function tweetsByUserId($user_id)
    {
        $user = User::find($user_id);
        return new TweetCollection($user->tweets);
    }
}

あとは呼び出すルーティングを確保すれば完了です! routesディレクトリのapi.phpファイルを以下のように編集してください。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TweetController;

Route::get('/tweet', [TweetController::class, 'index']);
Route::get('/tweet/{tweet_id}', [TweetController::class, 'show']);
Route::get('/tweet/user/{user_id}', [TweetController::class, 'tweetsByUserId']);

これで完成です! /tweet/user/2などにアクセスしてみましょう! user_id が2のユーザのツイートが取得できるはずです!
結果は以下です。

{
    "tweet_count": 6,
    "data": [
        {
            "data": {
                "type": "tweet",
                "tweet_id": 10,
                "attributes": {
                    "tweet": "Alice had not attended to this last remark, 'it's a.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/10"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 15,
                "attributes": {
                    "tweet": "Alice joined the procession, wondering very much to-night.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/15"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 74,
                "attributes": {
                    "tweet": "Involved in this way! Stop this moment, I tell you!' said.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/74"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 76,
                "attributes": {
                    "tweet": "SOUP!' 'Chorus again!' cried the Mock Turtle replied in an.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/76"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 86,
                "attributes": {
                    "tweet": "And how odd the directions will look! ALICE'S RIGHT FOOT.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/86"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 92,
                "attributes": {
                    "tweet": "AND SHOES.' the Gryphon repeated impatiently: 'it begins.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/92"
            }
        }
    ],
    "links": {
        "self": "http://127.0.0.1:8000/api/tweet/user/2"
    }
}

1行目に ツイート数6と出てますね!最終行あたりにリストを取得するためにアクセスしたURL(エンドポイント)も表示されていますね!

これで終わり!と言いたいところなんですが、
「結局 TweetResource の collection メソッドと何が違うの?」という方もいらっしゃるかと思いますので、比較をしてみましょう!
TweetControllerのtweetsFindByUserId()ファンクション内の return を TweetResource の collection メソッドに変えてみましょう!

    public function tweetsFindByUserId($user_id)
    {
        $user = User::find($user_id);
        return TweetResource::collection($user->tweets);
    }

同様に/tweet/user/2などにアクセスしてみましょう!結果が以下です!

{
    "data": [
        {
            "data": {
                "type": "tweet",
                "tweet_id": 10,
                "attributes": {
                    "tweet": "Alice had not attended to this last remark, 'it's a.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/10"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 15,
                "attributes": {
                    "tweet": "Alice joined the procession, wondering very much to-night.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/15"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 74,
                "attributes": {
                    "tweet": "Involved in this way! Stop this moment, I tell you!' said.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/74"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 76,
                "attributes": {
                    "tweet": "SOUP!' 'Chorus again!' cried the Mock Turtle replied in an.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/76"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 86,
                "attributes": {
                    "tweet": "And how odd the directions will look! ALICE'S RIGHT FOOT.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/86"
            }
        },
        {
            "data": {
                "type": "tweet",
                "tweet_id": 92,
                "attributes": {
                    "tweet": "AND SHOES.' the Gryphon repeated impatiently: 'it begins.",
                    "tweet_at": "2021/05/17 04:45",
                    "tweet_by": "Ambrose Jaskolski DDS"
                }
            },
            "links": {
                "self": "http://127.0.0.1:8000/tweet/92"
            }
        }
    ]
}

ま、当然ですが、 "tweet_count": 6,はありませんし、 "self": "http://127.0.0.1:8000/api/tweet/user/2"の部分はありません。カスタマイズした部分が少ないので大きく影響がないように感じるかと思いますが、色々とデータを付随させたい場合は ResourceCollection で構築していったほうがいいかと思います!

まとめ

以上で Resourceのcollectionメソッドと ResourceCollection の比較を終えたいと思います。多くの場合 Resource の collection メソッドで事足りるような気がするのですが、いろいろなカスタマイズを加えたい場合は ResourceCollection を使うことをオススメします。
この辺整えることができれば、アプリのAPIを一般公開化するのも見えてきそうですね^^v ビバ!美しいJSON返却データ!
ということで、ではまた〜

よかったらシェアしてね!

この記事を書いた人

Web Developer / Educator