介绍一个 Laravel 中有用的工具类:Fluent

前言

在之前使用 PHPStan 对代码进行静态检查的时候,如果把检查等级提升到 9,在把一个 mixed 类型的值传递给需要明确类型的参数时,就会出现提示。

function foo(int $a): int
{
    return $a * 1;
}

function bar(): mixed
{
    return 'a';
}

$a = bar();
$b = foo($a); // Parameter #1 $a of function foo expects int, mixed given.
var_dump($b);

这是因为我们这里 bar 返回了一个 mixed 类型,而 foo 期待一个 int 类型的参数。

这时候你可能觉得很简单,使用 (int)bar 的返回值强制转换一下不就好了吗?

$a = (int) bar(); // Cannot cast mixed to string.
$b = foo($a);
var_dump($b);

而这样同样会触发另一个问题,因为 mixed 可以是任意类型,它是不可靠的,这样强制转换的话,有可能出现异常。

要解决这个问题,我们有多种方案:

// 1、使用 is_int 进行检查,确保 `$a` 的一定是 int 类型的时候,才进行下面的操作。
$a = bar();
if (is_int($a)) {
    $b = foo($a);
    var_dump($b);
}

// 2、使用注释,人为地告诉 PHPStan,`$a` 是一个 int
/** @var int $a */
$a = bar();
$b = foo($a);
var_dump($b);

// 3、使用 assert(内置或者第三方的,其实跟第一种差不多,是在失败时会抛出异常),此处以 webmozart/assert 举例
use Webmozart\Assert\Assert;
// ...

$a = bar();
Assert::integer($a);
// 注意,如果我们启用了 PHP 的严格模式的话,我们实际还需要在这里对 $a 执行强制转换才行。
$b = foo($a);
var_dump($b);

其中第一种和第三种明显最安全,但是要引入额外的判断,适合一些确实来源类型不确定的场景。

第一种没有添加 else 的处理,而第三种会在检查到不是 int 时抛出异常,这些都需要额外处理。

而第二种则是一种欺骗,但在一些场景中,我们遇到的更多其实是这样的情况。

而在 Laravel 中,我们最常遇到的就是从请求获取参数,大部分时候,我们都在使用 Requestinputget 方法,而这两个方法,返回的值就是 mixed 类型的,而实际上我们可能已经在表单验证中,这个字段只能是字符、数字、布尔。如果我们这里为了糊弄 PHPStan 的话,就得编写类似于上面的代码。


认识 Fluent

那有没有更好的方案?答案是有的,那就是 \Illuminate\Support\Fluent(下文简称 Fluent 或 Fluent)。

Fluent 这个类在 Laravel 中存在了很久,早期它主要承担了一些简单的 get 和类数组访问的操作。

而在 Laravel 中,因为这个 PR# 53665,开始变得与众不同。

根据 PR 的介绍,下面这些方法,从 \Illuminate\Http\Concerns\InteractsWithInput 移动到了 \Illuminate\Support\Traits\InteractsWithDataFluent 使用 InteractsWithData 这个 Trait。

has($key)
only($keys)
exists($key)
filled($key)
hasAny($keys)
missing($key)
except($keys)
anyFilled($keys)
isNotFilled($key)
collect($key = null)
enum($key, $enumClass)
enums($key, $enumClass)
str($key, $default = null)
integer($key, $default = 0)
float($key, $default = 0.0)
string($key, $default = null)
boolean($key = null, $default = false)
date($key, $format = null, $tz = null)
whenHas($key, callable $callback, ?callable $default = null)
whenFilled($key, callable $callback, ?callable $default = null)
whenMissing($key, callable $callback, ?callable $default = null)

注意其中的 enum/enums/str/int/float/string/boolean/date/collect/array 方法,这些方法其实是自 Laravel 9 开始被陆陆续续添加到 InteractsWithInput 中。这些方法会将接收到的参数经过转换,然后返回预定的类型,我们便可以在 Request 上使用 integer/bool 等方法来接收请求参数。

因为 PHPStan 并不检查 vendor 里面的实现,所以,当这些方法的签名(或者存根文件)中声明了将会返回预定的类型,那么 PHPStan 就会选择信任。至此,这些错误将消失,同时我们获得了类型安全的数据。注意,这里框架内部并没有欺骗 PHPStan,而是确确实实地对数据进行了转换,只是在某些情况下,这些转换可能不符合我们的预期。

