搜索
首页后端开发php教程学会在Laravel中掌握查询范围

Learn to master Query Scopes in Laravel

在构建Laravel应用程序时,您可能需要编写具有约束条件的查询,这些约束条件在整个应用程序中的多个地方使用。也许您正在构建一个多租户应用程序,并且您必须不断向查询中添加where约束以按用户的团队进行筛选。或者,也许您正在构建一个博客,并且您必须不断向查询中添加where约束以筛选博客文章是否已发布。

在Laravel中,我们可以使用查询范围来帮助我们将这些约束条件整洁地保存在一个地方并重复使用。

在本文中,我们将研究局部查询范围和全局查询范围。我们将学习两者之间的区别,如何创建您自己的查询范围,以及如何编写它们的测试。

在阅读完本文后,您应该能够自信地在Laravel应用程序中使用查询范围。

什么是查询范围?


查询范围允许您以可重用的方式在Eloquent查询中定义约束条件。它们通常定义为Laravel模型上的方法,或者作为实现IlluminateDatabaseEloquentScope接口的类。

它们不仅非常适合在一个地方定义可重用的逻辑,而且还可以通过将复杂的查询约束隐藏在简单的函数调用之后来使您的代码更具可读性。

查询范围分为两种类型:

  • 局部查询范围 - 您必须手动将这些范围应用于您的查询。
  • 全局查询范围 - 默认情况下,这些范围会应用于模型上的所有查询,前提是已注册该查询。

如果您曾经使用过Laravel内置的“软删除”功能,您可能已经在不知不觉中使用了查询范围。Laravel使用局部查询范围为您提供模型上的withTrashedonlyTrashed等方法。它还使用全局查询范围自动向模型上的所有查询添加whereNull('deleted_at')约束,以便默认情况下查询中不会返回软删除的记录。

让我们来看看如何在Laravel应用程序中创建和使用局部查询范围和全局查询范围。

局部查询范围


局部查询范围定义为Eloquent模型上的方法,允许您定义可以手动应用于模型查询的约束条件。

假设我们正在构建一个具有管理面板的博客应用程序。在管理面板中,我们有两个页面:一个用于列出已发布的博客文章,另一个用于列出未发布的博客文章。

我们假设博客文章是使用AppModelsArticle模型访问的,并且数据库表具有一个可为空的published_at列,用于存储博客文章的发布时间。如果published_at列在过去,则该博客文章被认为已发布。如果published_at列在未来或为null,则该博客文章被认为未发布。

要获取已发布的博客文章,我们可以编写如下查询:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->where('published_at', 'get();</code>

要获取未发布的博客文章,我们可以编写如下查询:

<code>use App\Models\Article;
use Illuminate\Contracts\Database\Eloquent\Builder;

$unpublishedPosts = Article::query()
    ->where(function (Builder $query): void {
        $query->whereNull('published_at')
            ->orWhere('published_at', '>', now());
    })
    ->get();</code>

上面的查询并不特别复杂。但是,假设我们在整个应用程序中的多个地方使用它们。随着出现次数的增加,我们犯错或忘记在一个地方更新查询的可能性越来越大。例如,开发人员可能会意外地使用>=而不是来查询已发布的博客文章。或者,确定博客文章是否已发布的逻辑可能会更改,我们需要更新所有查询。

这就是查询范围非常有用的地方。因此,让我们通过在AppModelsArticle模型上创建局部查询范围来整理我们的查询。

局部查询范围是通过创建一个以scope开头并以范围的预期名称结尾的方法来定义的。例如,名为scopePublished的方法将在模型上创建一个published范围。该方法应该接受一个IlluminateContractsDatabaseEloquentBuilder实例并返回一个IlluminateContractsDatabaseEloquentBuilder实例。

我们将这两个范围都添加到AppModelsArticle模型中:

<code>declare(strict_types=1);

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Article extends Model
{
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('published_at', 'where(function (Builder $query): Builder {
            return $query->whereNull('published_at')
                ->orWhere('published_at', '>', now());
        });
    }

    // ...
}</code>

正如我们在上面的示例中看到的,我们将where约束从之前的查询移动到了两个单独的方法中:scopePublishedscopeNotPublished。我们现在可以在我们的查询中像这样使用这些范围:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->published()
    ->get();

$unpublishedPosts = Article::query()
    ->notPublished()
    ->get();</code>

在我个人看来,我发现这些查询更容易阅读和理解。这也意味着如果我们将来需要使用相同约束条件编写任何查询,我们可以重复使用这些范围。

全局查询范围


全局查询范围执行与局部查询范围类似的功能。但是,它不是在逐个查询的基础上手动应用,而是自动应用于模型上的所有查询。

正如我们前面提到的,Laravel内置的“软删除”功能使用了IlluminateDatabaseEloquentSoftDeletingScope全局查询范围。此范围会自动向模型上的所有查询添加whereNull('deleted_at')约束。如果您有兴趣了解其底层工作原理,可以在这里查看GitHub上的源代码。

例如,假设您正在构建一个具有管理面板的多租户博客应用程序。您可能只想允许用户查看属于其团队的文章。因此,您可能会编写如下查询:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->where('published_at', 'get();</code>

此查询很好,但很容易忘记添加where约束。如果您正在编写另一个查询并忘记添加约束,则最终会在您的应用程序中出现一个错误,该错误将允许用户与不属于其团队的文章进行交互。当然,我们不希望发生这种情况!

为了防止这种情况,我们可以创建一个全局范围,我们可以将其自动应用于我们所有AppModelArticle模型查询。

#如何创建全局查询范围

让我们创建一个全局查询范围,该范围按team_id列过滤所有查询。

请注意,为了本文的目的,我们保持示例简单。在实际应用程序中,您可能希望使用更强大的方法来处理用户未经身份验证或用户属于多个团队等情况。但现在,让我们保持简单,以便我们可以专注于全局查询范围的概念。

我们将首先在终端中运行以下Artisan命令:

<code>use App\Models\Article;
use Illuminate\Contracts\Database\Eloquent\Builder;

$unpublishedPosts = Article::query()
    ->where(function (Builder $query): void {
        $query->whereNull('published_at')
            ->orWhere('published_at', '>', now());
    })
    ->get();</code>

这应该已经创建了一个新的app/Models/Scopes/TeamScope.php文件。我们将对此文件进行一些更新,然后查看完成的代码:

<code>declare(strict_types=1);

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Article extends Model
{
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('published_at', 'where(function (Builder $query): Builder {
            return $query->whereNull('published_at')
                ->orWhere('published_at', '>', now());
        });
    }

    // ...
}</code>

在上面的代码示例中,我们可以看到我们有一个新的类,它实现了IlluminateDatabaseEloquentScope接口并具有一个名为apply的单个方法。这就是我们定义要应用于模型查询的约束条件的方法。

我们的全局范围现在可以使用了。我们可以将其添加到任何我们想要将查询范围缩小到用户团队的模型中。

让我们将其应用于AppModelsArticle模型。

#应用全局查询范围

有多种方法可以将全局范围应用于模型。第一种方法是在模型上使用IlluminateDatabaseEloquentAttributesScopedBy属性:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->published()
    ->get();

$unpublishedPosts = Article::query()
    ->notPublished()
    ->get();</code>

另一种方法是在模型的booted方法中使用addGlobalScope方法:

<code>use App\Models\Article;

$articles = Article::query()
    ->where('team_id', Auth::user()->team_id)
    ->get();</code>

这两种方法都将where('team_id', Auth::user()->team_id)约束应用于AppModelsArticle模型上的所有查询。

这意味着您现在可以编写查询,而无需担心按team_id列进行过滤:

<code>php artisan make:scope TeamScope</code>

如果我们假设用户属于team_id1的团队,则将为上面的查询生成以下SQL:

<code>declare(strict_types=1);

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\Auth;

final readonly class TeamScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('team_id', Auth::user()->team_id);
    }
}</code>

这很酷,对吧?!

#匿名全局查询范围

定义和应用全局查询范围的另一种方法是使用匿名全局范围。

让我们更新我们的AppModelsArticle模型以使用匿名全局范围:

<code>declare(strict_types=1);

namespace App\Models;

use App\Models\Scopes\TeamScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;

#[ScopedBy(TeamScope::class)]
final class Article extends Model
{
    // ...
}</code>

在上面的代码示例中,我们使用了addGlobalScope方法在模型的booted方法中定义匿名全局范围。addGlobalScope方法接受两个参数:

  • 范围的名称 - 如果您需要在查询中忽略它,则可以使用此名称来引用范围
  • 范围约束 - 定义要应用于查询的约束的闭包

与其他方法一样,这会将where('team_id', Auth::user()->team_id)约束应用于AppModelsArticle模型上的所有查询。

