Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

こんにちはみなさん

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

参考

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

niisan-tokyo
流行りに微妙に遅れてついていく、エンジニア9年生です。
roxx
人材紹介業むけプラットフォーム「agent bank」、リファレンスチェックサービス「back check」を運営。
https://roxx.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away