开发维护PHP时的问题总结

开发维护PHP时的问题总结

本博客总结了在维护老旧PHP项目时遇到的常见问题,特别是关联数组的使用。作者讨论了如何处理未定义的数组键、空字段判断,以及序列化空数组时的问题。通过提供正确的代码示例和防御性编程技巧,作者指出PHP关联数组与其他语言结构的差异,并提出了解决方案和预防措施。

开发维护PHP项目时的问题总结

由于工作原因,不得不接触一些古早代码,对其进行拓展和维护,在此期间碰到了许多坑坑绕绕。在这里记录一下,产生的效果和对应的解决方案与避免方法。

关联数组相关问题

PHP 的「对象」或「结构体」概念的相关物是 array,但是 PHP 的 array 可以是正常数组,也可以是关联数组,在关联数组的时候,可以把它和 golang 中的 map 类比,但是它们也有不少的差异。

Undefined Array Key 问题

正确姿势

在 PHP 中,如果一个关联数组使用如下:

$object = [
    'key_1' => 'value1',
    'key_2' => 'value2',
];

$key_1 = $object['key_1'];
$key_2 = $object['key_2'];

echo $key_1."\n";
echo $key_2."\n";

那么 PHP 在执行时可以正确处理并给出输出:

value1
value2

错误姿势

但如果我们这么使用:

$object = [
    'key_1' => 'value1',
    'key_2' => 'value2',
];

$key_1 = $object['key_1'];
$key_2 = $object['114514'];

echo $key_1."\n";
echo $key_2."\n";

这个时候 PHP 就要发怒了:

PHP Warning:  Undefined array key "key_3" in /path/to/php/test.php on line xxx

Warning: Undefined array key "key_3" in /path/to/php/test.php on line xxx

如果执行等级比较高的话,这个错误会直接导致整个链路被 panic 掉而无法执行下面的逻辑。

我们可以通过下面的方式来防御性编程:

$object = [
    'key_1' => 'value1',
    'key_2' => 'value2',
];

$key_1 = $object['key_1'];
$key_2 = $object['114514'] ?? '1919810';

echo $key_1."\n";
echo $key_2."\n";

这样就可以给不存在的字段在获取不到时设定一个初始值了。

总结

PHP 的关联数组在取不到对应的「字段」时,既不会返回空,也不会返回 null,而是会直接 warning 报错,(如果执行等级较低的话,就会返回 null),需要和 go 的 map 做出区分。

数组空字段问题

判断数组空字段

如同上文我们提到了,我们可以用 ?? 运算符来判断一个数组的某个字段是否为空,那么我们那可能在实际会这么使用:

$str = '{"key_1": "value_1", "key_2": ""}';

$object = json_decode($str, true);

echo $object['key_2'] ?? "empty";

如果对方没有对 key_2 设置 omitempty 的话,那么 json 就会如上面这样,此时如果我们直接使用 ?? 来判断字段是否为空,那么我们得到的就是一个空字符串,并不符合我们的期望,此时我们需要:

$str = '{"key_1": "value_1", "key_2": ""}';

$object = json_decode($str, true);

echo empty($object['key_2'] ?? '') ? "empty" : $object['key_2'];

此时就会如约输出 empty 了。

空数组附加问题

我们有的时候会将某个关联数组序列化输出,比如:

$object = ['key' => 'value'];

$response = json_encode($object);

echo $response."\n";

直接运行我们就会得到:

{"key":"value"}

但是如果我们需要输出的字段包括一个空结构的话,比如:

$object = [
    'error_code' => 114514,
    'error_msg' => 'too many requests',
    'data' => [],
];

$response = json_encode($object);

echo $response."\n";

以我们朴素的肉编器人肉编译器来看,它的结果应当是:

{"error_code": 114514, "error_msg": "too many requests", "data": {}}

但实际上,它的结果却是:

{"error_code":114514,"error_msg":"too many requests","data":[]}

data 字段是一个 数组 (咬牙切齿)。如果我们在 golang 中这么接 PHP 返回的答辩:

type Response struct {
    ErrorCode int `json:"error_code"`
    ErrorMsg string `json:"error_msg"`
    Data PayloadStruct `json:"data,omitempty"`
}

那么 go 就 亲切 地给你奖励:

Unmarshal json into struct: cannot unmarshal array into Go value

这个问题目前没有什么好的解决方案,只能通过 json.RawMessage 这种结构先把 data 接下来,然后使用额外的判断逻辑能否 unmarshal。

由此衍生的问题有:

1. 计算一个 json 对象的签名不一致

2. 必填字段校验逻辑复杂

语法相关易错点

使用未声明变量

此 topic 记录于2024年4月22日,首次遇到于2024年4月19日

在正常情况下,在PHP中使用未声明的变量,IDE(PhpStorm)会给出报错,比如下面的代码:

private function _error_func(): void
{
	echo $undefined_var;
}

IDE就会把 $undefined_var 给下划线标红,表示这是一个未声明、未定义的变量,但是在某些语法条件下,IDE会忽略掉这种情况:

private function _sample_func(): void
{
	if (!empty($undefined_var)) {
		echo 'defined';
	}
}

在这种情况下,IDE不会把 $undefined_var 标红并报错,因为 empty($undefined_var) 是一个合法的表达式,即使 if 表达式内的分支永远都不会进入。

Comment