afterQuery() を利用できます。SQLの条件を追加するだけでなく、取得結果に対して実行するClosureもクエリビルダーへ登録できます。
ただし、「SQLが1回実行されるたびに、同じ型の結果が渡されるフック」ではありません。取得メソッドによって、渡されるCollectionの中身や呼び出し回数が変わります。
このページはLaravel 13を対象にしています。公式ドキュメントのAfter Query Callbacksを出発点に、Eloquentのビルダーと公式テストから利用時の境界を確認します。
ローカルスコープに結果加工をまとめる
モデルをIDで参照する処理を繰り返す場合は、結果のキーを揃えるスコープを用意できます。次の例はapp/Models/Post.php に定義します。
$posts は引き続きEloquent Collectionで、要素も Post のままです。get(42) はキーが 42 のモデルを返し、存在しなければ null を返します。select() で取得列を限定する場合は、キーに使う id も含めてください。
1か所だけで加工するなら、get()->keyBy('id') の方が単純です。afterQuery() は、スコープや再利用するクエリに取得後の処理も含めたいときに選びます。スコープ自体の定義方法はEloquentのスコープを参照してください。
getの実行順を読む
Eloquent\Builder::get() は、グローバルスコープの適用、モデルの生成、指定されたリレーションのEagerロードを行い、最後に afterQuery のコールバックを実行します。
通常の get() では、コールバックからEagerロード済みのリレーションを利用できます。指定していないリレーションへのアクセスは、追加のSQLを発生させる可能性があるため、Eagerロードと組み合わせて設計します。
retrieved モデルイベントとはタイミングが異なります。retrieved は Model::newFromBuilder() で各モデルを生成するときに発火し、get() のEagerロードより前です。取得したモデルのまとまりを扱う afterQuery() と、各モデルのイベントは同じ役割ではありません。
返り値は次のコールバックへ渡される
afterQuery() は登録順にClosureを追加します。実行部分の applyAfterQueryCallbacks() は、次の実装になっています。
?? ではなく、PHPのtruthy判定を使う ?: です。
Collectionを直接変更し、何も返さない
void のコールバックも利用できます。その場合は、変更した同じオブジェクトを維持します。一方、新しいCollectionを作った場合は、そのCollectionを返す必要があります。
内部の引数と返り値は mixed ですが、自由な型への変換を公開APIにするのは避けます。例えば get() の結果を文字列や配列に置き換えると、first() が続けて呼ぶ ->first() などの処理を壊します。スコープでは、モデル・Collectionの型・件数・並び順を保つ加工を基本にしてください。
取得メソッドごとの違い
次の表は、Eloquentのビルダーに登録したafterQuery() の扱いです。
first() は内部で limit(1)->get()->first() を呼びます。first() を使うからといって、Closureの引数を Post に変更してはいけません。
値だけが必要ならスコープを使わず Post::query()->pluck('id') とします。モデルの加工後に値を取り出すなら Post::keyedById()->get()->pluck('id') と書けます。後者の pluck() は、クエリビルダーではなく取得済みのCollectionに対する操作です。
ページ件数や次の取得位置を変えない
paginate() の総件数はSQLで集計し、その後にページのモデルを取得します。取得後にCollectionをフィルタしても、総件数は再計算されません。
また、simplePaginate() と cursorPaginate() は加工後の結果を使って次ページを判断します。chunkById() はチャンク末尾のIDを次の取得位置に使います。コールバック内で件数や順序を変えると、ページ判定や継続位置が本来のクエリと一致しなくなることがあります。
絞り込み・ソート・取得列はSQL側で指定し、afterQuery() では取得結果の意味を変えない加工に留めます。APIの表示形式を変えるだけなら、APIリソースでの変換も検討してください。
公式ドキュメントには、モデルへ検索結果の抜粋属性を追加する例があります。モデル属性への代入は保存対象の変更にもなり得ます。DBに存在しない表示用属性を付けたモデルを、そのまま save() する設計は避けてください。
登録はクエリビルダーに残る
afterQuery() を呼ぶたびにClosureが追加され、実行しても登録は消えません。同じビルダーで再び取得すれば再実行され、同じスコープを二重に呼べばコールバックも二重に登録されます。ビルダーをcloneした場合も、登録済みのコールバックを引き継ぎます。
取得を複数回行う処理や長寿命ワーカーでは、用途ごとに新しいクエリを作り、コールバックは副作用のない結果加工にします。メール送信や永続化を置くと、チャンクやカーソルによる複数回の実行を、業務上の1回の処理と取り違えやすくなります。
パッケージで再利用するスコープを提供する場合も、対応する取得メソッドを公開APIとして明示し、型や呼び出し回数の契約をテストしてください。
取得経路の違いをテストする
利用側のLaravel 13アプリケーションでは、キーの加工だけでなく、first()・cursor()・空のページでの呼び出しを確認します。
次の例は、先ほどの Post モデルと、id・title・body・created_at・updated_at を持つ posts テーブルのマイグレーションがあることを前提にしています。RefreshDatabaseで、テスト間のレコードを分離します。
tests/Feature/AfterQueryTest.php にテストを作成します。
保守時のチェックリスト
- スコープが対応する取得メソッドと、Closureの引数・返り値の型を明示する。
get()の空の結果と、総件数0のpaginate()を区別して検証する。- チャンクやカーソルでは、呼び出し回数と取得順序が変わらないことを確認する。
- コールバックの二重登録やビルダーの再利用で、業務上の副作用が発生しない設計にする。
- Laravelの更新時には、
get()・pluck()・cursor()とBuildsQueriesの呼び出し経路を再確認する。
関連ページ
Eloquentのスコープ
SQLの制約をローカルスコープとグローバルスコープにまとめる基本を確認します。
Eloquentコレクション
モデルを扱うCollectionとSupport Collectionの違いを確認します。
Eloquent Observersとモデルイベント
取得結果の加工とは異なる、モデル単位のイベントを確認します。
APIリソース
APIへ返す表示データの変換を、モデルの保存処理から分離します。