介绍一个 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 中,我们最常遇到的就是从请求获取参数,大部分时候,我们都在使用 Request
的 input
、get
方法,而这两个方法,返回的值就是 mixed
类型的,而实际上我们可能已经在表单验证中,这个字段只能是字符、数字、布尔。如果我们这里为了糊弄 PHPStan 的话,就得编写类似于上面的代码。
认识 Fluent
那有没有更好的方案?答案是有的,那就是 \Illuminate\Support\Fluent
(下文简称 Fluent 或 Fluent
)。
Fluent
这个类在 Laravel 中存在了很久,早期它主要承担了一些简单的 get
和类数组访问的操作。
而在 Laravel 中,因为这个 PR# 53665,开始变得与众不同。
根据 PR 的介绍,下面这些方法,从 \Illuminate\Http\Concerns\InteractsWithInput
移动到了 \Illuminate\Support\Traits\InteractsWithData
,Fluent
使用 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
中使用,它包含了两个比较有用的方法,dispatchUnless
和 dispatchIf
,dispatchUnless
的第一个参数如果评估为 false
,则投递这个 Event
,dispatchIf
则相反。
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
的应用,那便是 Config
。config
作为我们经常使用的方法,我们经常使用 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 协议》,转载必须注明作者和本文链接
推荐文章: