使用 Laravel 模型作用域来保持代码整洁

当你继续开发 Laravel 应用程序时,你会发现你需要执行相同的查询。如果你的项目与我参与的大多数项目一样,那么你将从许多地方重复查询代码。

在需要更改查询之前,这种方法非常有效。因为查询会在整个代码中重复,所有现在需要在所有地方都对其进行修改。你可能会忘记修改某个地方了 🤦🏻。这是一个巨头疼的修改问题。由于忘记在多个地方更新代码,我甚至无法计算自己遇到了多少错误(以及自己编写了多少)。有一些简单的解决方案可以减少重复查询。

  • 您可以将所有数据库代码移动到存储过程,并且只能通过存储过程与数据库交互。
  • 为查询创建存储库、服务、构建器或其他类。
  • 在 Eloquent 模型中添加执行查询的方法。

这些方法都可以正常运行,但其中各有优缺点。Laravel内置的Eloquent 查询作用域是这些方案中一种较好的解决问题的方案。与我提到的其他方法相比, 查询作用域提供了一些优势。其中最重要的三个好处是:

  1. 它是 Laravel 的重要模块之一,并内置到 Eloquent 的工作方式中。
  2. 它可以更容易地创建较小的,可重复使用的查询范围,可以在较大的查询范围或一次性查询中组合。
  3. 它们可以自动应用于一个模型上的所有查询。

那么到底什么是查询作用域?

查询作用域是一个在模型的查询生成器上工作的方法,以修改将被运行的默认查询。查询范围与其他方法的不同之处在于, 它是建立在Laravel的Eloquent类中的, 意味着你会运行一组普通的约束条件, 而不是整个查询。下面是一个典型的方法, 你可能会写到还没有完成的任务, 并分配给一个用户.

public function GetActiveTasksForUser(int userId) : Collection
{  
    return Task::where('assigned_to_user_id', $userId)
        ->where('isComplete', false)
        ->get();
}

现在,如果你考虑一下典型的查询,你可能需要对一项任务进行查询。不难想象,你既需要查询一个用户的所有任务,又需要查询他们未完成的任务。让我们增加第二个方法,只获取分配给一个用户的任务。

public function GetAllTasksForUser(int userId) : Collection
{  
    return Task::where('assigned_to_user_id', $userId)->get();
}

请注意,这个方法的前半部分与前一个方法相同。可以看到,仅仅是两个不同的查询,已经再写重复的代码了。我们应该很容易看到,这可能会使项目很快失去控制。让我们把这两个约束条件(分配给用户的任务和未完成的任务)转换成查询范围,看看这对这些方法有什么帮助。

// 在任务模型中 (Task.php)
public function scopeAssignedToUser(Builder $query, int $userId) : void
{
    $query->where('assignedtouser_id', $userId);
}

public function scopeIncomplete(Builder $query) : void
{
    $query->where('isComplete', false);
}

可以更新之前的两个方法,使用新的查询作用域,避免重复的查询 WHERE 子句。

public function GetActiveTasksForUser(int $userId) : Collection
{  
    return Task::assignedToUser($userId)->incomplete()->get();
}

public function GetAllTasksForUser(int $userId) : Collection
{  
    return Task::assignedToUser($userId)->get();
}

这种方法的最佳之处在于,每当需要获取分配给特定用户或未完成任务的新查询时,我们都只需要调用简单方法。

如何创建查询作用域?

创建查询作用域与创建任何方法都非常相似。使该方法成为查询作用域,只需遵循三个要求。

  1. 局部作用域需要位于它们所应用的模型上。全局作用域需要实现 Illuminate\Database\Eloquent\Scope;稍后我们将会介绍局部作用域和全局作用域。
  2. 局部作用域需要以单词 scope 开头。对于局部作用域,单词 scope 后面的所有内容都成为使用 camelCasing 调用作用域的方法名。例如:调用 scopeAssignedToUser() 时,调用的是 assignedToUser。全局作用域没有任何特殊命名要求。
  3. 第一个参数应该是 Illuminate\Database\Eloquent\Builder 的实例。对于全局作用域,第二个参数应该是 Illuminate\Database\Eloquent\Model 的实例。

局部作用域可以没有其他参数,也可以在构造器参数之后添加任何参数。局部作用域中的参数用于更改当前运行的查询。例如,在 scopeAssignedToUser 中,我们传递了一个 $userId 参数来更改查询检查任务分配给哪个用户。

但查询不属于模型。

根据应用程序的大小和复杂性或编码标准,您可能会认为查询作用域不属于模型。的确,使用 Eloquent,很容易让您的模型变得臃肿,充满不属于它的逻辑。如果不小心,模型可能会变成业务逻辑、持久层和其他逻辑的混合体。

