什么场景下使用 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://stefrouschop.nl/when-to-use-lara...

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

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 8

辛苦翻译的同学了,但不必要翻译这些干货不多的文章。

1年前 评论
徐小花 (楼主) 1年前
L学习不停 (作者) 1年前
sanders

这确实是个困扰我的问题,大家都在什么时候使用全局作用域?

如果是楼主描述的应用场景,我不会在模型加载的时候就应用全局作用域。因为模型可能在定时任务或队列中使用,这时往往不会获取到授权的用户信息。与此相对的,我会在中间件中应用全局作用域。

1年前 评论
还不出来 1年前
Anjaxs 1年前

数据库敏感字段加密的时候

1年前 评论
sanders

放一下我的应用方法:

业务需求是:对于未授权用户只展示“公开”的团队数据,对于已授权用户只显示“公开”的或有加入数据(包括申请未通过)或属主是自己的团队信息。

namespace App\Http\Middleware\Team;

use App\Models\Team;
use App\Services\Visibility\VisibilityBase;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class VisibilityScopeMiddleware
{
    /**
     * 作用域名
     */
    public const GLOBAL_SCOPE = 'visibility';

    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param Closure(Request): (Response) $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        $user   = $request->user();

        if (!$user) {
            Team::addGlobalScope(
                self::GLOBAL_SCOPE,
                fn(Builder $query) => $query->where('visibility', VisibilityBase::PUBLIC)
            );

            return $next($request);
        }

        $userId = data_get($user, 'id');

        Team::addGlobalScope(
            self::GLOBAL_SCOPE,
            fn(Builder $query) => $query->where(
                fn(Builder $query) => $query->where('visibility', VisibilityBase::PUBLIC)
                    ->orWhere('owner_id', $userId)
                    ->orWhereHas(
                        'teamUsers',
                        fn(Builder $query) => $query->where('user_id',$userId)
                    )
            )
        );

        return $next($request);
    }
}

这个中间件会加在授权中间件的后面。

1年前 评论

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