【Laravel】スコープを使えば「うっかり」が減って「ラク」できる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、この間リリースされたLaravelの新バージョン8.xについては私の中で一旦落ち着きましたので、今回は新機能ではなく、元々ある便利な機能に的を絞ってご紹介していきたいと思います。

ズバリ、その紹介したい機能は・・・・・・

スコープ

です。

ここで言うスコープというのは、JavaScriptのスコープとは少し違っていて、主にDBテーブルの絞り込みをするwhere句に関連する機能のことです。

そして、一言でスコープといってもLaravelには以下2つのスコープがあって、どちらを使うべきかは状況によって変わってきます。

  • グローバル・スコープ
  • ローカル・スコープ

そこで❗

今回は、この2つのスコープをご紹介していきたいと思います。
ぜひ皆さんのお役に立てましたら嬉しいです😊✨

「5%だと思ったら、しっかり
ストロングの9%でヘロヘロです😂」

開発環境: Laravel 8.x(スコープは過去バージョンでも使えます)

「うっかり」を減らせるグローバル・スコープ

グローバル・スコープとは

グローバル・スコープは、簡単に言うと、「いつでも絞り込みが有効になる」機能です。

実際の例を見てみましょう。

いま、ユーザーが投稿した記事を保存するpostsというテーブルがあるとします。

もちろん誰でも投稿できるので、いろんなuser_idがデータとして保存されています。

・・・ということは、コードを書くときにひとつ気をつけないといけないことが出てきます。

そうです。「ログインしているユーザーのIDで絞り込みをしないと、他人の投稿まで変更や削除ができてしまう」という点です。

そのため、通常だと以下のようにwhere()を使って絞り込みをすることになると思います。

$user_id = auth()->id();
$posts = Post::where('user_id', $user_id)->get(); // 👈 本人のデータだけ取得

もちろんこの形でも間違いではありませんが、これに似たコードを何度も何度も書いていると、「うっかり」が発生し、where( ... )が抜けてしまうことも考えられます。

そんなとき「グローバル・スコープ」の登場です❗

つまり、グローバル・スコープを設定しておくと、書いてなくても必ず user_id の絞り込みを有効にすることができます。

$posts = Post::get(); // 👈 グローバル・スコープがあれば、毎回 user_id で絞り込みが有効になります

これは、get()だけでなく、first()などにも有効になります。

グローバル・スコープを設定する方法

では、実際にグローバル・スコープを設定する方法をご紹介しましょう。

app/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('user_id', function (Builder $builder) {

            $user_id = auth()->id();
            $builder->where('user_id', $user_id);

        });
    }
}

※ 設定はモデルごとに行います。皆さんはお好きなモデルでやってみてください。

まず、addGlobalScopeのすぐ横にあるuser_idは名前で、これは好きな文字列でOKです。(ただ、グローバル・スコープを解除するときにこの名前を使うので覚えやすいものがいいでしょう😊)

そして、function() { ... }内がグローバル・スコープとしていつでも実行されるwhere句になります。

では、設定前と後で、以下のコードがどう違ってくるか見てみましょう。

$posts = Post::get();
dd($posts->toArray());

(設定する前:全ての user_id がデータとして取得されます)

(設定した後:該当するuser_idだけが取得されます)

こんなふうにして「うっかり」を減らすことができるわけですね。

グローバルスコープを解除する方法

とはいえ、状況によっては、グローバル・スコープを解除したい場面も出てくると思います。

※ 例えば、管理者がpostsの全データを検索して変更や削除をする場合です。

そんな場合は次のようにすることで簡単にグローバル・スコープを解除することができます。

$posts = Post::withoutGlobalScope('user_id')->get();

user_idというのは先ほど設定した名前になります。

同じグローバル・スコープを何度も設定するのが面倒な場合

実はグローバル・スコープはもうひとつ設定方法があって、それが独自のクラスをつくる方法です。

この方法では、何度も同じグローバル・スコープをモデルに書かなくてもよくなり、省コード化できます。

では、実際に使い方を見ていきましょう。
以下のファイルを作成してください。

app/Scopes/UserIdScope.php

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class UserIdScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $user_id = auth()->id();
        $builder->where('user_id', $user_id);
    }
}

※残念ながら、artisanコマンドはありません😭

そして、このクラスをモデルに設定します。

<?php

namespace App\Models;

