PHP
tips
laravel

Laravel Tips #2 表示・非表示について

誰も「非表示機能を作ってください」って頼まれたことはあるんでしょう?
Laravelでのその機能について話をします。

一番簡単はやり方で考えると、こうなりますね:

// GET /products/1 
public function show(Product $product)
{
    abort_unless($product->isVisible(), 404);

    // コントローラー用のロジック
}

/products/1にリクエストがあるとき、Laravelのコンテーナーが1のIDを持つProductを探して、裏で Product::findOrFail(1) をやってくれますね。
商品を出すのはそこだけだったら別にいいんですけど、きっと商品があるシステムは商品が中心で、色んなところに出るんでしょう。
使うたびに表示のチェックを行うのがよくないですね。

それを解決するにはクエリースコープを作るのがいいですね。
でも、クエリースコープを作ると、コンテーナーからのバインディングを失うね。
それでも上のチェックよりは良く見えますね。findOrFailで見つからない場合は、ModelNotFoundExceptionを投げて、それで404になりますね。

// GET /products/1 
public function show($productId)
{
    $product = Product::visible()->findOrFail($productId);

    // コントローラー用のロジック
}

行数を減らして、Ifも消えたけど、それでも正しく思えないんですね…
どうしてもコンテーナーから解決して欲しいんですね!

グローバルスコープにしたら解決できそうですから、やってみましょう!

class Product extends Eloquent
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('visible', function ($query) {
            $query->visible();
        });
    }    
}

// GET /products/1 
public function show(Product $product)
{
    // コントローラー用のロジック
}

これですべてのProductには非表示だったらコンテーナーからの404になりますね!やった!
…と思うかもしれないけど、管理者が非表示の商品を取得したい場合はどうなるんでしょうか?

// GET /products/1 
public function show($productId)
{
    $product = Product::withoutGlobalScopes()->findOrFail($productId);

    // コントローラー用のロジック
}

前と同じような問題ですが、表示だったときの問題を、非表示のときに移動してしまっただけですね。

両方コンテーナーから解決してもらう方法

表示でも非表示でもコンテーナーから解決してほしいことになったなら、オブジェクトレベルで解決したほうがいいですね!
ここまで来たなら、明らかにProductっていうクラスが責任をたくさん溜まっていますね。2つの役を演技できないようですね。

class VisibleProduct extends Product
{
    //
}

// GET /products/1 
public function show(VisibleProduct $product)
{
    // コントローラー用のロジック
}

// GET /admin/products/1 
public function show(Product $product)
{
    // コントローラー用のロジック
}

VisibleかかっていることがProductには当たり前だと感じたら、逆も可能ですね。
恐らく管理者により、一般ユーザーにProductを表示するほうが珍しくないんでしょう。

// 名前は任せます
class UnscopedProduct extends Product
{
    //
}

// GET /products/1 
public function show(Product $product)
{
    // コントローラー用のロジック
}

// GET /admin/products/1 
public function show(UnscopedProduct $product)
{
    // コントローラー用のロジック
}

こうすると普通のユーザーに表示するときはデフォルトで守られているし、ガードを下げたい場合は綺麗にできますね。

ただ、一つあるのですが、Eloquentはクラス名から色んなプロパティーを仮定として出していますね。
例えば、UnscopedProductっていうクラスには unscoped_products っていうテーブルを期待しますね。
リレーションシップ用にも unscoped_product_id などを期待するので、是非それぞれのプロパティーはクラスを書いてください。

class UnscopedProduct extends Product
{
    protected $table = 'products';

    // getForeignKey()なども
}

結論

もちろん、マスターでしか編集しないキーワードなどの表示・非表示なんかにわざわざ別モデルを作成することなんて言ってないんですが、ドメインに関するモデルでしたら、是非やってみる価値あると思いますね。
間違いで気づかずに非表示の商品や、出版されるはずのない記事などを、ミスって公開してしまう可能性は減るんでしょう。コードがDRYになるのがボーナスですね。