👩‍🎓

【PHPDoc】Laravelで複数テーブルをJOINして取得した複雑な値に対して型情報を補足する

2023/08/31に公開

はじめに

こんにちは。kouです。

Laravelで、複数テーブルをJOINしてから複数テーブル間の任意のカラムを指定して値をSELECTした際に、「取得したコレクションの各要素が、SELECTで指定したカラムをプロパティとして持っている」ということをPHPDocで表現したかったのですが、この書き方が中々分からず少し手間取ったので備忘録として残しておくことにしました。

文章だけでは理解しづらいかと思うので、簡単にコードを載せて課題感を解説すると、要は以下のようなことです。

// todos タスク
// users ユーザー
// roles ロール
// 以上3つのテーブルがあると仮定します。
$query = DB::table('todos');

// 見やすくするためにテーブルごとにselectを分けています
$query->selectRaw('todos.id AS todo_id, todos.title, todos.status');
$query->selectRaw('users.id AS user_id, users.name AS user_name');
$query->selectRaw('roles.name AS role');

$query->join(/** users, roles をJOIN */);

// クエリ実行
$res = $query->get();

// 何かしらのDTO等への詰め替え処理
return $res->map(function (stdClass $item) {
    // 🤔 ここで「$item->*」に対して型の補完が出ると嬉しい
    return new SampleDto(
        todoId: $item->todo_id,
        title: $item->title,
        status: $item->status,
        userId: $item->user_id,
        userName: $item->user_name,
        userRole: $item->role,
    );
});

要するに、上記 $item に対していい感じに型補完が出るようにしたいということだったのですが、Laravelのクエリビルダなどへの理解不足から思いの外手間取ってしまいました。

結論

以下が、$item に対して型補完が効くようになったPHPDocの型注釈付きコードです。

$query = DB::table('todos');

$query->selectRaw('todos.id AS todo_id, todos.title, todos.status');
$query->selectRaw('users.id AS user_id, users.name AS user_name');
$query->selectRaw('roles.name AS role');

$query->join('users', 'todos.user_id', '=', 'users.id');
$query->join('roles', 'users.role_id', '=', 'roles.id');

$res = $query->get();

return $res->map(
    /**
     * 🐶 「$item->*」に対して型の補完が出るようになる
     *
     * @param object{
     *     todo_id: int,
     *     title: string,
     *     status: string,
     *     user_id: int,
     *     user_name: string,
     *     role: string,
     * } $item
     */
    function (object $item) {
        return new SampleDto(
            todoId: $item->todo_id,
            title: $item->title,
            status: $item->status,
            userId: $item->user_id,
            userName: $item->user_name,
            userRole: $item->role,
        );
    }
);

解説

mapメソッドのコールバック関数の引数($item)に対して、型を object で指定した上でPHPDocによる型注釈を施しました。
これで、「$item->」まで入力した時点で todo_iduser_name などの補完が表示されるようになります。

分かってしまえば非常にシンプルなことでした。

当初、Laravelのクエリビルダから返される値が stdClass のコレクションであったことから、$item の型として stdClass を指定してみたのですが、stdClass にはPHPDocで詳細な型付けができず、object型を指定すればいいんだということに気付くまでだいぶ時間がかかりました。

object型が指定できると分かれば、あとはドキュメントに従ってオブジェクトが持つプロパティの値をPHPDocに書いて目的達成です。


ちなみに別件ですが、本文中のコード上で、$query->* でメソッドを呼び出すだけ呼び出していますが、返り値などを特に使用していないのになぜこれで正しくクエリ条件が適用されていくのかについて疑問を抱いた方がいるかもしれません。
こちらの挙動については、弊社のまた別のメンバーが書いた以下の記事が詳しいです。Builder単位での切り出しによる再利用性の向上や可読性向上など、Laravelを学ぶ者にとってとても学びになる記事なので、是非気になった方は一読されてみることをおすすめします😌
https://zenn.dev/manalink_dev/articles/cde8e28206d235

Eloquent Builder版

これまでの解説では、Laravelのクエリビルダを前提として解説してきましたが、Query Builderではなく、Eloquent Builderを用いて同様のことを行ないたい場合は、以下のようにすると良さそうです。

// Illuminate\Database\Eloquent\Builder
$query = Todo::query();

/** ... SELECT作業や、JOIN作業、WHERE作業などなど ... */

// toBase() で Illuminate\Database\Query\Builder に戻す(※必須ではない)
$res = $query->toBase()->get();

return $res->map(
    /**
     * @param object{ todo_id: int, user_id: int } $item
     */
    function (object $item) {
        return new SampleDto(
            todoId: $item->todo_id,
            userId: $item->user_id,
        );
    }
);

$query->toBase()の部分で、Eloquent BuilderからQuery Builderへ戻しています。
勿論、Eloquent Builderのままで実行($query->get();)しても問題はないのですが、Eloquent Modelへの変換コストなどを考えると、複数テーブルに跨って値を取得したい今回のような状況ではあまりメリットがないように思えるため、個人的には toBase() した上でQuery Builderとして実行するのが良さそうに思いました。

上で述べているような複数テーブルに跨った値を取得するのではなく、JOINで複数テーブルを連結させた上でWHERE等で絞り込みをかけ、最終的に単一モデルの情報に戻して返したいような場合(ex. SELECT todos.* FROM ... にして返したいようなケース)であれば、Eloquent Modelとして返せると嬉しい場面が多いかと思うので、そういった場面では Eloquent Builderのまま使用するのが良さそうです。
またその場合は、取得したコレクションの要素に対してこの記事で挙げたようなPHPDocを用いた型注釈は恐らく不要になると思うため(Todoモデルのクラスを直接型として指定できるため)、今回のこの記事のスコープからは少し外れてしまいます。

おわりに

今回は、Laravelで、複数テーブルをJOINして複数テーブル間に跨る値をSELECTした際に、「取得したコレクションの各要素が、SELECTで指定したカラムをプロパティとして持っている」ということをPHPDocで表現する方法について解説しました。

但し、あくまでもPHPDocはソースコード上に独自に定義している型注釈に過ぎないため、今後SELECT句の改修をした際は、合わせてDocコメントの修正も行なわないと、どんどん実際に取得した実態とPHPDocによる型注釈間で乖離が生まれていってしまうため注意が必要です。

GitHubで編集を提案
マナリンク Tech Blog

Discussion