浪白的博客の小站
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

PHP 弱类型踩坑合集

写作时间:2026-06-15 20:30:00
# PHP
# 语言细节
# 避坑

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。

五、所有外部输入显式转换类型。

六、比较一律用 ===。

这几条坚持下来,弱类型带来的问题能减少八成。剩下的两成基本都是历史代码,改不动也没办法。

avatar

浪白

邪恶的浪白

RECOMMENDED

Composer 用了这么多年,几个我认为值得坚持的习惯

2026-04-27 19:15:00

context 传参这件事,我踩过的三个坑

2026-05-06 20:05:00

Go 的 error 处理,用久了反而觉得还行

2026-04-19 22:15:00

Table of Contents