支持 Laravel.io 的持续开发 →

在 Laravel 中使用 "Conditionable" 特性

17 Apr, 2024 9 min read

简介

条件是任何代码库的组成部分。如果不能条件性地执行代码(例如使用 ifelse),我们的软件将非常有限,无法完成更多操作。实际上,它们是你开始编程后最早学习的东西之一。

但使用 ifelse 有时可能会阻止你连缀方法调用。

在这篇文章中,我们将简要介绍如何使用 Laravel 的 Illuminate\Support\Traits\Conditionable 特性来帮助你连缀方法调用,并将 when 方法添加到你自己的 PHP 类中。

什么是 Conditionable 特性?

为了了解 Conditionable 特性做了什么,让我们看看一些代码示例,然后重写使用特性的代码。

你是否经常写出像这样的代码,在 Eloquent 查询中条件性地添加一个 where 子句?

$query = User::query()
    ->where(...)
    ->where(...);

if (Auth::user()->isAdmin()) {
    $query->where(...);
}

$users = $query->get();

从最初 glance 来看,由于代码分为三个独立的块,可能无法立即看出所有代码是否都与查询相关。我们可能会在进行 if 块的内部做一些操作,这些操作并未改变查询,而是运行了额外的逻辑(尽管我们在此示例中没有这样做)。我更希望能够保持查询在一个单独的链中。这只是我个人的观点。

旁注:你可能已经注意到我在查询的开始使用了 ::query()。如果你以前没见过,我有一篇关于这是做什么的文章:[在 Laravel Eloquent 查询中使用 'query()'](https://ashallendesign.co.uk/blog/using-query-in-laravel-eloquent-queries).

如果是链式调用,它可能看起来更像这样

$users = User::query()
    ->where(...)
    ->where(...)
    ->when(
        Auth::user()->isAdmin(),
        fn ($query) => $query->where(...),
    })
    ->get();

从上面的代码示例中我们可以看到,我们已经用 when 方法替换了 if 块。

我们能够访问这个方法,是因为我们用来构建 Eloquent 查询的 Illuminate\Database\Eloquent\Builder 类使用了 Illuminate\Database\Concerns\BuildsQueries 特性,该特性又使用了 Illuminate\Support\Traits\Conditionable 特性。

如果传递给 when 方法的第一个参数的结果为真,则执行第二个参数(闭包或箭头函数)。如果结果是假的,则不执行闭包。

我不知道你是否也有同样的感受,但我发现这种方式更易于阅读和理解。我的大脑可以立即看懂代码,并理解它与同一事物(在这种情况下是查询)有关。我猜这是因为代码紧凑并遵循了 Gestalt 原则中的距离

Laravel 代码库中有几个其他地方可以使用 when 方法,例如

但如果我们想在自定义类中使用它怎么办呢?

让我们看看如何自己使用 Conditionable 特性。

添加 Conditionable 特性

开始使用 Conditionable 特性非常简单。你只需要将它添加到你的类中。

让我们假设我们有一个可以用来构建和存储 PDF 报告的示例类:

declare(strict_types=1);

namespace App\Services;

final class ReportBuilder
{
    private bool $includeCharts = false;

    private bool $includeTables = false;

    private string $colour;

    public function buildPdfReport(): string
    {
        // Build the PDF report.
        // Save it in storage (e.g. - S3).
        // Return the URL of the PDF report for downloading.
    }

    public function includeCharts(): self
    {
        $this->includeCharts = true;

        return $this;
    }

    public function includeTables(): self
    {
        $this->includeTables = true;

        return $this;
    }

    public function darkMode(): self
    {
        $this->colour = 'dark';

        return $this;
    }

    public function lightMode(): self
    {
        $this->colour = 'light';

        return $this;
    }
}

我们可能想要在像 控制器 这样的地方使用这段代码:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\ReportBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ReportController extends Controller
{
    public function __invoke(Request $request, ReportBuilder $reportBuilder): JsonResponse
    {
        if ($request->boolean('includeCharts')) {
            $reportBuilder->includeCharts();
        }

        if ($request->boolean('includeTables')) {
            $reportBuilder->includeTables();
        }

        $request->boolean('darkMode')
            ? $reportBuilder->darkMode()
            : $reportBuilder->lightMode();

        $url = $reportBuilder->buildPdfReport();

        return response()->json([
            'url' => $url,
        ]);
    }
}

在上面的例子中,我们使用 ifelse 语句来在 ReportBuilder 类上条件性地调用方法。在构建完 PDF 报告后,我们在 JSON 响应中返回 PDF 报告的 URL。

让我们更新我们的 ReportBuilder 类以使用 Conditionable 特性

declare(strict_types=1);

namespace App\Services;

use Illuminate\Support\Traits\Conditionable;

final class ReportBuilder
{
    use Conditionable;
    
    // The rest of the class...
}

