Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.x] Add pipe method query builders #55171

Merged
merged 8 commits into from
Mar 27, 2025

Conversation

timacdonald
Copy link
Member

@timacdonald timacdonald commented Mar 26, 2025

This PR introduces a pipe method to the base query builder and eloquent query builder. The functionality of this method is the same as the collection pipe method.

$records = DB::query()
    ->from('...')
    // ...
    ->tap(new TappableScope) // returns the query
    ->pipe(new ActionScope); // executes the query and returns the result

Why?

Model scopes offer the ability to return the result of a query. Scopes that execute the query and return a result I've personally dubbed action scopes. My blog post on the idea.

An action scope:

class Voucher extends Model
{
    // ...

    public function scopeExtendUntil(Builder $builder, Carbon $date): int
    {
        return $builder->update(['expires_at' => $date]);
    }
}

Usage:

$count = Voucher::query()
    ->whereExpired()
    ->extendUntil(Carbon::now()->addYear());

// $count is now an integer containing the number of extended vouchers.

return "{$count} vouchers have been extended for a year";

You can consider native methods things like count, get, pluck, etc., to be built-in action scopes.


When using the query builder directly for non-eloquent queries, I very much enjoy using tappable scopes for reusable query filtering.

class ServerFilter
{
    public function __construct(private string $server)
    {
        //
    }

    public function __invoke(Builder $query): void
    {
        if ($this->server) {
            $query->where('server', $this->server);
        }
    }
}

Usage:

$result = DB::query()
    ->from('...')
    // ...
    ->tap(new ServerFilter($request->query('server')))
    ->get();

There is currently no way to create an action scope using this pattern.

Imagine you have many queries throughout your application's controllers that all have their own unique filtering and always end in a similar pattern: order, paginate, include query string, and map into an value object.

$result = DB::query()
    ->from('...')
    // ...
    ->orderBy('timestamp')
    ->cursorPaginate(100, cursorName: 'c')
    ->withQueryString()
    ->through(fn ($row) => new ValueObject($row));

With the pipe method, it is possible to wrap this up into a reusable action scope:

class Paginate
{
    /**
     * @param  class-string  $mapInto
     * @param  literal-string  $orderBy
     * @param  non-negative-int  $perPage
     */
    public function __construct(
        private string $mapInto,
        private string $orderBy = 'created_at',
        private int $perPage = 100,
    ) {
        //
    }

    public function __invoke(Builder $query): CursorPaginator
    {
        return $query->orderByDesc($this->orderBy)
                     ->cursorPaginate($this->perPage, cursorName: 'c')
                     ->withQueryString()
                     ->through(fn ($row) => new $this->mapInto($row));
    }
}

Query may now be refactored:

$result = DB::query()
    ->from('...')
    // ...
    ->pipe(new Paginate(mapInto: ValueObject));

Aside from the above, I often find the pipe helper useful when building collection pipelines to inline some more complex logic related to the query that I otherwise would have to split out before the collection call chain.

@laravel laravel deleted a comment from github-actions bot Mar 26, 2025
@timacdonald timacdonald changed the title [12.x] Pipe query builder [12.x] Add pipe method query builders Mar 26, 2025
@timacdonald timacdonald marked this pull request as ready for review March 26, 2025 01:50
@taylorotwell taylorotwell merged commit 0b0a274 into laravel:12.x Mar 27, 2025
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants