sajadsholi's avatar

global scopes for select query

I write a trait in laravel named prioritiable and I add a global scope to the query to sort the query result based on priority value

The problem is the scope add the order by SQL clause to all SELECT, DELETE , UPDATE , INSERT but I want to just to have the scope on select

so I did this

trait Prioritiable
{
    protected static function bootPrioritiable(): void
    {
        static::retrieved(function ($model) {
            $model->addGlobalScope(OrderByPriorityScope::class);
        });
    }

}

The problem solved for first, find , firstOrFail , findOrFail but the scope does not work for get , all , paginate, since I think these three functions do not fire the retrieved event

the AI says use $builder->getQuery()->type == 'select' in the scope apply function but the type does not exists and throws error

0 likes
10 replies
Glukinho's avatar
Level 31

@sajadsholi When a query is being built, it doesn't "know" for what operation it will be applied.

Post::where('name', 'some name')->orderBy('username')->get();
                                                     ^
                      the query doesn't know here if |
	              it will SELECT or UPDATE or DELETE |

And when retrieved event is dispatched, the data is already retrieved from DB and it's too late to apply a scope.

I think you have two options:

  1. utilize local scope instead of global and apply it for every scoped query,
  2. call ::withoutGlobalScope(OrderByPriorityScope::class) for queries that should NOT be scoped.
JussiMannisto's avatar

The static::retrieved method will not work as it doesn't modify the database query at all. It's a model lifecycle hook method that is executed after the model has been retrieved from the database.

It's not possible to reliably add an order-by clause using a global scope only to SELECT queries, given how query builders work. When the scope is applied, Eloquent might not yet know what the database operation will be.

In my opinion, it's much better to add a local scope and use it explicitly:

MyModel::byPriority()->get();

I'd use global scopes very judiciously in general, as hidden logic is always a potential source of bugs.

Glukinho's avatar

Yesterday I found out a global scope is applied even when you try to fetch a particular model with simple Post::find(123).

I mean, if post with id 123 doesn't satisfy applied global scope, it will not be returned with find(). I find it's completely wrong and as another reason to not use global scopes.

JussiMannisto's avatar

@Glukinho It's completely correct, though. find() is just another query method and global scopes are just scopes. Take soft-deleted models for example: find() should return null on a soft-deleted model even if that ID exists in the database.

But I agree, be very careful with global scopes.

Glukinho's avatar

@JussiMannisto for me it was a surprise.

I think more logical would be:

Post::find(123); // particular model is always returned regardless of global scopes as I explicitly want that row in table

Post::where('id', 123)->first(); // query with conditions may or may not return a model, respecting global scopes
JussiMannisto's avatar

@Glukinho That would be inconsistent with the rest of the Eloquent methods. find() is just a convenience method, just like first(). This is what find() does:

return $this->whereKey($id)->first($columns);
1 like
Snapey's avatar

@Glukinho global scope are very often used to enforce tenancy. find(n) would break lots of security if it always returned the model.

1 like

Please or to participate in this conversation.