LoginSignup
68
61

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-24

こんにちはみなさん

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

参考

海外では紹介されていることがあったりした

68
61
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
61