根据我的经验,匿名全局范围不如在单独的类中定义全局范围常见。但了解它们可用是很有好处的,以备不时之需。

#忽略全局查询范围

有时您可能希望编写一个不使用已应用于模型的全局查询范围的查询。例如,您可能正在构建一个需要包含所有记录的报表或分析查询,而不管全局查询范围如何。

如果是这种情况,您可以使用两种方法之一来忽略全局范围。

第一种方法是withoutGlobalScopes。如果未向其传递任何参数,此方法允许您忽略模型上的所有全局范围:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->where('published_at', 'get();</code>

或者,如果您只想忽略给定的一组全局范围,您可以将范围名称传递给withoutGlobalScopes方法:

<code>use App\Models\Article;
use Illuminate\Contracts\Database\Eloquent\Builder;

$unpublishedPosts = Article::query()
    ->where(function (Builder $query): void {
        $query->whereNull('published_at')
            ->orWhere('published_at', '>', now());
    })
    ->get();</code>

在上面的示例中,我们忽略了AppModelsScopesTeamScope和另一个名为another_scope的虚构匿名全局范围。

或者,如果您只想忽略单个全局范围,可以使用withoutGlobalScope方法:

<code>declare(strict_types=1);

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Article extends Model
{
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('published_at', 'where(function (Builder $query): Builder {
            return $query->whereNull('published_at')
                ->orWhere('published_at', '>', now());
        });
    }

    // ...
}</code>

#全局查询范围注意事项

务必记住,全局查询范围仅应用于通过模型进行的查询。如果您使用IlluminateSupportFacadesDB外观编写数据库查询,则不会应用全局查询范围。

例如,假设您编写了此查询,您希望它只抓取属于登录用户的团队的文章:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->published()
    ->get();

$unpublishedPosts = Article::query()
    ->notPublished()
    ->get();</code>

在上面的查询中,即使在AppModelsArticle模型上定义了AppModelsScopesTeamScope全局查询范围,也不会应用该范围。因此,您需要确保在数据库查询中手动应用约束条件。

测试局部查询范围


既然我们已经学习了如何创建和使用查询范围,那么我们将研究如何为它们编写测试。

有多种方法可以测试查询范围,您选择的方法可能取决于您的个人喜好或您正在编写的范围的内容。例如,您可能希望为范围编写更多单元样式的测试。或者,您可能希望编写更多集成样式的测试,这些测试会在诸如控制器之类的上下文中测试范围。

就我个人而言,我喜欢混合使用两者,这样我就可以确信范围正在添加正确的约束,并且范围实际上正在查询中使用。

让我们从前面的示例publishednotPublished范围开始,并为它们编写一些测试。我们将需要编写两个不同的测试(每个范围一个):

  • 一个测试检查published范围只返回已发布的文章。
  • 一个测试检查notPublished范围只返回未发布的文章。

让我们看看这些测试,然后讨论正在做的事情:

<code>use App\Models\Article;

$publishedPosts = Article::query()
    ->where('published_at', 'get();</code>

我们可以在上面的测试文件中看到,我们首先在setUp方法中创建一些数据。我们创建了两篇已发布的文章、一篇未安排的文章和一篇已安排的文章。

然后是一个测试(only_published_articles_are_returned),它检查published范围只返回已发布的文章。还有一个测试(only_not_published_articles_are_returned),它检查notPublished范围只返回未发布的文章。

通过这样做,我们现在可以确信我们的查询范围正在按预期应用约束条件。

在控制器中测试范围


正如我们提到的,测试查询范围的另一种方法是在控制器中使用的上下文中测试它们。虽然范围的隔离测试可以帮助断言范围正在向查询添加正确的约束,但它实际上并没有测试范围是否按预期在应用程序中使用。例如,您可能忘记向控制器方法中的查询添加published范围。

通过编写断言在控制器方法中使用范围时返回正确数据的测试,可以捕获这些类型的错误。

让我们以具有多租户博客应用程序的示例为例,并为列出文章的控制器方法编写一个测试。我们假设我们有一个非常简单的控制器方法,如下所示:

<code>use App\Models\Article;
use Illuminate\Contracts\Database\Eloquent\Builder;

$unpublishedPosts = Article::query()
    ->where(function (Builder $query): void {
        $query->whereNull('published_at')
            ->orWhere('published_at', '>', now());
    })
    ->get();</code>

我们假设AppModelsArticle模型已应用我们的AppModelsScopesTeamScope

我们将要断言只返回属于用户团队的文章。测试用例可能如下所示:

<code>declare(strict_types=1);

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Article extends Model
{
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('published_at', 'where(function (Builder $query): Builder {
            return $query->whereNull('published_at')
                ->orWhere('published_at', '>', now());
        });
    }

    // ...
}</code>

