📄

[Laravel] 記事の公開・下書き機能を実装するためのベストプラクティス

管理者と一般ユーザのアクセス制限とか、特定のページに対するアクセス制限とかは見つかるのだけど、特定のレコードへのアクセスを制限する方法が見つからなかったのでメモ。

実装したいこと

次のような Post テーブルがあったとする。

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string("title");
    $table->text("body");
    $table->tinyInteger("status");
    $table->timestamps();
});

statusPUBLISHEDDRAFT のどちらかとする。

class Post extends Model
{
    const PUBLISHED = 1;
    const DRAFT = 2;
}

コントローラはこんな感じ。

class PostsController extends Controller
{
    public function index()
    {
        $posts = Post::all();
        return view('post.index', compact('posts'));
    }

    public function show(Post $post)
    {
        return view('post.show', compact('post'));
    }
}

このとき、statusDRAFT のレコードを一覧に表示させない、かつ ID 指定でアクセスできないようにするにはどうすればいいか?

ベストプラクティス

グローバルスコープを使う

具体的には、Post モデルに以下のようなグローバルスコープを追加する。 これにより、コントローラに定義したすべてのアクションにおいて、DRAFT のレコードが無視される。

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('published', function ($query) {
        $query->where('status', self::PUBLISHED);
    });
}

おまけ:たどり着くまでに試したこと

1. where() をコントローラに書く

まずは、コントローラに直接 where() を書いた。

 public function index()
 {
-    $posts = Post::all();
+    $posts = Post::where('status', Post::PUBLISHED)->get();

     return view('post.index', compact('posts'))
 }

 publich function show(Post $post)
 {
+    abort_unless($post->status === Post::PUBLISHED, 404);

     return view('post.show', compact('post'))
 }

これでも正しく動作するけれど、コントローラ内のすべてのメソッドに処理を書く必要があるので、保守性しにくい。

2. ローカルスコープを使う

保守性を高めるために、以下のようなローカルスコープを追加した。

public function scopePublished($query)
{
    $query->where('status', self::PUBLISHED);
}

それに伴って、コントローラも書き換える

 public function index()
 {
-    $posts = Post::where('status', Post::PUBLISHED)->get();
+    $posts = Post::published()->get();

     return view('post.index', compact('posts'))
 }

 publich function show(Post $post)
 {
-    abort_unless($post->status === Post::PUBLISHED, 404);
+    abort_unless(Post::published()->where($post->id)->exists(), 404);

     return view('post.show', compact('post'))
 }

show() がかなり煩雑になってしまったものの、定数を参照する処理をまとめることができた。 でも、まだすべてのメソッドに処理を追加する必要がある。

3. グローバルスコープを使う

すべてのメソッドに追加するならグローバルスコープでいいじゃん、ということで、Post モデルのローカルスコープを削除して、以下のようにグローバルスコープを追加した。

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('published', function ($query) {
        $query->where('status', self::PUBLISHED);
    });
}

コントローラは元に戻して大丈夫。

public function index()
{
    $posts = all();
    return view('post.index', compact('posts'))
}

publich function show(Post $post)
{
    return view('post.show', compact('post'))
}

グローバルスコープを使うことによって、コントローラを変更していないにもかかわらず、post.index に下書きの記事は表示されなくなり、post.show で ID を指定しても 404 になるようになったemoji-smile