A global scope constrains every query of a model. The shop's reports should never count cancelled orders, so Order carries #[ScopedBy(ExcludeCancelledScope::class)], and the scope class of Generating Models implements Scope with a one-line apply(Builder $builder, Model $model): $builder->where($model->qualifyColumn('status'), '!=', OrderStatus::Cancelled).
use App\Models\Order;
use App\Models\Scopes\ExcludeCancelledScope;
$visible = Order::count();
$all = Order::withoutGlobalScope(ExcludeCancelledScope::class)->count();
echo "$visible of $all\n";
echo Order::find(5)?->id ?? 'order 5 is hidden', "\n";-- select count(*) as `aggregate` from `orders` where `orders`.`status` != 'cancelled' -- select count(*) as `aggregate` from `orders` 9 of 10 -- select * from `orders` where `orders`.`id` = 5 and `orders`.`status` != 'cancelled' limit 1 order 5 is hidden
qualifyColumn() wrote orders.status, unambiguous even when a join adds another status. Even find() obeyed the scope, its power and its danger: an admin screen or refund job must remember withoutGlobalScope() (or withoutGlobalScopes()), or rows seem to vanish. SoftDeletes is itself a global scope, and static::addGlobalScope('name', fn (Builder $q) => ...) in booted() registers one without a class. Keep global scopes for truly universal rules, such as tenancy.