在上面的测试中,我们正在创建两个团队。然后,我们创建一个属于团队一的用户。我们为团队一创建 3 篇文章,为团队二创建 2 篇文章。然后,我们充当用户并向列出文章的控制器方法发出请求。控制器方法应该只返回属于团队一的 3 篇文章,因此我们通过比较文章的 ID 来断言只返回这些文章。

这意味着我们可以确信全局查询范围正在控制器方法中按预期使用。

结论


在本文中,我们学习了局部查询范围和全局查询范围。我们学习了它们之间的区别,如何创建和使用它们,以及如何为它们编写测试。

希望您现在应该能够自信地在Laravel应用程序中使用查询范围。

以上是学会在Laravel中掌握查询范围的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
PHP中的依赖注入:避免常见的陷阱PHP中的依赖注入:避免常见的陷阱May 16, 2025 am 12:17 AM

DependencyInjection(DI)inPHPenhancescodeflexibilityandtestabilitybydecouplingdependencycreationfromusage.ToimplementDIeffectively:1)UseDIcontainersjudiciouslytoavoidover-engineering.2)Avoidconstructoroverloadbylimitingdependenciestothreeorfour.3)Adhe

如何加快PHP网站:性能调整如何加快PHP网站:性能调整May 16, 2025 am 12:12 AM

到Improveyourphpwebsite的实力,UsEthestertate:1)emplastOpCodeCachingWithOpcachetCachetOspeedUpScriptInterpretation.2)优化的atabasequesquesquesquelies berselectingOnlynlynnellynnessaryfields.3)usecachingsystemssslikeremememememcachedisemcachedtoredtoredtoredsatabaseloadch.4)

通过PHP发送大规模电子邮件:有可能吗?通过PHP发送大规模电子邮件:有可能吗?May 16, 2025 am 12:10 AM

是的,itispossibletosendMassemailswithp.1)uselibrarieslikeLikePhpMailerorSwiftMailerForeffitedEmailSending.2)enasledeLaysBetemailStoavoidSpamflagssspamflags.3)sylectynamicContentToimpovereveragement.4)

PHP中依赖注入的目的是什么?PHP中依赖注入的目的是什么?May 16, 2025 am 12:10 AM

DependencyInjection(DI)inPHPisadesignpatternthatachievesInversionofControl(IoC)byallowingdependenciestobeinjectedintoclasses,enhancingmodularity,testability,andflexibility.DIdecouplesclassesfromspecificimplementations,makingcodemoremanageableandadapt

如何使用PHP发送电子邮件?如何使用PHP发送电子邮件?May 16, 2025 am 12:03 AM

使用PHP发送电子邮件的最佳方法包括:1.使用PHP的mail()函数进行基本发送;2.使用PHPMailer库发送更复杂的HTML邮件;3.使用SendGrid等事务性邮件服务提高可靠性和分析能力。通过这些方法,可以确保邮件不仅到达收件箱,还能吸引收件人。

如何计算PHP多维数组的元素总数?如何计算PHP多维数组的元素总数?May 15, 2025 pm 09:00 PM

计算PHP多维数组的元素总数可以使用递归或迭代方法。1.递归方法通过遍历数组并递归处理嵌套数组来计数。2.迭代方法使用栈来模拟递归,避免深度问题。3.array_walk_recursive函数也能实现,但需手动计数。

PHP中do-while循环有什么特点?PHP中do-while循环有什么特点?May 15, 2025 pm 08:57 PM

在PHP中,do-while循环的特点是保证循环体至少执行一次,然后再根据条件决定是否继续循环。1)它在条件检查之前执行循环体,适合需要确保操作至少执行一次的场景,如用户输入验证和菜单系统。2)然而,do-while循环的语法可能导致新手困惑,且可能增加不必要的性能开销。

PHP中如何哈希字符串?PHP中如何哈希字符串?May 15, 2025 pm 08:54 PM

在PHP中高效地哈希字符串可以使用以下方法:1.使用md5函数进行快速哈希,但不适合密码存储。2.使用sha256函数提高安全性。3.使用password_hash函数处理密码,提供最高安全性和便捷性。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )专业的PHP集成开发工具

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。