Framework: [Proposal] Default scope for Eloquent models

Created on 16 Mar 2014  Â·  9Comments  Â·  Source: laravel/framework

I wrote a question about this at Stack Overflow, and apparently it is not currently supported. So here's a proposal.

There should be a way to define a default scope to use per model. Similar to soft deleting, this scope would always take effect unless specifically asked otherwise.

This feature would be useful in many occasions, such as by default hiding muted/banned users, or some private items.

Could be implemented in a model like so:

public function defaultScope($query) {
    return $query->where('muted', '=', '0');
}

Most helpful comment

Would like to see simpler way to do this as well.
IDEAL SITUATION:

//in Posts
public function setGlobal($query){
$query->where('is_published',1)
}

//elsewhere
Posts::all() //with global scope
$user->posts() //has global scope by default (so we don't have to think about it when we write relationships) 
Posts::globalOverride() //overrides global and sets as plain

No idea if this is possible but I'll look at eloquent and see..

unless a rails dev has another ideas for syntax? @simonweil

All 9 comments

This is possible in 4.2. Check out the source on how soft delete works for more info.

I hope there are plans to document this. This is what I've figured out so far:

You can add a global/default scope to a model via the Eloquent\Model::addGlobalScope method. The best place to do this appears to be inside the boot method which only gets called once when the first instance is constructed. The parent method _does_ do stuff though, so we have to make sure to call it. (@taylorotwell Perhaps there should be an empty stub for this method so that developers don't accidentally override it?)

Here's an example:

class Vehicle extends Eloquent {
    protected static function boot() {
        static::addGlobalScope(new BelongsToCompanyScope);
        parent::boot();
    }

    public function company() {
        return $this->belongsTo('Company');
    }
}

There's also a static $globalScopes property, but I'm not sure that's usable. My scope class looks like this:

class BelongsToCompanyScope implements ScopeInterface {
    public function apply(Builder $builder) {
        $model = $builder->getModel();
        $builder->where($model->getTable().'.company_id','=',Auth::user()->company_id);
    }

    public function remove(Builder $builder) {
        // TODO: Implement remove() method.
    }
}

The remove method is a bit trickier to implement. I think we need an easier way of doing this; perhaps it could be done automatically -- each where criterion is a Builder object isn't it? Could we add a flag to indicate which scope it came from so that they could be removed later?

Also, are you sure there's no bug on this line? $query->wheres is being modified as it's looped over. What happens if it finds more than one match?

Edit: It appears this scope is applied when creating models in your seeder too, which may cause issues.

Edit2: Nevermind, it appears this is not a good idea at all. It causes the criteria to get added in multiple times when you try building up a query.

Sorry to ressurect this issue, but I'm not sure if i've found a bug or not – I've found that a global scope that adds a ->where() call, causes $model->save() calls to fail, due to the way that \Eloquent\Model::performUpdate() uses "dirty" attributes only.

Should I create a new issue for this? Or am I doing something outside the intended usage of global scopes?

Here's the apply() method I've defined in my global scope class:

public function apply(Builder $builder)
{
    // Hardcoded for the sake of example, ID actually will come from session    
    $builder->where('company_id', '=', '1');
}

Taylor added a little bit of info on global scopes to the docs today that will help: http://laravel.com/docs/eloquent#global-scopes

@HipsterJazzbo I had that issue too. Ended up using a macro instead so I could apply it only where needed.

public function apply(Builder $builder) {
    $builder->macro('withGuid', function (Builder $builder) {
        /** @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder */
        $model = $builder->getModel();
        $builder->leftJoin('guids', function (JoinClause $join) use ($model) {
            $join->on('guids.pk', '=', $model->getTable().'.'.$model->getKeyName());
            $join->on('guids.type', '=', Sql::raw('?',[get_class($model)]));
        });
        $builder->addSelect('guids.guid');
        return $builder;
    });
}

I don't think these are documented either..?

maybe something like this docs will help:
http://laravel.com/docs/eloquent#global-scopes

@huglester Huh? That's exactly where @tonydew linked. I don't see any mention of builder macros on that page.

Global scopes in Laravel are complicated (unlike rails)...

Would like to see simpler way to do this as well.
IDEAL SITUATION:

//in Posts
public function setGlobal($query){
$query->where('is_published',1)
}

//elsewhere
Posts::all() //with global scope
$user->posts() //has global scope by default (so we don't have to think about it when we write relationships) 
Posts::globalOverride() //overrides global and sets as plain

No idea if this is possible but I'll look at eloquent and see..

unless a rails dev has another ideas for syntax? @simonweil

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shopblocks picture shopblocks  Â·  3Comments

JamborJan picture JamborJan  Â·  3Comments

progmars picture progmars  Â·  3Comments

klimentLambevski picture klimentLambevski  Â·  3Comments

iivanov2 picture iivanov2  Â·  3Comments