同时,也意味着我们在使用时也要注意,因为大多数方法,它其实就是使用的强制类型转换,比如在一些为 null 的字段,可能接收到的就不符合预期了。这时候可以使用 blank 或者 filled 方法来检查。

至此,这篇文章已经接近了尾声,我们现在知道 Fluent 类使用了 InteractsWithData 所以支持这里面所有的方法,以及未来可能的方法。

而在 Laravel 11 之后,在其内部也有许多应用:

  • 比如 Http 包的响应中,添加了 fluent 方法,将响应转换成 Fluent,你现在可以使用 Fluent 上的所有方法,比如,它支持嵌套的对象属性获取,就像这样 $response()->fluent()->int('product.id')

  • Request::safe() 方法的返回值就是经过 Fluent 包装的 Request::validated(),通过表单验证的数据。

  • 还提供了 fluent 助手函数,可以方便地把对象/数组包装成 Fluent 对象。

  • 你也可以把 Fluent 应用到你的项目中去。

  • 在上周刚刚发布的 Laravel 12.19.0 版本中,还为模型添加了 AsFluent 这个 Cast,用来方便地把模型上的 json 字段,在读取时转换为 Fluent 对象。

除此之外,你可能还意外发现,Fluent 还有一些奇怪的应用:

比如在迁移中,实际上大部分类型方法比如(string/integer/mediumText)等,返回的都是一个派生自 Fluent 的对象,因为这里用 Fluent 的另一个特性,它在内部扩展了 __call 方法,当你在上面调用一些不存在的方法的时候,它实际上会把方法名作为 key、参数作为值保存到实例中,然后继续返回自身,这样用来保存一些字段的配置项,在最终生成迁移语句的时候读取这上面的配置即可。

再比如 \Illuminate\Foundation\Bus\Dispatchable 这个 Trait,我们一般会在 Event 中使用,它包含了两个比较有用的方法,dispatchUnlessdispatchIfdispatchUnless 的第一个参数如果评估为 false,则投递这个 EventdispatchIf 则相反。

public static function dispatchIf($boolean, ...$arguments)
{
    if ($boolean instanceof Closure) {
        $dispatchable = new static(...$arguments);

        return value($boolean, $dispatchable)
            ? static::newPendingDispatch($dispatchable)
            : new Fluent;
    }

    return value($boolean)
        ? static::newPendingDispatch(new static(...$arguments))
        : new Fluent;
}

dispatch 实际返回的其实是 \Illuminate\Foundation\Bus\PendingDispatch 这个对象,在这上面我们可以调用 onConnection/onQueue/delay/afterCommit 等等方法,而在评估为不派发时,这里实际就会返回一个 Fluent 对象,也是利用了前面提到的这个特性,让我们在评估为失败时调用这些方法也不至于报错(方法不存在)。


其他应用

除此之外,Laravel 中还有一种类似于 Fluent 的应用,那便是 Configconfig 作为我们经常使用的方法,我们经常使用 config('app.url') 或者类似的方式来获取配置,但是因为 config 函数的特殊性,在它没有接收参数或者第一个参数为 null 的时候,他其实会返回一个 Repository 对象(原文字面意思理解为Config对象,但实际上返回的是Illuminate\Config\Repository实例),而在传入数组时,则又会返回 void。虽然对于 PHPStan 你可以标注完整的返回类型,但是它还是无法猜测你获取到的配置项的值。

Config 现在也为我们提供了如下方法:string/integer/float/boolean/array。现在,你可以使用这些方法获得预定类型的配置项值,但是,它不是一个 Fluent,因为它会检查获取出来的配置项值,如果不满足对应的类型,它就会抛出异常,如果满足,就会转为指定的类型,就像我们前面写的第三种方式类似,而 Fluent 它是会进行转换的,而不检查。


结语

最后,值得一提的是,如果你使用 PHPStan 进行类型检查,那么你其实也不必一味追求更好的检查等级,选择合适的即可(比如 level 8,就不会检查上述提到的 mixed)。

虽然更严格的检查会驱使你写出更严谨的代码,但是往往也要耗费更多的精力,但是它可以在未来帮你节约不少时间。


本文使用 Gemini 进行了排版优化、专有名词处理、错别字、错误用词处理。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

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