Fluently removing global scopes with Laravel's new ScopedBy Attribute

February 13, 2024

Laravel v10.44 just shipped with a new approach to global scopes: #[ScopedBy]. Using my codebase as an example: Products have a global scope that hides them if their Company is not visible.

1<?php
2 
3namespace App\Models;
4 
5use App\Models\Scopes\CompanyVisibility;
6use Illuminate\Database\Eloquent\Attributes\ScopedBy;
7 
8#[ScopedBy([CompanyVisibility::class])]
9class Product
10{
11 ...
12}

I like this syntax a lot, and I also like to use a scope to remove global scopes. It wasn't immediately obvious how to do that but after a little source diving I found the answer via the SoftDelete trait: an extend method that registers a macro.

1artisan make:scope CompanyVisibility
1<?php
2 
3namespace App\Models\Scopes;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Scope;
8 
9class CompanyVisibility implements Scope
10{
11 public function apply(Builder $builder, Model $model): void
12 {
13 $builder->whereHas('company', fn ($builder) => $builder->active())
14 }
15 
16 public function extend(Builder $builder): void
17 {
18 $builder->macro('withHidden', fn (Builder $builder) => $builder->withoutGlobalScope($this));
19 }
20}

That new builder macro means you can fluently remove the global scope:

1$product = Product::withHidden()->first();
2 
3$companyProducts = $company->products()->withHidden()->get();

You could achieve the same thing using withoutGlobalScope(CompanyVisibility::scope) but I think writing it once and using a fluent method is much nicer.

This blog is an experiment: less about education and more about documenting the oddities I've experienced while building web apps.

My hope is that you're here because of a random search and you're experiencing something I've had to deal with.

Popular Posts

Recent Posts

View all