[Laravel] 記事の公開・下書き機能を実装するためのベストプラクティス
管理者と一般ユーザのアクセス制限とか、特定のページに対するアクセス制限とかは見つかるのだけど、特定のレコードへのアクセスを制限する方法が見つからなかったのでメモ。
実装したいこと
次のような Post テーブルがあったとする。
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string("title");
$table->text("body");
$table->tinyInteger("status");
$table->timestamps();
});
status
は PUBLISHED
か DRAFT
のどちらかとする。
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'));
}
}
このとき、status
が DRAFT
のレコードを一覧に表示させない、かつ 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 になるようになった