いっきのblog

技術とか色々

LaravelのMacroを使ってBuilderに機能を追加する方法

どうも、くずき(@kzkohashi)です。
先日、正しいJSON APIのフォーマットにするために以下のライブラリを導入した。

github.com

paginateは正しいクエリーパラメーターを取っていない

恥ずかしながら最近知ったのだが、Eloquentで使用しているpaginateは、完璧なJSON APIのフォーマットに対応していない。
ページ番号で使われるpage=2などのクエリパラメーターは、正しくは、page[number]=2にすべきみたいだ。加えて、ページ番号だけでなく、そのページにどれくらい表示させたいかなどはpage[size]=20で表現する。

なので、導入前と導入後ではこうなる。
導入前
xxx.com/xxx?page=2&size=20 // sizeに関しては独自実装

導入後
xxx.com/xxx?page[number]=2&page[size]=20

さらにきになる方は以下を参照。

Note: JSON API is agnostic about the pagination strategy used by a server. Effective pagination strategies include (but are not limited to): page-based, offset-based, and cursor-based. The page query parameter can be used as a basis for any of these strategies. For example, a page-based strategy might use query parameters such as page[number] and page[size], an offset-based strategy might use page[offset] and page[limit], while a cursor-based strategy might use page[cursor].

JSON API — Latest Specification (v1.0)

ライブラリの実装を見て見る

ここからが本題。
このライブラリを導入し、設定ファイルを書くとjsonPaginate()が使えるようになる。

YourModel::where('my_field', 'myValue')->jsonPaginate();

どうやって実装したんだろうとコードをみて見るとBuilder::macroによって独自の関数を追加できるみたいだ。

<?php

  ...
  
  Builder::macro(config('json-api-paginate.method_name'), function (int $maxResults = null, int $defaultSize = null) {
      $maxResults = $maxResults ?? config('json-api-paginate.max_results');
      $defaultSize = $defaultSize ?? config('json-api-paginate.default_size');
      $numberParameter = config('json-api-paginate.number_parameter');
      $sizeParameter = config('json-api-paginate.size_parameter');
      $size = request()->input('page.'.$sizeParameter, $defaultSize);
      if ($size > $maxResults) {
          $size = $maxResults;
      }
      return $this->paginate($size, ['*'], 'page.'.$numberParameter)
          ->setPageName('page['.$numberParameter.']')
          ->appends(array_except(request()->input(), 'page.'.$numberParameter));
  });

github.com

これはLaravelMacroという機能で、Laravelが提供している機能に独自のメソッドなどを拡張実装できるみたい。
Builder以外には、HTML, Responseなどを拡張できる。

他にどう使えるか?

例えば、Eloquentが返すModelではなく、独自に実装したEntityを返すcollectionを用意するとか

<?php

Builder::macro('toDomainCollection', function () {
    /** @noinspection PhpUndefinedMethodInspection */
    return $this->get()->map(function (Domainable $model) {
        return $model->toDomain();
    });
});

レスポンスである条件の時に決まった内容などを送る時とか

<?php

Response::macro('notPermitted', function ($type = null) {
    if (strtolower($type) === 'success') {
        return $this->json(['data' => []], 200);
    }
    return $this->json(['data' => []], 403);
});

今まで作ってたやり方を見直すだけでも色々ありそーだなーと思います。