先别急着注销查询作用域。有一个简单的方法可以利用查询作用域,而不使模型臃肿。由于查询作用域只是在模型的查询构建器上工作的方法,所以您可以扩展查询构建器并在扩展的类上包含您的查询作用域。这就把每个模型的所有查询作用域放在它自己的专用类中。

// 在 TaskQueryBuilder.php
use Illuminate\Database\Eloquent\Builder;

class TaskQueryBuilder extends Builder
{
    public function assignedToUser(int $userId) : self
    {
        return $this->where('assigned_to_user_id', $userId);
    }
}

// 在 Task.php
public function newEloquentBuilder($query) : TaskQueryBuilder
{
    return new TaskQueryBuilder($query);
}

// 仍然可以像以前一样调用
Task::assignedToUser($userId)->get();

局部作用域 vs. 全局作用域

局部作用域和全局作用域之间的主要区别在于,局部作用域必须被调用,并且用于可能并不总是需要的重复性的数据库约束。另一方面,对于模型上的每个查询,全局作用域都会自动运行。它们是为您几乎总是要运行的东西准备的,并且在查询该模型时不想忘记的。对于全局作用域,可以考虑像不加载软删除记录(Laravel 的 SoftDeletes 特性使用全局作用域),或者为多租户应用程序(为多个不同的公司提供数据集的应用程序)检查记录是否属于某个公司。

我们上面创建的作用域是局部作用域。下面的示例是全局作用域,它们在创建和使用方面有一些不同。

  • 它们需要自己的专用类,并在模型的引导方法中注册。
  • 方法名不以 scope 为前缀。
  • 它们不能是同一个请求中的每个查询都有的容易变化的参数。全局作用域的任何参数都必须是某种类型的全局状态。
  • 它们无需显式调用即可运行。为了避免在查询中运行它们,需要使用 withoutGlobalScope 方法。

让我们创建一个全局作用域的示例,用于检查一个记录是否属于某个公司。

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class BelongsToCompanyScope implements Scope
{
    protected CompanyResolver $resolver;

    public function __construct(CompanyResolver $resolver)
    {
        $this->resolver = $resolver
    }

    /**
     * 将作用域应用到给定的 Eloquent 查询构建器
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     *
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('company_id', $this->resolver->getCurrentCompanyId());
    }
}

此示例显示了如何使用传递到全局作用域中的解析器来更改要为其加载记录的公司。解析程序可以是跟踪当前用户的当前公司的任何类。它可能存储在会话、缓存或查询用户的数据库表中。

这是让全局作用域使用可能根据当前用户或会话更改的参数的最简单方法之一。然而,它也有不利的一面,我们现在正在将某种类型的全局状态引入范围,而且该模型还必须知道 CompanyResolver 这一概念。这可能会使将来修复错误和更改功能变得更加困难。出于这个原因,我不太喜欢在较大的项目或任何不依赖于全局用户或整个应用程序需要知道的会话状态的项目中这样做。

为了能够使用这个BelongsToCompany作用域,我们需要在模型类中注册它。

// 在 Task.php 文件中
/**
 * 模型的“booted”文
 *
 * @return void
 */
protected static function booted()
{
    static::addGlobalScope(new BelongsToCompanyScope(new CompanyResolver));
}

现在,每次我们查询一个任务时,它都会自动将where company_id = $someCompanyId加入到查询中。如果我们不希望全局范围的查询都加上这个查询条件,要为特定的查询排除它也是很容易的。例如,当我们想获得所有公司的所有任务。在这种情况下,我们需要排除BelongsToCompanyScope

Task::withoutGlobalScope(BelongsToCompanyScope::class);

如果我们想排除多个作用域或所有作用域,我们可以使用 withoutGlobalScopes。当调用时没有设置参数的话,那么所有的作用域都会被删除。另外,当用一个数组调用它时,则它会删除数组中的所有作用域。

查询作用域只是减少重复查询代码的一种方法

正如文章前面提到的,查询作用域只是减少重复代码的一种方法。它们可能不是每个情况或项目的最佳选择,但它们应该是你依需求可以随手拿来用的称手工具之一。总之,得由你和你的团队根扰实际的情况来决定你的项目要遵循什么模式和做法。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/kevinhickssw/avoid-the-pa...

译文地址:https://learnku.com/laravel/t/62478

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4

scope是挺好,就是IDE追踪不到。
phpstorm用久了,只要是IDE追踪不到的,都不想用 :joy:
或者一定要加上@uses注释让IDE能追踪到,已经变成强迫症了

2年前 评论
amosmz 1年前
Lu_Shangyu (作者) 11个月前

方法名字建议还是小写开头,别搞成类驼峰

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!