将 null 传递给不可为 null 的内部函数参数 - 将现有代码库更新到 php 8.1

Passing null to non-nullable internal function parameters - Updating Existing Code Base to php 8.1

提问人:mseifert 提问时间:1/4/2022 最后编辑:outismseifert 更新时间:12/8/2022 访问量:5584

问:

我刚刚开始升级我的代码以兼容 php 8.1。我有很多代码,我将潜在的空值传递给内部函数。

if (strlen($row) > 0) {
   ...
} 

其中$row来自可能具有 null 值的源(例如查询)。这可能会生成弃用警告;在这种情况下:

已弃用:strlen():不推荐将 null 传递给字符串类型的参数 #1 ($string)

我正在寻找最简单、最省时的方法来处理升级此代码,例如修复可以进行全局搜索和替换的修复。似乎将我传递给内部函数的变量进行类型转换可以在不更改功能的情况下工作。

error_reporting(E_ALL);
$row = null;

if (strlen((string) $row) > 0) {
   ...
}

除了以这种方式编码的道德方面之外,这种方法对内部函数是否存在问题?有没有更好的方法(除了完全重写代码和以不同的方式处理 null)?我更喜欢这个向后兼容 v7.4 的解决方案,尽管我可能会接受 8.0 兼容性。

我知道我的用户定义的函数还有其他选择。

PHP-8.1

评论


答:

6赞 IMSoP 1/5/2022 #1

如果您明确尝试处理 的情况,那么稍微干净一点的修复方法是使用“null 合并运算符”。nullstrlen($row ?? '')

在大多数情况下,这两者可能是等价的,但实际上,如果值是可以转换为字符串的其他类型,它们的行为会有所不同:strict_types=1

declare(strict_types=1);
$row = 42;
echo strlen($row); // TypeError: must be of type string, int given
echo strlen((string) $row); // Succeeds, outputting '2'
echo strlen($row ?? ''); // TypeError: must be of type string, int given

另一方面,请注意,运算符是基于 ,而不是 ,因此未定义的变量的行为会有所不同:??isset=== null

declare(strict_types=1);
$row = [];
echo strlen($row['no_such_key']); // Warning: Undefined array key; TypeError: must be of type string, null given
echo strlen((string) $row['no_such_key']); // Warning: Undefined array key; outputs '0'
echo strlen($row['no_such_key'] ?? ''); // No warning, just outputs '0'

如果你关心这种情况,那么与旧行为最直接等效的代码就更详细了:

echo strlen($row === null ? '' : $row);

评论

0赞 mseifert 1/9/2022
感谢您提供详细的用例场景。我注意到没有通过 an 没有抛出 8.1 的警告。你知道为什么吗?似乎 php 8 并没有在所有情况下都强制执行严格类型。declare(strict_types=1);int
0赞 IMSoP 1/9/2022
@mseifert 这正是strict_types声明的用途 - 如果值可以安全地转换为字符串,则允许这样做。对于默认模式,更好的名称是“scalar_types=cast”,对于“严格”模式,更好的名称是“scalar_types=error”。这在 8.1 中没有改变,只是对 null 的处理,它从未受到该设置的影响。
0赞 mseifert 1/9/2022
但是,随着strict_types的声明,我得到了.这是否意味着 42 无法安全地转换为字符串?Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, int given
0赞 mseifert 1/9/2022
似乎无论如何,我只需要担心值,因为它在没有警告或整数错误的情况下工作。这准确吗?strlenNULL
0赞 IMSoP 1/9/2022
@mseifert 同样,这就是strict_types设置的用途 - 在默认模式下,它会将 int 转换为字符串并继续;在模式下,它会引发错误。这就是为什么它被称为“严格”,因为它对你被允许做的事情更严格。42'42'strict_types=1
9赞 Craig Francis 10/14/2022 #2

回答有关“处理升级此代码的最简单、最省时的方法”的问题。

简而言之,你不能。


首先,介绍一些背景...

大约 15% 的开发人员使用 strict_types=1,因此大多数开发人员都不这样做。

您可以暂时忽略此问题(弃用),但是 PHP 9.0 会将其设置为致命类型错误,从而导致很多问题。

也就是说,您仍然可以将字符串与 NULL 连接起来:

$name = NULL;
$a = 'Hi ' . $name;

您仍然可以将 NULL 与空字符串进行比较:

if ('' == NULL) {
}

您仍然可以使用 NULL 进行计算(它仍被视为 0):

var_dump(3 + '5' + NULL); // Fine, int(8)
var_dump(NULL / 6); // Fine, int(0)

您仍然可以打印/回显 NULL:

print(NULL);
echo NULL;

你仍然可以将 NULL 传入,并用 强制为空字符串,例如sprintf()%s

sprintf('%s', NULL);

你仍然可以强制其他值(遵循规则),例如

strlen(15);
htmlspecialchars(1.2);
setcookie('c', false);

我假设从一开始就开始,NULL 胁迫一直这样工作,并且还记录在案:

  • To String: “null 始终转换为空字符串。”
  • To Integer: “null 始终转换为零 (0)”。
  • To Float:“对于其他类型的值,通过先将值转换为 int 然后转换为 float 来执行转换”
  • 布尔值:“当转换为布尔值时,以下值被认为是假的 [...]特殊类型 NULL”

