Laravelのマニュアルにない?小技: Eloquentのboot時にtraitのbootを別に走らせる

こんにちはみなさん

Laravelを半年以上使い続けてきたのですが、不気味なほどよくできているというか、「こんなんできないかなぁ」とか思うと、大体Laravelで完結できちゃったりします。
一方で、マニュアルを漁っても出てこない機能とかあって、結局ソース読んだりLaracastを見に行ったりするわけです。

そんなわけで今回は、いろんな使いみちがありそうなのに、マニュアルに見当たらなかった、Eloquentに関する小技を紹介します。
(SoftDelete眺めてたら見つけました)

# Eloquentのboot

EloquentはLaravelのORMですが、初回呼び出し時に`boot`という静的メソッドを呼び出して、初期設定をしているようです。
このbootの使い方としては、マニュアルサイトではグローバルスコープを設定したり、イベントを設定したりしています。
例えばこんなコードを考えてみます。

```Item.php
<?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を暗黙に登録するようになっています。
いちいちルーティング作るのも面倒なので、テスト作りました。

```ItemTest.php
<?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を作ります

```Behavior/UserMust.php
<?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クラスを書き換えます。

```Item.php
<?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をすっきりさせることができます。
今回はこんなところです。

# 参考
[海外では紹介されていることがあったりした](http://www.archybold.com/blog/post/booting-eloquent-model-traits)