从这里,我们可以重构我们的控制器以使用 when 方法

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\ReportBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ReportController extends Controller
{
    public function __invoke(Request $request, ReportBuilder $reportBuilder): JsonResponse
    {
        $url = $reportBuilder
            ->when(
                value: $request->boolean('includeCharts'),
                callback: fn ($builder) => $builder->includeCharts(),
            )->when(
                value: $request->boolean('includeTables'),
                callback: fn ($builder) => $builder->includeTables(),
            )->when(
                value: $request->boolean('darkMode'),
                callback: fn ($builder) => $builder->darkMode(),
                default: fn ($builder) => $builder->lightMode(),
            )->buildPdfReport();

        return response()->json([
            'url' => $url,
        ]);
    }
}

从上面的代码示例中我们可以看到,我们已经用 when 方法替换了 ifelse 语句。

你可能还注意到,我们也向第三个 when 方法传递了一个 default 参数,该参数确定了如果值是假的会发生什么。如果值是假的,则调用 lightMode 方法。

注意:为了使代码示例简短易懂,我没有为箭头函数添加返回类型和类型提示。在实际应用中,我建议添加这些以提供更严格类型安全和清晰度。

传递值给 when 回调

虽然我们没有在上面的例子中涵盖它,但你可以在回调函数(callback 参数)中访问第一个参数(value 参数)的结果。如果你需要在回调函数中使用这个值,这会非常有用。

例如,让我们看看这个代码块:

Post::query()
    ->when(
        value: Auth::user(),
        callback: function (Builder $query, User $user) {
            // We can access the $user variable here...
            $query->where('user_id', $user->id);
        },
    )

在上面的例子中,如果有已登录的用户(使用 Auth::user() 访问),它将被作为第二个参数传递给回调函数。

unless 方法

除了 when 方法,Conditionable 特性还提供了一个 unless 方法。这个方法与 when 方法相反。如果值是假的,则执行回调函数。如果值是真的,则不执行回调函数。

例如,我们可能想写一个查询来获取用户可以查看的一些博客文章。如果用户是管理员,则我们想获取所有文章。如果用户不是管理员,则我们只想获取已发布的文章

Post::query()
    ->unless(
        value: Auth::user()->isAdmin(),
        callback: function (Builder $query) {
            $query->where('status', 'published');
        },
    )

我个人觉得unless方法相当难以阅读和理解。我花费了一些时间才搞清楚正在使用的逻辑,所以我更喜欢使用when方法。

但这并不是在批评unless方法。这仅仅是因为我的大脑工作方式。你可能与我相反,觉得unless方法更容易理解和使用。

我应该使用Conditionable特性吗?

与开发中的许多事情一样,你是否有意愿使用Conditionable特性完全取决于个人喜好。

对我而言,我发现当我需要链式调用方法时,使用when方法会使代码更容易阅读和理解。它把所有与同一事物相关的内容都放在了一个地方。

然而,的确有某些情况下,使用ifelse语句可能更为合适。例如,如果你有大量需要执行的逻辑,那么使用if语句可能更好。你不想有一个难以阅读和理解的大闭包。或者,可能只是因为你觉得ifelse语句更容易阅读和理解。

因此,这确实是一件你应该根据具体情况考虑的事情。这里没有正确或错误答案。只需确保它对你和你的团队都是舒适的,并且能够理解。

如果你选择将代码从使用ifelse语句重构成使用when方法,我建议确保你有一些测试覆盖了重构的代码。这将帮助你确保重构后的代码仍然按预期工作,并且在过程中没有破坏任何东西。

结论

希望这篇文章能让你快速了解Conditionable特性是什么以及你如何在你的代码库中使用它。

如果你喜欢阅读这篇帖子,我很乐意听到你的反馈。同样,如果你有任何改进未来帖子的建议,我也很乐意听到。

你可能还感兴趣阅读我的220多页电子书《Battle Ready Laravel》,其中更深入地讨论了类似主题。

或者,你可能想阅读我的其他440多页电子书《Using APIs in Laravel》,它教你如何使用Laravel从其他服务中消费API。

如果你有兴趣在每次我发表新帖子时收到更新,请在下面免费注册我的通讯。

继续构建精彩的事物!🚀lara

最后更新于3个月前。

driesvints 喜欢这篇文章

1
喜欢这篇文章? 让作者知道,给他们一个掌声!
ash-jc-allen (Ash Allen) 我是来自英国普雷斯顿的自由职业Laravel web开发者。我维护Ash Allen Design博客,并参与了许多酷炫和激动人心项目 🚀

你可能还喜欢的文章

2024年3月11日

如何利用Larastan将你的Laravel应用从0到9进行优化

在Laravel应用执行前发现错误是可能的,多亏了Larastan,这是一个...

阅读文章
2024年7月19日

无需特质规范化API响应

我注意到大多数用于API响应的库都是使用特质实现的,并且...

阅读文章
2024年7月17日

通过Discord通知收集你的Laravel项目的反馈

如何在Laravel项目中创建反馈模块,并在收到消息时收到Discord通知...

阅读文章

我们想感谢以下这些 令人惊叹的公司 为我们提供支持

您的标志在这里吗?

Laravel.io

Laravel用于解决问题、知识分享和社区建设的门户。

© 2024 Laravel.io - 版权所有。