返回

PHPStan 报 StdClass 属性类型不足?5 种解决方法

php

PHPStan 报 Insufficient Property Type in StdClass,咋整?

遇到一个 PHPStan 报错,说 StdClass 的属性类型不足。 挺烦人的,具体啥情况呢,看下这段代码:

return State::all()
    ->map(fn (State $state) => (object) [
        'value' => $state->id,
        'label' => $state->name,
    ])
    ->sortBy('label')
    ->prepend((object) [
        'value' => null,
        'label' => '',
    ])
;

这段代码想动态构建一个下拉框的选项,并且在最前面加一个空的选项。 但是 PHPStan 报错了:

Parameter #1 $value of method
  Illuminate\Support\Collection<int,object{
    value: int, label: string}&stdClass>::prepend()
  expects   
    object{value: int, label: string}&stdClass,
    object{value: null, label: string}&stdClass given.                                   
💡 Property ($value) type int does not accept type null.

我估计 PHPStan 觉得,value 属性一开始被赋值成了一个整数,所以后面就只能是整数了。但我们这第一条数据 value 明确是 null 呀!

为啥会这样?

根本原因在于 PHPStan 的类型推断机制。 当我们用 map 方法处理 State 集合时, PHPStan 会根据第一个元素的 $state->id (通常是整数) 推断出 value 属性的类型是 int。 之后,prepend 方法尝试插入一个 valuenull 的对象, 这与之前推断出的 int 类型冲突,就报错了。

更广义的说,当我们创建一个 stdClass 对象,并且第一次给它的某个属性赋值时, PHPStan 就”记住了“这个类型. 后面如果再给这个属性赋一个类型不符的值, 就会报这个错。

咋解决?

好几种办法,一个个说。

1. 类型转换 (Casting)

最简单的, 直接把 null 转换成整数。

return State::all()
    ->map(fn (State $state) => (object) [
        'value' => $state->id,
        'label' => $state->name,
    ])
    ->sortBy('label')
    ->prepend((object) [
        'value' => (int) null, // 这里把 null 转换成整数
        'label' => '',
    ])
;

原理: 强制类型转换把 null 变成了 0,符合了 PHPStan 推断的 int 类型。

优点: 简单粗暴,改动最小。

缺点: 如果你的业务逻辑里,valuenull 有特殊含义, 这种方式就改变了语义。

2. 使用 array 替代 stdClass

既然问题出在 stdClass,那干脆不用它,用数组行不行?

return State::all()
    ->map(fn (State $state) => [
        'value' => $state->id,
        'label' => $state->name,
    ])
    ->sortBy('label')
    ->prepend([
        'value' => null,
        'label' => '',
    ])
;

原理: PHP 的数组是动态类型的,可以接受不同类型的值。PHPStan 对数组的处理方式和对象不同。

优点: 简单,大多数情况下都能解决问题。

缺点: 丢失了对象的语义,如果后续有其他地方依赖于对象的结构, 可能会有问题。如果整个程序是严格使用object传递参数,可能后续其他类型检查时会出问题。

3. 使用辅助函数

把创建对象的操作封装到一个辅助函数里。

function createStateOption($value, $label) {
    return (object) [
        'value' => $value,
        'label' => $label,
    ];
}

return State::all()
    ->map(fn (State $state) => createStateOption($state->id, $state->name))
    ->sortBy('label')
    ->prepend(createStateOption(null, ''))
;

原理: PHPStan 对函数返回值的类型推断和直接在匿名函数里创建对象不同. 通过一个独立的函数, PHPStan 更容易推断出正确的类型。经过我的实验,直接将对象构建放到独立的函数里, PHPStan 推理就不会出错了.

优点: 代码更清晰,可读性更好。

缺点: 需要额外定义一个函数。

4. 使用联合类型 (Union Types) 和 Docblocks (进阶)

如果既想保留 stdClass, 又想告诉 PHPStan value 可以是 intnull,那可以用联合类型。

/**
 * @param int|null $value
 * @param string $label
 * @return object{value: int|null, label: string}
 */
function createStateOption2($value, $label)
{
     return (object) [
        'value' => $value,
        'label' => $label
    ];
}
return State::all()
        ->map(fn(State $state) => createStateOption2($state->id, $state->name))
        ->sortBy('label')
        ->prepend(createStateOption2(null, ''))
;

或者我们不改变函数,而是增加collection的提示

/** @var \Illuminate\Support\Collection<int, object{value: int|null, label: string}> $states */
$states =  State::all()
    ->map(fn (State $state) => (object) [
        'value' => $state->id,
        'label' => $state->name,
    ]);
    
   return  $states->sortBy('label')
    ->prepend((object) [
        'value' => null,
        'label' => '',
    ]);

原理:

  • 联合类型: int|null 表示 value 可以是整数或者 null
  • Docblocks: 通过 @param, @return 等注释,可以明确告诉 PHPStan 函数参数和返回值的类型。包括更详细的object{}定义
  • 注意,使用了 object{} 必须依赖 psalm 插件才可以

优点:

  • 类型信息更准确,能更好地利用 PHPStan 的静态分析能力。
  • 代码的意图更明确。

缺点: 需要写 Docblocks, 对于不熟悉的人来说有点门槛。而且代码会略微繁琐一些.

5.直接屏蔽对应行的phpstan检查(不建议,但是最快)

return State::all()
            ->map(fn (State $state) => (object) [
                'value' => $state->id,
                'label' => $state->name,
            ])
            ->sortBy('label')
            // @phpstan-ignore-next-line
            ->prepend((object) [
                'value' => null,
                'label' => '',
            ])
        ;

直接屏蔽该行,简单直接,不到万不得已,还是正规处理比较好。

总结

  • 这个问题核心是静态分析时, 对象初始赋值决定了后续属性类型推断, nullint冲突, 我们可以把null强转int.
  • 平时为了规避问题,可以使用array, 规避掉stdClass.
  • 或者抽离出独立的构建函数,避免直接构建object, phpstan 在此场景分析表现更好.
  • 如果要强化类型,可以结合联合类型,类型提示, Docblocks.

要选哪个方案,取决于具体情况. 如果是简单场景, 类型转换或用数组就行。 如果需要更严格的类型检查, 或者要保留对象的语义, 可以用辅助函数或联合类型。根据项目需求,选择最适合的方式最重要.