介绍一个 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 这个类在 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 协议》,转载必须注明作者和本文链接
 
           Rache1 的个人博客
 Rache1 的个人博客
         
             
             
             
             
             
             
             
             
             
                     
                     
           
           关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号 
 
推荐文章: