PHP 的弱类型是把双刃剑。方便,但坑很多。整理一下这些年真实踩过的。
一、字符串转数字的规则
var_dump("10" + 5); // int(15)
var_dump("10abc" + 5); // PHP 8: 警告,结果 15
var_dump("abc" + 5); // PHP 8: 警告,结果 5
PHP 8 之前,"abc" + 5 是 5 且不报错。 这就是很多老代码里隐藏的 bug 来源。
PHP 8.0 之后改成了"前导数字"规则:"10abc" 取 10 并给个 Warning,"abc" 取 0 并给 Warning。
但 Warning 不等于异常。如果 error_reporting 没配好,或者用了 @ 抑制,还是静默出错。
我的做法: 所有从外部来的数字都显式转换,不靠隐式。
$page = (int) ($_GET['page'] ?? 1);
$price = (float) ($input['price'] ?? 0);
二、比较运算符的传递性被破坏
var_dump(0 == "a"); // PHP 7: true,PHP 8: false
var_dump("1" == "01"); // true(都是数字字符串)
var_dump("10" == "1e1"); // true
var_dump(100 == "1e2"); // true
var_dump("abc" == 0); // PHP 7: true,PHP 8: false
PHP 8 改了字符串和数字的比较规则(数字字符串才转数字,非数字字符串转成字符串比),修掉了很多反直觉的情况。但还是不要用 == 比字符串。
规则:永远用 ===。
例外是判断 null,这时候 == null 和 === null 语义不同:
$v = 0;
$v == null // true(0 松散等于 null)
$v === null // false
所以判断"是不是 null"必须用 ===。判断"是不是空"用 empty()。
三、空值判断的四种写法
$v = "0";
empty($v) // true —— "0" 被认为是空!
isset($v) // true
$v == false // true
!$v // true
$v = [];
empty($v) // true
$v = 0.0;
empty($v) // true
$v = "0.0";
empty($v) // false —— 只有字符串 "0" 特殊
"0" 被 empty 判断为空,这是 PHP 的历史遗留,无数 bug 来源于此。
场景:表单里数量字段填 0,用 if (empty($qty)) 判断就会认为没填。
我的做法:区分"未提供"和"值为空"。
// 判断字段是否存在
if (!array_key_exists('qty', $input)) {
// 没提供
}
// 判断是否为空字符串
if ($input['qty'] === '') {
// 提供了但是空
}
// 判断数值是否有效
if (!is_numeric($input['qty'])) {
// 不是数字
}
四、数组键的类型转换
$arr = [];
$arr[1] = 'a';
$arr['1'] = 'b'; // 同一个键!覆盖了
$arr[true] = 'c'; // 键变成 1,又覆盖
$arr[1.9] = 'd'; // 键变成 1(截断,不是四舍五入)
$arr[null] = 'e'; // 键变成 ''
var_dump($arr);
// [1 => 'd', '' => 'e']
只有整数和字符串能做键,其它类型会被转换:
| 原值 | 转换后 |
|---|---|
true |
1 |
false |
0 |
null |
'' |
1.9 |
1 |
| 数组 | TypeError |
| 对象 | TypeError(除非实现 ArrayAccess) |
坑在于:从 JSON 拿到的键是字符串 "1",代码里用 int 1 去取,看着不一样实际是同一个。这通常是"能用"的,但如果你做了 array_flip 或者比较键类型,就会出问题。
反过来也有坑:"01" 和 "1" 是不同的键("01" 不是合法的十进制整数格式,保持字符串)。
五、字符串和数字的数组排序
$arr = ['10', '9', '100'];
sort($arr);
// ['10', '100', '9'] 字符串排序!
因为元素是字符串,sort 默认按字符串比。要按数字比:
sort($arr, SORT_NUMERIC); // ['9', '10', '100']
或者:
usort($arr, fn($a, $b) => $a <=> $b); // 飞船运算符,按数值比较
这个坑在分页排序里特别常见——"第 10 页"排到了"第 2 页"前面。
六、json_decode 的第二个参数
$json = '{"a":1,"b":{"c":2}}';
$obj = json_decode($json); // stdClass
$obj->b->c; // 2
$arr = json_decode($json, true); // 关联数组
$arr['b']['c']; // 2
混用会出错:json_decode($json)->b['c'] 会报错,因为 ->b 是 stdClass 不是数组。
我现在的习惯是统一用 true,返回数组。这样和 PHP 数组函数能直接配合。代价是失去类型信息,但本来 JSON 也没什么类型。
大整数要小心:
$json = '{"id": 12345678901234567890}';
json_decode($json, true); // id 变成 float,精度丢了
用 JSON_BIGINT_AS_STRING:
json_decode($json, true, 512, JSON_BIGINT_AS_STRING);
// id 是字符串 "12345678901234567890"
这个坑在对接第三方支付、雪花 ID 的时候遇到过。
七、浮点数比较
var_dump(0.1 + 0.2 === 0.3); // false!
$price = 0.1 + 0.2;
var_dump($price); // float(0.30000000000000004)
这不是 PHP 的问题,是 IEEE 754 二进制浮点的固有问题。
涉及金额绝对不能用 float。
// 错误
$total = 19.99 * 3; // 59.970000000000006
// 正确:用整数分
$totalCents = 1999 * 3; // 5997 分 = 59.97 元
// 或者用 bcmath
bcadd('19.99', '0.01', 2); // "20.00"
数据库存金额也用 DECIMAL 不用 FLOAT/DOUBLE。
我在项目里的约定是:内部计算全用整数分,只在输出的时候转成元。 字段名统一加 _cents 后缀,看到就知道是整数。
八、类型声明不是运行时保证
PHP 7+ 支持标量类型声明,但默认是弱模式:
function add(int $a, int $b): int {
return $a + $b;
}
add("5", "3"); // 不报错!字符串被转成整数,返回 8
add("abc", 3); // TypeError("abc" 不能转数字)
要强制得加 declare(strict_types=1) 在文件第一行:
<?php
declare(strict_types=1);
function add(int $a, int $b): int { ... }
add("5", "3"); // TypeError
注意:strict_types 是"调用方"的文件决定的,不是被调用方。也就是说:
- A 文件声明了 strict,调用 B 文件的函数 → 严格检查
- C 文件没声明,调用同一个函数 → 宽松检查
这个设计很奇怪,意味着你不能通过给库加 strict_types 来保证调用安全。得每个入口文件都加。
我现在的项目所有文件第一行都是 declare(strict_types=1),用 PHPStan 检查有没有漏。
九、null 和默认参数
function greet(string $name = "world") {
return "Hello $name";
}
greet(null); // PHP 8.1 之前:$name 变成 ""(TypeError?)
PHP 8.1 之前,null 传给非空类型参数会被转成空字符串(因为在弱模式下 null 可以转标量)。8.1 之后废弃,会报 deprecation。
要允许 null 必须显式声明:
function greet(?string $name = null) {
return "Hello " . ($name ?? "world");
}
可空类型必须写 ?。 我见过很多 string $x = null 这种写法,PHP 8.4 之后这个也会废弃。
十、实践中怎么减少踩坑
一、开严格错误报告。 开发环境:
error_reporting = E_ALL
display_errors = On
生产环境:
error_reporting = E_ALL
display_errors = Off
log_errors = On
E_ALL 一定要开,包括 deprecation,不然升级 PHP 的时候会集中爆发。
二、用静态分析工具。 PHPStan 或 Psalm,级别调到 6 以上能抓出大部分类型问题。
vendor/bin/phpstan analyse --level=8 src
三、新代码全加 declare(strict_types=1)。
四、金额、ID 用字符串或整数,不用 float。
五、所有外部输入显式转换类型。
六、比较一律用 ===。
这几条坚持下来,弱类型带来的问题能减少八成。剩下的两成基本都是历史代码,改不动也没办法。