こんにちはみなさん
Laravelを半年以上使い続けてきたのですが、不気味なほどよくできているというか、「こんなんできないかなぁ」とか思うと、大体Laravelで完結できちゃったりします。
一方で、マニュアルを漁っても出てこない機能とかあって、結局ソース読んだりLaracastを見に行ったりするわけです。
そんなわけで今回は、いろんな使いみちがありそうなのに、マニュアルに見当たらなかった、Eloquentに関する小技を紹介します。
(SoftDelete眺めてたら見つけました)
Eloquentのboot
EloquentはLaravelのORMですが、初回呼び出し時にboot
という静的メソッドを呼び出して、初期設定をしているようです。
このbootの使い方としては、マニュアルサイトではグローバルスコープを設定したり、イベントを設定したりしています。
例えばこんなコードを考えてみます。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class Item extends Model
{
public static function boot()
{
parent::boot();
self::addGlobalScope('mine', function (Builder $b) {
$b->where('user_id', \Auth::user()->id);
});
self::creating(function ($q) {
$user_id = \Auth::user()->id;
$q->user_id = $user_id;
});
}
}
認証成功していないと確実に失敗するコードですが、とりあえずおいておきます。
まず、addGlobalScope
を使うことで、どんなときでもitems
テーブルにSQLを流すときには、ログインユーザーのIDをwhere句に入れるようになります。
一方、creating
イベントを登録することで、データ作成時にその時ログインしているユーザーのIDを暗黙に登録するようになっています。
いちいちルーティング作るのも面倒なので、テスト作りました。
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\{User, Item};
/**
* @group item
*/
class ItemTest extends TestCase
{
use DatabaseTransactions;
public function testExample()
{
$user = factory(User::class)->create();// ユーザー
$other = factory(User::class)->create();// 別アカウントのユーザー
// 対象ユーザーでログインしてモデルを使う
$this->actingAs($user);
$item = new Item;
$item->content = '適当';
$item->save();
// 別アカウントユーザーでログインしてモデルを使う
$this->actingAs($other);
$item = new Item;
$item->content = 'これは違う';
$item->save();
// 対象ユーザーでログインしてItemを取得してみる
$this->actingAs($user);
$test = Item::orderBy('id', 'desc')->first();
$this->assertEquals('適当', $test->content);
}
}
で、ユニットテスト走らせてみましょう。
ログを眺めるとこんな感じのが出てきます。
SQL: 「insert into `items` (`content`, `user_id`, `updated_at`, `created_at`) values (?, ?, ?, ?) 」, data:[適当, 5, 2017-03-23 23:36:34, 2017-03-23 23:36:34]
SQL: 「insert into `items` (`content`, `user_id`, `updated_at`, `created_at`) values (?, ?, ?, ?) 」, data:[これは違う, 6, 2017-03-23 23:36:34, 2017-03-23 23:36:34]
SQL: 「select * from `items` where `user_id` = ? order by `id` desc limit 1 」, data:[5]
insert
のときに指定されていないuser_id
がパラメータに加えられていて、select
のときにもwhere句にuser_id
が加えられているのがわかります。
traitのboot***
今回のようにItem
モデルだけであればboot
に直書きする方法でもいいのですが、これがItem2
,Item3
...となると、面倒くさくなるし、コピペコードが量産されて心の健康によろしくありません。
こんな時のためにtrait
を使ってboot
を別出しする機能があるようです。
とりあえずこんなtraitを作ります
<?php
namespace App\Behavior;
use Illuminate\Database\Eloquent\Builder;
trait UserMust
{
public static function bootUserMust()
{
static::addGlobalScope('mine', function (Builder $b) {
$b->where('user_id', \Auth::user()->id);
});
static::creating(function ($q) {
$user_id = \Auth::user()->id;
$q->user_id = $user_id;
});
}
}
このboot + {trait名}
の静的メソッドを定義すると、これをuseしたEloquentはboot時にこの静的メソッドを同時に呼ぶようになります。
次にItmeクラスを書き換えます。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Behavior\UserMust;
class Item extends Model
{
use UserMust;
}
随分スッキリしました。
Itemクラスではtraitをuseしているだけです。
で、テストを走らせると、
SQL: 「insert into `items` (`content`, `user_id`, `updated_at`, `created_at`) values (?, ?, ?, ?) 」, data:[適当, 11, 2017-03-23 23:50:50, 2017-03-23 23:50:50]
SQL: 「insert into `items` (`content`, `user_id`, `updated_at`, `created_at`) values (?, ?, ?, ?) 」, data:[これは違う, 12, 2017-03-23 23:50:50, 2017-03-23 23:50:50]
SQL: 「select * from `items` where `user_id` = ? order by `id` desc limit 1 」, data:[11]
ちゃんと機能していますね。
insert, selectにuser_idが追加されているのがわかります。
もし、これと同じ挙動をさせたいEloquentが他にもある場合は、各クラス定義でこのtraitをuseするだけでオッケーです。
まとめ
SaaSや地域ごとのサービスとかを運用すると、複数のテーブルで共通の縛りを設ける場合があるのですが、今回のようにうまくtraitを使うことで、Eloquentをすっきりさせることができます。
今回はこんなところです。