Complex Queries

When scopes multiply, extract a query object

Imagine a controller with this query chain:

// app/Http/Controllers/UserController.php
$results = User::search($request)
    ->onlyEditor()
    ->filterByStatus($request)
    ->orderBy('created_at', 'desc')
    ->paginate();

Testing this in a feature test means creating users with different roles, statuses, and dates. One small change breaks everything.

The Solution: Query Object
// app/Queries/SearchAndFilterUsersQuery.php
class SearchAndFilterUsersQuery
{
    public function run($request)
    {
        return User::search($request)
            ->onlyEditor()
            ->filterByStatus($request)
            ->orderBy('created_at', 'desc')
            ->paginate();
    }
}
// app/Http/Controllers/UserController.php
public function index(Request $request, SearchAndFilterUsersQuery $query)
{
    $results = $query->run($request);

    return view('users.index', compact('results'));
}
Feature Test — mock the query object
// tests/Feature/Http/Controllers/UserController/IndexTest.php
$query = Mockery::mock(SearchAndFilterUsersQuery::class);
$this->app->instance(SearchAndFilterUsersQuery::class, $query);

$users = User::factory()->count(3)->create();

$query->shouldReceive('run')
    ->once()
    ->andReturn($users);

$response = $this->get('/users');

$response->assertViewIs('users.index')
    ->assertViewHas('results', $users);
Unit Test — test the query object
// tests/Unit/Queries/SearchAndFilterUsersQueryTest.php
class SearchAndFilterUsersQueryTest extends TestCase
{
    #[Test]
    public function run_finds_users_by_name(): void
    {
        $user1 = User::factory()->create(['name' => 'John']);
        $user2 = User::factory()->create(['name' => 'Jane']);

        $query = new SearchAndFilterUsersQuery;
        $results = $query->run((object)['search' => 'John']);

        $this->assertTrue($results->contains($user1));
        $this->assertFalse($results->contains($user2));
    }
}