LaravelのAPIリソース

0
118

「LaravelのUI層」で少し触れましたが、APIリソースの使用方法に関しては少し細かく見ておいた方が良さそうなので、改めて今回はAPIリソースに特化して確認したいと思います。

なお、APIリソースに関してはEloquentとセットで語られる場合が多いですが、試しにEloquentとは無関係な自製クラスで動作確認したところ問題なく機能するようでしたので、特にEloquent限定と言う訳ではなさそうです。

基本形(APIリソース)

まずは基本形として単一オブジェクトからの変換を確認します。
具体的には以下のクラスを考えます。

class sampleClass
{
    public $prop = 'ABC';
}

上記に対するAPIリソースとして以下のようなものを用意します。

<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\JsonResource;

class Pattern1Resource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'param' => $this->prop,
        ];
    }
}

上記では指定されたオブジェクトのプロパティをそのまま出力用配列の要素に設定しているだけですが、何らかの変換を行うように処理を記述することもできますし、指定されたオブジェクトと関係なく要素を追加することもできるようです。
要は、あくまで出力用配列を定義することが主目的で、引数で指定したオブジェクトをどのように使うかは自由と言うことです。

コントローラにおける上記APIリソースの呼び出しは以下のような書式です。
$objectは先に示したクラスのインスタンスです。

return new Pattern1Resource($object);

処理結果は以下の通り。

array:1 [
  "data" => array:1 [
    "param" => "ABC"
  ]
]

ポイントは変換結果全体がキー「data」の要素として扱われる点です。
もし、「data」で括られたくなかったら「withoutWrapping」メソッドを実行します。

Pattern1Resource::withoutWrapping();
return new Pattern1Resource($object);

結果は以下の通り。

array:1 [
  "param" => "ABC"
]

「withoutWrapping」は本来は上記のように実行時に用いるものではなく、「AppServiceProvider」などで実行するもののようです。つまり、個別のアウトプットに対してではなく、システム全体で統一的に「data」で括るかどうかを決めておくべきだと言うことですね。

APIコレクションリソース

一覧表示などではアプトプット対象が単一オブジェクトではなく複数オブジェクトになります。
クエリビルダなどではコレクション型になりますし、Laravelの中で複数要素を扱うんならコレクションでしょ?ってことで、コレクション限定で変換の仕組みが用意されているようです。

生成方法は以下になります。

php artisan make:resource Misc/Pattern2Resource
php artisan make:resource Misc/Pattern2ResourceCollection

ポイントはコレクションの要素となるクラス用とコレクション全体用の2つのAPIリソースを用意することと、コレクション用のAPIリソースの名称を要素用のAPIリソースの名称+「Collection」にすると言う点です。

要素用のAPIリソースは以下のような内容になります。

<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\JsonResource;

class Pattern2Resource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'param' => $this->prop,
        ];
    }
}

コレクション用のAPIリソースは以下になります。

<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\ResourceCollection;

class Pattern2ResourceCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'list' => $this->collection,
        ];
    }
}

上記ではごく簡単に1つの要素しか持たない形になっていますが、当然ながら他の要素を持たせることもできます。
上記で「collection」と言うプロパティを参照していますが、先に示した名称の整合性を持たせることで、「$this->collection」として取得できるデータの形式が要素用APIリソースで変換された結果になります。

実行イメージは以下の通り。

return new Pattern2ResourceCollection($collection);

出力結果は以下の通り。

array:1 [
  "data" => array:1 [
    "list" => array:3 [
      0 => array:1 [
        "param" => 1
      ]
      1 => array:1 [
        "param" => 2
      ]
      2 => array:1 [
        "param" => 3
      ]
    ]
  ]
]

「data」で括られている点は単独オブジェクトを対象とする場合と同様です。

APIリソースのcollectionメソッド

ややこしいのですが、先に示したように最初からコレクションを対象としたAPIリソースが存在する一方で、単一オブジェクト用のAPIリソースにも「collection」メソッドが存在し、これにより同クラスのオブジェクトから構成されたコレクションの処理ができます。

実行イメージは以下の通り。

return Pattern1Resource::collection($collection);

結果は以下の通り。

array:1 [
  "data" => array:3 [
    0 => array:1 [
      "param" => "A"
    ]
    1 => array:1 [
      "param" => "B"
    ]
    2 => array:1 [
      "param" => "C"
    ]
  ]
]

コレクションリソースによる変換とメソッド「collection」による変換の違いは、前者がコレクション全体を一つの要素とし、他の要素も含めた出力用配列を生成できるのに対し、後者はあくまで指定されたAPIリソースにより各要素が変換された結果の配列ができるのみと言う点です。

リレーション

対象となるオブジェクトに別のオブジェクトが紐付いている場合も多いと思います。
そのようにツリー構造を持つオブジェクト群に対して全体を芋づる式に変換する方法も用意されています。

ここでは簡単に以下のようなMainとSubの2つのクラスを考えます。

class Main
{
    public $propMain1;
    public $propMain2;

    public function __construct($arg1, $arg2)
    {
        $this->propMain1 = $arg1;
        $this->propMain2 = $arg2;
    }
}

class Sub
{
    public $propSub;

    public function __construct($arg)
    {
        $this->propSub = $arg;
    }
}

上記を下記のような操作でインスタンス化および紐付けします。

$SubObj = new Sub('sub');
$MainObj = new Main('main', $SubObj);

単純に1つのMainインスタンスに1つのSubインスタンスが紐付いた形ができます。

上記に対して下記のような「MainResource」「SubResource」2つのAPIリソースを用意します。

<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\JsonResource;

class MainResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'paramMain1' => $this->propMain1,
            'paramMain2' => new SubResource($this->propMain2),
        ];
    }
}
<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\JsonResource;

class SubResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'paramSub' => $this->propSub,
        ];
    }
}

ポイントはMain側でSubの変換用のAPIリソースを呼び出している点です。

上記のように定義しておいて、ツリー構造のルートとなるMainインスタンスを引数にMainResourceを呼び出します。

return new MainResource($MainObj);

処理結果は以下の通り。Subも含めて変換が行われています。

array:1 [
  "data" => array:2 [
    "paramMain1" => "main"
    "paramMain2" => array:1 [
      "paramSub" => "sub"
    ]
  ]
]

では少し発展させて、MainインスタンスにSubのコレクションが紐付く場合も考えて見ます。

作り方としては以下のようにして見ます。

$baseArray = [];
for ($idx=0; $idx < 3; $idx++) {
    $baseArray[] = new Sub('sub'.$idx);
}
$MainObj = new Main('main', collect($baseArray));

上記を処理できるようMain用APIリソースを少し変更します。

<?php

namespace App\Http\Resources\Misc;

use Illuminate\Http\Resources\Json\JsonResource;

class MainResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'paramMain1' => $this->propMain1,
            'paramMain2' => SubResource::collection($this->propMain2),
        ];
    }
}

変わった点はSubの変換処理として「collection」メソッドを使うようにした点です。

この結果、アウトプットは以下のようになります。

array:1 [
  "data" => array:2 [
    "paramMain1" => "main"
    "paramMain2" => array:3 [
      0 => array:1 [
        "paramSub" => "sub0"
      ]
      1 => array:1 [
        "paramSub" => "sub1"
      ]
      2 => array:1 [
        "paramSub" => "sub2"
      ]
    ]
  ]
]

Mainインスタンスに紐付いたSubインスタンスのコレクションに関しても適正に変換されています。

総括

APIリソースの基本的なパターンとしては、

  • 単独オブジェクトの変換
  • コレクションの変換(コレクション全体を1つの要素とし、他の要素も追加できる)
  • コレクションの要素のみの変換

の3つがあり、かつこれらを組み合わせることで複雑なツリー構造を持つインスタンス群でもAPIでアウトプットとしてJSON化できる形(数値または文字列から構成される多次元配列)に変換できることが分かりました。

APIリソースをうまく使いこなせば、出力に関する処理を可読性・拡張性の良い形で集約できそうな印象です。