LoginSignup
0
0

LaravelのEloquentでorderByを使うとメソッドチェーンの補完が効かなくなるわけについて備忘録

Last updated at Posted at 2023-11-25

以下のようなページングを想定した一覧情報を返すAPIコントローラ処理を書いていた時のこと

ArticleController.php
    private const PER_PAGE = 30;

    public function index(IndexRequest $request): AnonymousResourceCollection
    {
        $query = Article::query();
        // withとかwhereとかいろいろ省略
        $articles = $query->paginate(self::PER_PAGE);

        return ArticleResource::collection($articles);
    }

一覧について「作成日の降順で取得」という仕様が抜けてたので以下のように修正しました

ArticleController.php
   public function index(IndexRequest $request): AnonymousResourceCollection
   {
       $query = Article::query();
       // withとかwhereとかいろいろ省略
-       $articles = $query->paginate(self::PER_PAGE);
+       $articles = $query->orderBy('created_at', 'desc')->paginate(self::PER_PAGE);

        return ArticleResource::collection($articles);
   }

こちら期待する動作はするのですが、先ほどまで効いていたpaginate()の補完が効きません。。
改修前後のカーソルを当てたときの型情報や入力補完の状況は以下の通り。

$articles paginate
改修前 caeea1fe-2ca9-5dbc-a4a0-15971099b291.png
LengthAwarePaginator
c176c64f-4a2f-7984-caf1-578abbdd65bd.png
補完が効く:innocent:
改修後 caeea1fe534af8a9-69e9-2ca7-6cba-35238e0d50d0.png
mixed
bcc77873-2501-33a7-c85c-442a6acebc31.png
補完が効かない:disappointed_relieved:

※LengthAwarePaginatorはpaginateのreturnで指しているインタフェースです

$articles\Illuminate\Contracts\Pagination\LengthAwarePaginatorインタフェースを指せていたのがmixedとなってしまいました。
この原因について調べてみました。


今回の備忘録におけるPHPとLaravelのバージョンや開発環境の情報は以下の通りです。
関係していそうなエディタの拡張機能やPHPのライブラリも記載しています。

項目 内容
PHP 8.2.10
Laravel Framework 10.22.0
OS Windows 11 Pro
エディタ VSCode
エディタの拡張機能 PHP Intelephense
その他 WSL2+Dockerで環境構築
composerで以下のライブラリを導入済み
- barryvdh/laravel-ide-helper
- laravel/pint
- nunomaduro/larastan

最初に調べたこと

  • Article::query() は Laravelフレームワーク内に実装されている \Illuminate\Database\Eloquent\Builder(以後、Eloquent\Builder) というクラスを返す
  • Eloquent\Builderの中には内包する形で \Illuminate\Database\Query\Builder(以後、Query\Builder)を持つ
    • Eloquent\Builderの中で $this->query として参照できるようにしている
    • このQuery\Builderのインスタンスはコンストラクタで設定している

image.png
image.png
※ちなみにですが、Eloquent\Builder内においてEloquent\BuilderとQuery\Builderはクラス名が被るため、Query\Builderの方はasでQueryBuilderという別名をつけていました。

image.png


その他、VSCodeでorderByの補完がされないことも確認

image.png

orderByはどこに実装されているか

image.png

  • Eloquent\Builder内を「 orderBy(」で検索してみましたが何も見つかりませんでした。

image.png
image.png
image.png

  • Eloquent\Builderの中でuse宣言によって参照されている3つのトレイト(BuildsQueries、ForwardsCalls、QueriesRelationships)の中にも見当たりません。。

image.png

  • Query\Builderの中にorderByの実装を見つけました!

こちら見つけたはいいですが開発中のコントローラにおける$queryの実体はEloquent\Builderです。
Query\Builderに実装されていることが確認できたとしても、これってコントローラ側のメソッドチェーンから呼び出せるものではなくない??という疑問が生まれました。

ここで煮詰まってしまいChatGPTに質問してみました。

image.png

私:
Eloquent\Builderに対してorderByが実装されていないにも関わらず
エラーにはならずにQuery\BuilderのorderByメソッドが呼べているのはなぜでしょうか?

ChatGPT:
EloquentのIlluminate\Database\Eloquent\Builderには
orderBy()メソッドが直接実装されていないにも関わらず、
orderBy()メソッドが使える理由は、Eloquentがマジックメソッドを使用しているためです。

マジックメソッド!
ってなんだっけ。。

PHP: マジックメソッド - Manual

Eloquent\Builderの__callマジックメソッドについて

Eloquent\Builderの__callマジックメソッドを確認してみます。

image.png

  • 中でいろいろやっていますが、1971行目の $this->forwardCallTo($this->query, $method, $parameters); でQuery\Builderに対してマジックメソッドの引数($method$parameters)を渡している風なメソッドの呼び出しを確認できます。

image.png

  • forwardCallToメソッドはEloquent\Builderの中に実装されているものではなくForwardsCallsというトレイトとして切り離されていました。
    • コードを言語化すると「$object$methodというメソッドを$parametersを渡して呼び出すことをトライする」といったことを行っているようです。
    • また、このforwardCallToメソッドについてPHPDocのコメントにmixedをreturnすると書いてあります。
      • これによってVSCodeの補完が切れているみたい

__callマジックメソッドから呼び出されるForwardsCallsトレイトのforwardCallToメソッドにおいて

  • $object$this->query
  • $method'orderBy'
  • $parameters['created_at', 'desc']

が入っているためtry/catchの中では以下のようなコードが実行されているということになります。

    try {
-        return $object->{$method}(...$parameters);
+        return $this->query->orderBy(...['created_at', 'desc']);
    } catch (Error|BadMethodCallException $e) {
~~~省略~~~
    }

これによってEloquent\BuilderからQuery\BuilderのorderByメソッドを呼び出しているということがわかりました。

結論

  • Eloquent\Builderに対してorderByメソッドの実装が無いのにも関わらず、エラーを出さずに動作しているのは同クラス内の__callマジックメソッドの実装のおかげ。
  • Eloquent\BuilderにはorderByメソッドの実体はないため、エディタの補完として出てこないのは当然

ちなみに

今回Eloquent\Builderの中身を調査した過程で、Eloquent\Builder内にlatestというメソッドを見つけました。

image.png

  • コメントや実装などから今回の「作成日の降順」を取得するという目的に合致したメソッドであることがわかります(パラメータとして他のカラム名を指定することもできる)。
  • こちらのメソッドは Eloquent\Builder内で$thisを返しておりメソッドチェーンが切れることもありません。
    • つまりは今回の「作成日の降順で取得」という改修におけるベストプラクティスになるのではないかなと思います。

以下のような感じで補完が切れないこと、$articlesがmixedとならないことを確認できました。

image.png
image.png


なお、こちらのlatestメソッドについてEloquent\Builderの$this->query->latestによってQuery\Builderの方に実装されている同latestメソッドを呼び出しており、そちらを見るとQuery\Builder内に実装されたorderByメソッドを呼び出していました。

image.png

感想

結構長いこと補完が効いたり効かなかったりする問題について疑問に思っていましたが、今回の調査によって概ね解決できてとてもすっきりしました。
なるべく補完の効くコードを書くことを心掛けたいなと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0