use App\Scopes\UserIdScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new UserIdScope);
    }
}

どうでしょう。さっきと比べると、たった1行でグローバル・スコープを有効にすることができるようになりました。

そして、Postだけでなく、user_idで絞り込みをしたいモデルにも使い回しができるのでとても便利です👍

なお、解除する場合は次のようになります。

Post::withoutGlobalScope(UserIdScope::class)->get();

ちなみに:where句以外も使えます

わかりやすくするためにwhere句のみで説明しましたが、orderBy()なども使えます。

protected static function booted()
{
    static::addGlobalScope('user_id', function (Builder $builder) {

        $builder->orderBy('user_id', 'desc');

    });
}

「ラク」できるローカル・スコープ

ローカル・スコープとは

ローカル・スコープ」はグローバル・スコープと違って、いつでも自動で有効になるものではありませんが、「必要なときに臨機応変で」ラクができる機能です。

こちらも実際の例を見てみましょう。

例えば、usersテーブルの中から以下の条件に合うデータを取得する場合です。

  • 指定した月(例えば今月、先月など)にユーザー登録した
  • メール認証を済ませている(email_verified_atnullではないもの)

こういった場合、通常は以下のようにしてコードを書くと思います。

$year = 2020;
$month = 9;
$start_dt = Carbon::createFromDate($year, $month)->firstOfMonth();
$end_dt = $start_dt->copy()->endOfMonth();

$users = User::whereBetween('created_at', [$start_dt, $end_dt])
    ->whereNotNull('email_verified_at')
    ->get();

これももちろん間違いではありません。
ただ、いろんな場所で何度もこのコードが必要になるとしたらどうでしょう。

「うーん、毎回このコード書くのめんどくさい・・・😭」

とはならないでしょうか。

そんなときにローカル・スコープの登場です❗

簡単にいうと、以下のように独自のwhere句を作ってたった1行だけでOKになります。

$users = User::whereCreated($year, $month)->get(); // 👈 さっきと同じ条件にできる

ローカル・スコープを設定する方法

では、実際にローカル・スコープをつくってみましょう。
グローバル・スコープと同じく各モデル内に設定します。

app/Models/User.php

<?php

namespace App\Models;

use Carbon\Carbon; // 👈 追加しました

// 省略

class User extends Authenticatable
{
    // 省略

    // スコープ
    public function scopeWhereCreated($query, $year, $month) {

        $start_dt = Carbon::createFromDate($year, $month)->firstOfMonth();
        $end_dt = $start_dt->copy()->endOfMonth();

        $query->whereBetween('created_at', [$start_dt, $end_dt])
            ->whereNotNull('email_verified_at');

    }

上のコードを見ていただくとわかるとおり、「scope」+ 「xxxxx」というメソッドをつくると、「xxxxx()」がローカル・スコープになります。

つまり、今回で言うとwhereCreated()ですね。

なお、変数が不要な場合は、$queryだけ残しておけばOKです。
例えば、今月登録したユーザーだけを取得するスコープはこちらです。

public function scopeWhereCreatedThisMonth($query) {

    $start_dt = today()->firstOfMonth();
    $end_dt = $start_dt->copy()->endOfMonth();

    $query->whereBetween('created_at', [$start_dt, $end_dt])
        ->whereNotNull('email_verified_at');

}

ちなみに:where句以外も使えます

グローバル・スコープと同じですが以下のようにすると、orderBy()など別のメソッドで条件指定することもできます。

public function scopeOrderByCreated($query, $direction = 'asc') {

    $query->orderBy('created_at', $direction);

}

使い方はこうなります。

$users = User::orderByCreated('desc')->get();
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravelの便利な機能「スコープ」についてご紹介しました。

スコープを使うことで、何度も同じコードを書かなくてよくなりますし、さらに、もし変更があったとしてもスコープの中身だけ書き換えるだけで全ての場所に適用できます

ということで、省コードになりますので、

  • 開発速度が早くなる
  • コードが少ない = バグも少ない

となって、我々にとってはメリットが大きいといっていいんじゃないでしょうか。(もちろん使いどころの判断が重要ですけどね👍)

ぜひみなさんもスコープを活用してコードを書いてみてくださいね。

ではでは〜❗

「リアルでもグローバルな視野
がほしいです😂」

このエントリーをはてなブックマークに追加       follow us in feedly