If you are using a class-based global scope, you are not required to pass a name when registering the scope.
For example, inside a model, this will work:
protected static function booted(): void
{
parent::booted();
static::addGlobalScope(new OrderBy('last_name', 'desc'));
}
However, if you are using multiple instances of the same class-based scope, you will run into a problem.
Looking at this code, you would expect it to be ordered by both last_name
and first_name
, in that order:
protected static function booted(): void
{
parent::booted();
static::addGlobalScope(new OrderBy('last_name', 'desc'));
static::addGlobalScope(new OrderBy('first_name'));
}
But in reality, the model will only be ordered by first_name
.
Why is this?
If we peek inside Laravel's addGlobalScope
function, we can see that it replaces the first class-based scope registration with the second one, because they are both instances of the same class:
// this code is basically the same since Laravel 9 through Laravel 12
public static function addGlobalScope($scope, $implementation = null)
{
if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) {
return static::$globalScopes[static::class][$scope] = $implementation;
} elseif ($scope instanceof Closure) {
return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
} elseif ($scope instanceof Scope) {
// This is the offending line. The key is just our scope's class name.
return static::$globalScopes[static::class][get_class($scope)] = $scope;
} elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) {
return static::$globalScopes[static::class][$scope] = new $scope;
}
}
The solution is easy. Pass in a unique string scope name when registering both instances of the class-based scope. Now the scope registration is keyed by the scope name parameter, and they will both be applied in the order you registered them:
protected static function booted(): void
{
parent::booted();
static::addGlobalScope('order-by-last', new OrderBy('last_name', 'desc'));
static::addGlobalScope('order-by-first', new OrderBy('first_name'));
}
The ability to pass a name with a class-based scope was added in Laravel 9, so you'll need to be on at least that version to use this solution.
Here to help,
Joel
P.S. If you're staring at a problem in your Laravel app and not sure how to solve it, I can help.