在构建Laravel应用程序时,您可能需要编写具有约束条件的查询,这些约束条件在整个应用程序中的多个地方使用。也许您正在构建一个多租户应用程序,并且您必须不断向查询中添加where
约束以按用户的团队进行筛选。或者,也许您正在构建一个博客,并且您必须不断向查询中添加where
约束以筛选博客文章是否已发布。
在Laravel中,我们可以使用查询范围来帮助我们将这些约束条件整洁地保存在一个地方并重复使用。
在本文中,我们将研究局部查询范围和全局查询范围。我们将学习两者之间的区别,如何创建您自己的查询范围,以及如何编写它们的测试。
在阅读完本文后,您应该能够自信地在Laravel应用程序中使用查询范围。
什么是查询范围?
查询范围允许您以可重用的方式在Eloquent查询中定义约束条件。它们通常定义为Laravel模型上的方法,或者作为实现IlluminateDatabaseEloquentScope
接口的类。
它们不仅非常适合在一个地方定义可重用的逻辑,而且还可以通过将复杂的查询约束隐藏在简单的函数调用之后来使您的代码更具可读性。
查询范围分为两种类型:
- 局部查询范围 - 您必须手动将这些范围应用于您的查询。
- 全局查询范围 - 默认情况下,这些范围会应用于模型上的所有查询,前提是已注册该查询。
如果您曾经使用过Laravel内置的“软删除”功能,您可能已经在不知不觉中使用了查询范围。Laravel使用局部查询范围为您提供模型上的withTrashed
和onlyTrashed
等方法。它还使用全局查询范围自动向模型上的所有查询添加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
约束从之前的查询移动到了两个单独的方法中:scopePublished
和scopeNotPublished
。我们现在可以在我们的查询中像这样使用这些范围:
<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_id
为1
的团队,则将为上面的查询生成以下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
全局查询范围,也不会应用该范围。因此,您需要确保在数据库查询中手动应用约束条件。
测试局部查询范围
既然我们已经学习了如何创建和使用查询范围,那么我们将研究如何为它们编写测试。
有多种方法可以测试查询范围,您选择的方法可能取决于您的个人喜好或您正在编写的范围的内容。例如,您可能希望为范围编写更多单元样式的测试。或者,您可能希望编写更多集成样式的测试,这些测试会在诸如控制器之类的上下文中测试范围。
就我个人而言,我喜欢混合使用两者,这样我就可以确信范围正在添加正确的约束,并且范围实际上正在查询中使用。
让我们从前面的示例published
和notPublished
范围开始,并为它们编写一些测试。我们将需要编写两个不同的测试(每个范围一个):
- 一个测试检查
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中文网其他相关文章!

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

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

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

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

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

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

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

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


热AI工具

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

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

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

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

热门文章

热工具

SublimeText3 Linux新版
SublimeText3 Linux最新版

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

记事本++7.3.1
好用且免费的代码编辑器

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具

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