戏里戏外

Laravel 中使用自定义查询构造器替代 Scope

2024-11-27#Laravel#Eloquent

查询作用域(Query Scopes) 让查询更易读,但它使用的是魔术方法。

虽然查询作用域很实用,但自定义查询构造器提供了更多的灵活性和更好的开发体验。

使用自定义构建器可以让代码更容易维护,也更容易理解。

查询作用域

考虑以下代码:

<?php

use App\Models\User;
     
$users = User::query()
  ->where('votes', '>', 100)
  ->where('active', 1)
  ->orderBy('created_at')
  ->get();

当查询变得过于复杂或难以阅读时,可以使用查询作用域将它们抽象出来:

<?php

namespace App\Models;
     
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

/**
 * @method static Builder popular()
 * @method static Builder active()
 */
class User extends Model
{
  public function scopePopular($query)
  {
    return $query->where('votes', '>', 100);
  }

  public function scopeActive($query)
  {
    return $query->where('active', 1);
  }
}

现在可以这样使用它们:

$users = User::popular()
  ->active()
  ->orderBy('created_at')
  ->get();

自定义查询构造器

使用自定义查询构造器来实现相同的功能,但代码更加清晰:

  1. 创建一个自定义查询构造器类 App\Eloquent\Builders\UserQueryBuilder
<?php

namespace App\Eloquent\Builders;

use Illuminate\Database\Eloquent\Builder;

class UserQueryBuilder extends Builder
{
  public function popular(): self
  {
    return $this->where('votes', '>', 100);
  }

  public function active(): self
  {
    return $this->where('active', 1);
  }
}
  1. 在模型的 newEloquentBuilder 方法中使用它
<?php

namespace App\Models;

use App\Eloquent\Builders\UserQueryBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
  public function newEloquentBuilder($query): UserQueryBuilder
  {
    return new UserQueryBuilder($query);
  }
}
  1. 现在可以像这样使用它
$users = User::query()
  ->popular()
  ->active()
  ->orderBy('created_at')
  ->get();

测试

使用 Pest 可以很容易的测试这些查询构造器。

<?php

use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;

uses(LazilyRefreshDatabase::class);
beforeEach(function () {
    // 创建测试数据
    User::factory()->count(3)->create(['votes' => 50, 'active' => 1]);
    User::factory()->count(2)->create(['votes' => 150, 'active' => 1]);
    User::factory()->create(['votes' => 150, 'active' => 0]);
});

it('can query popular users', function () {
    $users = User::query()->popular()->get();

    expect($users)->toHaveCount(3)
        ->each(fn ($user) => $user->votes->toBeGreaterThan(100));
});

it('can query active users', function () {
    $users = User::query()->active()->get();

    expect($users)->toHaveCount(5)
        ->each(fn ($user) => $user->active->toBeTrue());
});

it('can combine multiple query conditions', function () {
    $users = User::query()
        ->popular()
        ->active()
        ->get();

    expect($users)->toHaveCount(2)
        ->each(fn ($user) => $user->votes->toBeGreaterThan(100)
            ->active->toBeTrue()
        );
});

这些测试确保了:

  1. popular() 方法正确筛选出投票数超过 100 的用户
  2. active() 方法正确筛选出活跃用户
  3. 可以正确组合多个查询条件

通过这些测试可以确保查询构造器的行为符合预期,并且在代码修改时能够及时发现问题。

优势

  1. 更好的 IDE 支持
  2. 更清晰的代码组织
  3. 没有魔术方法
  4. 更容易测试
  5. 更好的类型提示