PHP & Laravel
Tappable Query Scopes in Laravel
Enhance Laravel Query Scopes with Tappable Query Scopes - Learn about tappable query scopes in Laravel, a clean way to apply multiple scopes.
May 20223 min read
I recently saw an awesome tweet while browsing twitter which introduced this idea of tappable query scopes, and wanted to share it and dig into it a little more. The idea originally came from a PR on the laravel/framework repo in the comments link here.
What is does is allow you to create class based scopes that are invokable, and are called using the tap() method in Laravel, it looks a little like the below:
class MatchingEmail{ public function __construct( protected readonly string $email, ) {}
public function __invoke(Builder $query): void { $query->where('email', $this->email); }}
User::query()->tap(new MatchingEmail('taylor@laravel.com'))->get();Now if we step back a minute, and look at it. It is an amazing idea! It looks incredible, and I am wondering how it could be taken a step further? Let’s dig in.
I have a project locally called laravel-playground that I use to test out packages and ideas, it is very much worth doing! It allows me to test these ideas without it effecting any projects I might already have.
So what we have is an invokable class that acts like a callable that we can simply call to extend the query we are building. So in effect the above example could use the below:
User::query()->tap(function (Builder $query) { $query->where('email', 'taylor@laravel.com');})->get();But what if we want to call multiple scopes on a query? Do we want to do something like the following:
User::query() ->tap(new MatchingEmail('taylor@laravel.com')) ->tap(new ActiveUser()) ->get();While this is still quite clean, adding multiple tap calls on the query is going to start to get confusing. So I played around with a little bit of code that allows me to do the following:
User::query()->filter( new MatchingEmail('taylor@laravel.com'), new ActiveUser())->get();So what we are doing is passing in multiple callables so that we can iterate over them and apply them to the current builder instance. To do this all we have to do is create a new Macro for Illuminate\Database\Eloquent\Builder
Builder::macro('filter', function (...$scopes): Builder { collect($scopes)->each(function ($scope) { $this->tap($scope); }); return $this;});So we are extending the Builder and adding the filter method. This allows us to pass in a variadic list of callables to then collect and iterate over (this could be a foreach if you want) and then we call the tap method behind the scenes to apply the scope changes. eventually returning the builder instance back.
Related
More in PHP & Laravel
Sending And Receiving Webhooks In Laravel
A webhook is a contract between two codebases that never meet. Both ends of it in plain Laravel: what to sign, how long to retry, and how the receiver avoids doing the work twice.
Oct 2026 · 12 min read
PHP & LaravelBuilding a home-education platform with Laravel 13, Inertia and React, and shipping it to Laravel Cloud
A Laravel 13, Inertia and React platform for home educating my son: an append-only learning record, a planner, AI kept on a short lead, and what Laravel Cloud asked of the code.
Oct 2026 · 31 min read
PHP & LaravelThe UK is mandating e-invoicing in 2029, and your invoicing code is not ready
From 2029 every UK VAT invoice has to be structured data that passes about eleven hundred rules. What that means for the invoices table you already have, and the four fields that break first.
Sept 2026 · 10 min read
Same subject