无论如何,要修复...第一部分,它试图找到您需要更新的代码。

只要将 NULL 传递给这些函数参数之一,就会发生这种情况。

至少有 335 个参数受此影响

还有另外 104 个有点问题;和 558 其中 NULL 有问题,您应该修复这些问题,例如 .define(NULL, 'value')

诗篇是我能找到的唯一能够帮助解决这个问题的工具。

诗篇需要处于非常高的检查水平(1、2或3)。

而且,您不能使用基线来忽略问题(开发人员在现有项目中引入静态分析时使用的一种技术,因此它只检查新代码/编辑代码)。

如果你以前没有使用过静态分析工具(别担心,建议只有 33% 的开发人员使用);然后期望花费大量时间修改您的代码(从最宽松的第 8 级开始,然后慢慢完成)。

我无法让 PHPStan、Rector、PHP CodeSniffer、PHP CS Fixer 或 PHPCompatibility 找到这些问题(结果);Juliette 已经证实,让 PHPCompatibility 解决这个问题将“非常困难”,因为它“不可靠地嗅探”(来源)。


一旦你找到了每一个问题,第二部分是编辑。

不容易引起问题的地方是更换水槽,例如

example_function(strval($name));
example_function((string) $name);
example_function($name ?? '');

或者,您可以尝试跟踪变量的源,并尝试首先阻止将其设置为 NULL。

以下是一些非常常见的 NULL 来源:

$search = (isset($_GET['q']) ? $_GET['q'] : NULL);
 
$search = ($_GET['q'] ?? NULL); // Fairly common (since PHP 7)
 
$search = filter_input(INPUT_GET, 'q');
 
$search = $request->input('q'); // Laravel
$search = $request->get('q'); // Symfony
$search = $this->request->getQuery('q'); // CakePHP
$search = $request->getGet('q'); // CodeIgniter
 
$value = mysqli_fetch_row($result);
$value = json_decode($json); // Invalid JSON, or nesting limit.
$value = array_pop($empty_array);

其中一些函数需要第二个参数来指定默认值应该是什么,或者您可以更早地使用...但要小心,你的代码可能会专门检查 NULL via ,你不想破坏它。strval()($a === NULL)

许多开发人员不会意识到他们的某些变量可能包含 NULL - 例如,期望 (他们创建的)始终提交所有输入字段;由于网络问题、浏览器扩展、用户在浏览器中编辑 DOM/URL 等原因,可能不会发生这种情况。<form>


一年中的大部分时间里,我一直在研究这个问题。

我开始编写两个RFC来尝试解决这个问题。首先是更新一些函数以接受 NULL(这并不理想,因为它会让使用 strict_types 的开发人员感到不安);第二个RFC是允许NULL在这种情况下继续被胁迫......但我没有把它付诸表决,因为我刚刚收到了一大堆负面反馈,而且我不希望将来引用这种拒绝来解释为什么这个问题无法解决(虽然最初的更改几乎没有讨论过,但这个会)。

NULL 似乎被区别对待,因为它从未被视为“标量值”——我认为很多开发人员并不关心这种区别,但它时不时地出现。

对于我一直在合作的开发人员,大多数人都忽略了这个问题(希望它以后能得到解决,这可能不是最好的主意);例如:

function ignore_null_coercion($errno, $errstr) {
  // https://github.com/php/php-src/blob/012ef7912a8a0bb7d11b2dc8d108cc859c51e8d7/Zend/zend_API.c#L458
  if ($errno === E_DEPRECATED && preg_match('/Passing null to parameter #.* of type .* is deprecated/', $errstr)) {
    return true;
  }
  return false;
}
set_error_handler('ignore_null_coercion', E_DEPRECATED);

一个团队正试图坚持所有事情,例如.但一年多后,他们仍然发现了问题(他们表示使用 8.1 alpha 1 进行测试)。strval()trim(strval($search))

我正在考虑的另一个选择是创建一个库,在命名空间下将所有这些 ~335 函数重新定义为可为 null;例如:

namespace allow_null_coercion;

function strlen(?string $string): int {
    return \strlen(\strval($string));
}

然后,开发人员将包含该库,并自行使用命名空间:

namespace allow_null_coercion;

$search = $request->input('q'); // Could return NULL

// ...

echo strlen($search);

评论

0赞 kiatng 8/30/2023
你的第二个RFC对我来说很有意义。您在 NULL 强制中描述的所有问题都发生在 OpenMage 项目上。我们从去年开始研究 PHP 8 .1 的兼容性,但 NULL 强制问题不断出现在意想不到的地方。对于基于松散类型的旧项目,升级 PHP 并不容易。因此,不幸的是,您的 RFC 没有向前推进。
0赞 Michael Bolli 12/8/2022 #3

Rector 可能是您快速解决问题的最佳选择。它有解决这个问题的规则,它也通过将其转换为字符串来修复它:NullToStrictStringFuncCallArgRector

-     mb_strtolower($value);
+     mb_strtolower((string) $value);