以下のようなページングを想定した一覧情報を返すAPIコントローラ処理を書いていた時のこと
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);
}
一覧について「作成日の降順で取得」という仕様が抜けてたので以下のように修正しました
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 | |
---|---|---|
改修前 |
LengthAwarePaginator |
補完が効く |
改修後 |
mixed |
補完が効かない |
※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のインスタンスはコンストラクタで設定している
- Eloquent\Builderの中で
※ちなみにですが、Eloquent\Builder内においてEloquent\BuilderとQuery\Builderはクラス名が被るため、Query\Builderの方はasでQueryBuilderという別名をつけていました。
その他、VSCodeでorderBy
の補完がされないことも確認
orderByはどこに実装されているか
- Eloquent\Builder内を「 orderBy(」で検索してみましたが何も見つかりませんでした。
- Eloquent\Builderの中でuse宣言によって参照されている3つのトレイト(BuildsQueries、ForwardsCalls、QueriesRelationships)の中にも見当たりません。。
- Query\Builderの中に
orderBy
の実装を見つけました!
こちら見つけたはいいですが開発中のコントローラにおける$query
の実体はEloquent\Builderです。
Query\Builderに実装されていることが確認できたとしても、これってコントローラ側のメソッドチェーンから呼び出せるものではなくない??という疑問が生まれました。
ここで煮詰まってしまいChatGPTに質問してみました。
私:
Eloquent\Builderに対してorderByが実装されていないにも関わらず
エラーにはならずにQuery\BuilderのorderByメソッドが呼べているのはなぜでしょうか?
ChatGPT:
EloquentのIlluminate\Database\Eloquent\Builderには
orderBy()メソッドが直接実装されていないにも関わらず、
orderBy()メソッドが使える理由は、Eloquentがマジックメソッドを使用しているためです。
マジックメソッド!
ってなんだっけ。。
↓
PHP: マジックメソッド - Manual
Eloquent\Builderの__callマジックメソッドについて
Eloquent\Builderの__callマジックメソッドを確認してみます。
- 中でいろいろやっていますが、1971行目の
$this->forwardCallTo($this->query, $method, $parameters);
でQuery\Builderに対してマジックメソッドの引数($method
と$parameters
)を渡している風なメソッドの呼び出しを確認できます。
- 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
というメソッドを見つけました。
- コメントや実装などから今回の「作成日の降順」を取得するという目的に合致したメソッドであることがわかります(パラメータとして他のカラム名を指定することもできる)。
- こちらのメソッドは Eloquent\Builder内で$thisを返しておりメソッドチェーンが切れることもありません。
- つまりは今回の「作成日の降順で取得」という改修におけるベストプラクティスになるのではないかなと思います。
以下のような感じで補完が切れないこと、$articles
がmixedとならないことを確認できました。
なお、こちらのlatest
メソッドについてEloquent\Builderの$this->query->latest
によってQuery\Builderの方に実装されている同latest
メソッドを呼び出しており、そちらを見るとQuery\Builder内に実装されたorderBy
メソッドを呼び出していました。
感想
結構長いこと補完が効いたり効かなかったりする問題について疑問に思っていましたが、今回の調査によって概ね解決できてとてもすっきりしました。
なるべく補完の効くコードを書くことを心掛けたいなと思います。