しがないプログラマの雑記帳

冴えないおじさんの、備忘録な雑記

LaravelでREST APIのJSONを柔軟に変形させたい

この記事は Web × PHP TechCafe Advent Calendar 2019 の12/15の記事です。

LaravelでモデルをREST APIのレスポンスとして返す際には、コントローラからモデルのCollectionやインスタンスを返すことで、レスポンスはJSON形式で返されます。しかし、モデルのプロパティを変形してからJSONのレスポンスとして返したいこともあります。

Laravelの API Resource を利用することで、REST APIのレスポンスとして返すJSONの値を柔軟に設定できます。時刻のフォーマットを変換してJSONに埋め込むような場合に便利です。

テーブル構造の例

例として、決まった日付の範囲で、日付ごとのスケジュールを編集するシステムを考えます。日付の範囲を格納するテーブルを dates, スケジュールのデータを格納するテーブルを schedules とします。テーブル構造は以下に示します。

f:id:sierra-kilo:20191214103014p:plain

テーブル定義をマイグレーションファイルに記述します。dates のテーブル構造を CreateDatesTable クラス、 schedules のテーブル構造を CreateSchedulesTable クラスとしてそれぞれ定義します。(use 文やコメントは省略しています)

class CreateDatesTable extends Migration
{
    public function up()
    {
        Schema::create('dates', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->date('date');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('dates');
    }
}
class CreateSchedulesTable extends Migration
{
    public function up()
    {
        Schema::create('schedules', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->text('description')->nullable();
            $table->unsignedBigInteger('date_id');
            $table->time('start');
            $table->time('end');
            $table->text('remarks')->nullable();
            $table->timestamps();
            $table->softDeletes();

            $table->foreign('date_id')->references('id')->on('dates');
        });
    }

    public function down()
    {
        Schema::dropIfExists('schedules');
    }
}

モデルとAPI Resourceの実装

モデルの定義

テーブルをPHPのオブジェクトとして扱うために、 datesschedules テーブルに対応するモデルクラス DateSchedule クラスを定義します。外部キー制約を schedules テーブルの date_id カラムに設定したので、モデルにもリレーションを設定します。

class Date extends Model
{
    protected $fillable = ['date'];
}
class Schedule extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'title',
        'description',
        'date_id',
        'start',
        'end',
        'remarks'
    ];

    public function date()
    {
        return $this->belongsTo(Date::class);
    }
}

JSONの構造とAPI Resource

スケジュールを以下のようなJSON形式のレスポンスで返すことを考えます。

{
  "data": [
    {
      "id": 1,
      "title": "PHP Conference Japan 2019",
      "description": "国内最大級のPHPのカンファレンス",
      "date": "2019-12-01",
      "start": "10:00",
      "end": "18:00",
      "remarks": "東京都大田区南蒲田1丁目20-20 大田区産業プラザ PiO"
    }
  ]
}

JSONに含まれる date には Date クラスの date プロパティの値を、 startend はそれぞれ H:i 形式の文字列を設定します。

JSONレスポンスをAPI Resourceとして返すために、 Resource クラスを作成します。雛形を作成するコマンドは以下の通りです。

php artisan make:resource ScheduleResource

雛形を作成したら、 toArray() の内容を以下に示すコード例のように修正します。

class ScheduleResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
            'date' => $this->date->date,
            'start' => Carbon::parse($this->start)->format('H:i'),
            'end' => Carbon::parse($this->end)->format('H:i'),
            'remarks' => $this->remarks,
        ];
    }
}

変数 $this には Schedule クラスのインスタンスが格納されているので、リレーション先の Date クラスのオブジェクトも date プロパティから取得できます。日付の文字列を取得するためには $this->date->date を参照します。予定の開始時刻と終了時刻を示すプロパティ startendH:i 形式の文字列に変換するために、Laravelに組み込まれているライブラリ Carbon を利用します。

コントローラからAPI Resourceを用いてレスポンスを生成

API Responseをコントローラのメソッドから返すことで、JSON形式のレスポンスが生成できます。単純にAPI Resourceを返す場合は、ルーティングで直接 Resource クラスのインスタンスを返却することもできます。

モデルのインスタンス単体を Resource として返す場合は、 Resource クラスのインスタンスを返却することで実現できます。複数のモデルのインスタンスCollection として返したい場合は、 collection()を利用します。 Resource クラスのインスタンスnew する際の引数はモデルクラスのインスタンスを、 collection()の引数にはモデルのインスタンスCollection を指定します。

APIコントローラの index() から、スケジュールを開始時刻順にソートしてREST APIとしてJSON形式のレスポンスを返すコード例は以下の通りです。

public function index()
{
    return ScheduleResource::collection(
        Schedule::orderBy('date_id')
            ->orderBy('start')
            ->get()
    );
}

まとめ

LaravelのAPI Resourceを利用すれば、REST APIのレスポンスとして返すJSONを柔軟に変形できます。デフォルトでは JSONdata フィールドに Resource クラスの toArray() で返されるデータが格納されますが、サービスプロバイダで Resource::withoutWrapping()data キーを削除できます。

モデルのフィールドを変形する程度なら、モデルのアクセサを利用するという手段もあります。レスポンスで返すJSONのフィールドをどのように変形したいかによって、モデルのアクセサとAPI Resourceを使い分けると良さそうですね。

参考文献