什么场景下使用 Laravel 的模型全局作用域?
Laravel 全局作用域很棒,但我没有看到它们被广泛使用。 相反,我看到很多局部范围被用来实现同样的事情。 通过正确实施全局范围,代码和安全性将得到极大改善。 让我用一个简单的例子来说明这一点。
局部范围方式
在我们的代码库中,我们有一个模型 Transaction
为我们的用户存储交易。 如果我们想从数据库中获取登录用户的事务,我们可以这样做:
$transactions = Transaction::where('user_id', auth()->id())->get();
由于我们会在整个代码库中大量使用它,因此在 Transaction
模型中创建一个局部范围是有意义的,如下所示:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Transaction extends Model
{
// ...
public function scopeForLoggedInUser($query): void
{
$query->where('user_id', auth()->id());
}
}
有了这个局部范围,我们可以像这样进行查询:
$transactions = Transaction::forLoggedInUser()->get();
这是一个很好的 DRY (Don’t Repeat Yourself) 重构,可以稍微清理一下。
一个你应该问自己的问题
局部作用域很棒,我尽可能地使用它们来保持代码干爽。 但是当我创建局部范围时,我学会了问自己这个问题:「这个模型的大多数查询都会使用这个局部范围吗?」
如果答案是否定的,请保留局部范围。 使用它很有意义。
当答案是肯定的
这将是考虑全局范围时的重点。 全局范围始终应用于给定模型的所有查询。 你可以简单地通过创建一个实现了Illuminate\Database\Eloquent\Scope
的类来创建一个全局作用域。
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TransactionsForLoggedInUserScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('user_id', auth()->id());
}
}
之后,你应该让你的模型了解全局范围:
<?php
namespace App\Models;
use App\Models\Scopes\TransactionsForLoggedInUserScope;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
// ...
protected static function booted(): void
{
static::addGlobalScope(new TransactionsForLoggedInUserScope());
}
}
如果你现在要获取所有交易,它只会返回登录用户的交易。
Transaction::all();
删除全局范围
在某些情况下,你想创建一个不应用全局范围的 Transaction
查询。 例如,在管理概览或某些全局统计计算中。 这可以通过使用 withoutGlobalScope
方法来完成:
Transaction::withoutGlobalScope(TransactionsForLoggedInUserScope::class)->get();
匿名全局范围
还有一种方法可以在不使用额外文件的情况下创建全局范围。 你可以像这样包含查询,而不是指向 TransactionsForLoggedInUserScope
类:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
// ...
protected static function booted(): void
{
static::addGlobalScope('for_logged_in_users', function (Builder $builder) {
$builder->where('user_id', auth()->id());
});
}
}
就我个人而言,我不喜欢匿名全局作用域,因为如果查询有点复杂,它会使你的模型膨胀。为了在整个代码库中保持一致,我总是倾向于使用外部文件全局范围,即使查询像我们的示例中那样简单。
全局范围的缺点
我不会骗你,如果你正在开发一个具有全局范围的代码库,却没有意识到它们,你可能会陷入毫无意义的境地。有一天(非常糟糕),这让我对自己的职业选择提出了质疑😂. 修补时会得到完全出乎意料的结果。如果这种情况发生过一次,那么你已经了解到,在你不太熟悉的代码库中工作时,将 withoutGlobalScopes
方法放在工具带中是很好的。
安全
如果你的应用程序的安全性依赖于全局范围,那么当你或其他开发人员将来对它进行更改时,这是危险的。想象一下,在我们的例子中,有人会删除或更改全局范围。那么用户交易的所有结果都将是完全错误的!这就是为什么为全局作用域实现测试非常重要,所以这是不可能发生的。我们的示例的典型(PEST)测试如下所示:
it("should only get the transactions for the logged-in user", function (){
$user = User::factory()->create();
$otherUser = User::factory()->create();
$transaction_1 = Transaction::factory()->create(['user_id' => $user->id]);
$transaction_2 = Transaction::factory()->create(['user_id' => $otherUser->id]);
$this->actingAs($user);
expect(Transaction::all())->toHaveCount(1)
->and($transaction_1->is(Transaction::first()));
});
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
辛苦翻译的同学了,但不必要翻译这些干货不多的文章。
这确实是个困扰我的问题,大家都在什么时候使用全局作用域?
如果是楼主描述的应用场景,我不会在模型加载的时候就应用全局作用域。因为模型可能在定时任务或队列中使用,这时往往不会获取到授权的用户信息。与此相对的,我会在中间件中应用全局作用域。
数据库敏感字段加密的时候
放一下我的应用方法:
业务需求是:对于未授权用户只展示“公开”的团队数据,对于已授权用户只显示“公开”的或有加入数据(包括申请未通过)或属主是自己的团队信息。
这个中间件会加在授权中间件的后面。