DB からデータを取得する時、様々な条件のもとに取得してくる事が多いと思います。
その度にwhereをいくつも連結して実装するのも、読み解くのも大変ですよね。
何が大変かと言うと、結局その実装で取得されるデータが何を表しているのか分かりにくい、というのが原因の1つなのではないかと感じました。
そういった事を解決してくれるクエリスコープという機能の1つであるローカルスコープについて検証しました。
参考:Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans
ローカルスコープの検証
マイグレーションファイルの準備
以下のような users テーブルを用意します。
- is_active カラム:有効なユーザーかどうかを表すフラグ
role カラム(※ 例として以下のような分類とします)
admin:管理者prime:一般よりも権限を持つユーザーuser:一般ユーザー
マイグレーションの定義
<?php public function up() { Schema::create('users', function (Blueprint $table) { $table->string('id', 36)->primary(); $table->string('last_name'); $table->string('first_name'); $table->boolean('is_active'); $table->enum('role', ['admin', 'prime', 'user']); $table->timestamps(); }); }
テストデータの準備
今回は、Fakerを使い、10人分のテストデータを生成しました。
Fakerはランダムなテストデータを生成するのに便利なライブラリです。
以下のサンプルのように人の名前だけでなく、emailや住所なども生成してくれます。
参考:[PHP] Fakerでランダムなフェイクデータを作成する - Qiita
<?php $roles = ['prime', 'user']; $faker = Factory::create('ja_JP'); foreach (range(1, 10) as $i) { // 最初の1人だけ admin にし、それ以外はランダムで prime か user ロール。 $role = $i === 1 ? 'admin' : $roles[rand(0, 1)]; User::create([ 'id' => $faker->uuid, 'last_name' => $faker->lastName, 'first_name' => $faker->firstName, 'is_active' => $faker->boolean(), 'role' => $role, ]); }
生成したデータは以下の通りです。

検証1:シンプルな定義(有効なユーザーを取得する)
scopeをプレフィックスとした function をモデルに定義する必要があります。
ただし、この function を呼び出す時は、scopeを付けません。
モデル定義
ここでは、有効状態のユーザーを操作する頻度が多い事を想定し、
以下のように User モデルにscopeActiveという名前で function を追加します。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { 〜 略 〜 protected $casts = [ 'is_active' => 'boolean', ]; public function scopeActive(Builder $query): Builder { return $query->where('is_active', true); } }
実行
定義したスコープを使って件数を取得します(想定では8人)。
<?php logger(User::Active()->count());
出力結果
[2019-05-06 08:59:09] testing.INFO: Query Time:15.99ms] select count(*) as aggregate from `users` where `is_active` = ? [2019-05-06 08:59:09] testing.INFO: array ( 0 => true, ) [2019-05-06 08:59:09] testing.DEBUG: 8
発行されるクエリがis_activeを条件にしている事、カウントも適切な値が取得できた事を確認できました。
少しハマったポイント
※ ドキュメントにはUser::active()と、aが小文字で書かれており、これを実行すると以下のようなエラーが発生しました。
試しにAと大文字から書き始めたら正常に動くようになりました。
BadMethodCallException: Call to undefined method App\Models\User::acitve()
検証2:スコープに引数を渡せるようにする(動的スコープ)
モデル定義
roleがprimeもしくはuserのリストを取得できるようにしたい、といったケースに対応できるようにするため、以下のようにscopeOfTypeという function を追加します。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { 〜 略 〜 // $type には `admin`, `prime`, `user` のいずれかを指定します。 public function scopeOfType(Builder $query, string $type): Builder { return $query->where('role', $type); } }
実行
定義したスコープを使い、primeなユーザー数数を取得します(想定では3人)。
<?php logger(User::OfType('prime')->count());
出力結果
[2019-05-06 09:21:46] testing.INFO: Query Time:20.1ms] select count(*) as aggregate from `users` where `role` = ? [2019-05-06 09:21:46] testing.INFO: array ( 0 => 'prime', ) [2019-05-06 09:21:46] testing.DEBUG: 3
このように、roleカラムを対象にprimeで絞っており、結果が3である事も確認できました。
もちろん、User::OfTypeに'user'を渡せば、roleを対象にuserで絞る事ができます。
<?php logger(User::OfType('user')->count());
[2019-05-06 09:39:39] testing.INFO: Query Time:17.09ms] select count(*) as aggregate from `users` where `role` = ? [2019-05-06 09:39:39] testing.INFO: array ( 0 => 'user', ) [2019-05-06 09:39:39] testing.DEBUG: 6
検証3:作成したスコープの合わせ技
実行
作成した2つの function を合わせて使用し、アクティブでroleがuserのユーザー数を取得します。(想定は4人)
合わせる時は単純に繋げて使用する事ができます。
<?php logger(User::Active()->OfType('user')->count());
出力結果
[2019-05-06 09:43:51] testing.INFO: Query Time:17.5ms] select count(*) as aggregate from `users` where `is_active` = ? and `role` = ? [2019-05-06 09:43:51] testing.INFO: array ( 0 => true, 1 => 'user', ) [2019-05-06 09:43:51] testing.DEBUG: 4
is_activeおよびroleカラムを対象に絞り、結果が4である事を確認できました。
感想
スコープの名称(function 名)を適切なもので定義する事で、その実装がどういうデータを取得してこようとしているか、判断しやすくなるなと感じました。 むしろ判断しやすくなるようなスコープを定義する必要があるかもしれません。
また、例えばあるカラムの名前を変更する事になった場合、そのカラムを利用するクエリ(例:where('カラム名', $value))を、スコープの中に閉じ込めておくことで、ソースコードの修正も容易になるのかなと思いました。
スコープを積極的に使用して、メンテナンス性の高いモデルを実装していきたいなと思いました。