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

以下のようなページングを想定した一覧情報を返すAPIコントローラ処理を書いていた時のこと
```php: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);
    }
```

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

```diff_php: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|
|---|---|---|
|改修前|<img width="300" alt="caeea1fe-2ca9-5dbc-a4a0-15971099b291.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/caeea1fe-2ca9-5dbc-a4a0-15971099b291.png"><br />LengthAwarePaginator|<img width="300" alt="c176c64f-4a2f-7984-caf1-578abbdd65bd.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/c176c64f-4a2f-7984-caf1-578abbdd65bd.png"><br />補完が効く:innocent:|
|改修後|<img width="300" alt="caeea1fe534af8a9-69e9-2ca7-6cba-35238e0d50d0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/534af8a9-69e9-2ca7-6cba-35238e0d50d0.png"><br />mixed|<img width="300" alt="bcc77873-2501-33a7-c85c-442a6acebc31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/bcc77873-2501-33a7-c85c-442a6acebc31.png"><br />補完が効かない: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](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client)|
|その他|WSL2+Dockerで環境構築<br />composerで以下のライブラリを導入済み<br />- [barryvdh/laravel-ide-helper](https://github.com/barryvdh/laravel-ide-helper)<br />- [laravel/pint](https://github.com/laravel/pint)<br />- [nunomaduro/larastan](https://github.com/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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/1c969b3d-6285-691e-42ba-30a3101df92a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/9eb93121-f44c-cb08-87d9-077a224d180e.png)
`※ちなみにですが、Eloquent\Builder内においてEloquent\BuilderとQuery\Builderはクラス名が被るため、Query\Builderの方はasでQueryBuilderという別名をつけていました。`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/1f287bd0-c704-2c87-2894-3258616de283.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/0ec8de91-cb2e-c749-d6d4-b7a01f588e52.png)

## orderByはどこに実装されているか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/97a30c3b-28d5-e896-66bd-5e518ee885bc.png)
- Eloquent\Builder内を「 orderBy(」で検索してみましたが何も見つかりませんでした。

---
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/64ad02d8-770d-ca03-adb0-664a7a123a67.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/cd0e77ce-cb39-c932-27b5-1cb5e4e90be0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/b666bcb3-44e0-adab-4977-4aaf1a07da2d.png)
- Eloquent\Builderの中でuse宣言によって参照されている3つのトレイト（BuildsQueries、ForwardsCalls、QueriesRelationships）の中にも見当たりません。。

---
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/ea1413e8-c370-700e-dab4-95214701fee8.png)
- Query\Builderの中に`orderBy`の実装を見つけました！

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2a1c9245-da62-c300-4c06-30b186785f0f.png)
```
私：
Eloquent\Builderに対してorderByが実装されていないにも関わらず
エラーにはならずにQuery\BuilderのorderByメソッドが呼べているのはなぜでしょうか？

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

マジックメソッド！
ってなんだっけ。。
↓
[PHP: マジックメソッド - Manual](https://www.php.net/manual/ja/language.oop5.magic.php)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/3176694a-57f1-5f63-b6c3-58b35a0adfc2.png)
- 中でいろいろやっていますが、1971行目の `$this->forwardCallTo($this->query, $method, $parameters);` でQuery\Builderに対してマジックメソッドの引数（`$method`と`$parameters`）を渡している風なメソッドの呼び出しを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/9a423a80-c80b-d232-0478-1a7a14410d7f.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の中では以下のようなコードが実行されているということになります。
```diff_php
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/ba8f8dcf-c6d6-93f1-d21b-900bd63fe380.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/1e01947a-fbd3-0a77-682c-8eeb426763c8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/ded13264-a98a-2419-8fba-b99ef8213a64.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/ed1fbf15-1f48-45e5-2ecb-077800e9162b.png)

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