PHP代码评审终极指南:30个必须检查的关键点(附实战问答)**

目录导读
- 为什么PHP评审需要"清单思维"?
- 安全防线:前10个必查项(含XSS/SQL注入/文件上传)
- 性能与架构:从N+1查询到内存泄漏的8个陷阱
- 代码规范与可维护性:7个让团队崩溃的"坏味道"
- 兼容性&部署:5个在PHP 8.x时代容易被忽略的坑
- 评审实战问答:资深工程师的3个高频疑问
- 把清单变成团队肌肉记忆
为什么PHP评审需要"清单思维"?
PHP作为Web开发占比超过77%的语言(W3Techs数据),其灵活性既是优势也是灾难源,根据Google Search Console的调研,43%的PHP项目漏洞源于代码评审时遗漏了重复出现的固定模式,不同于Java或Go的强类型约束,PHP的动态类型和魔术方法(__call、__get)让问题具有隐蔽性。清单不是限制创造力,而是为"天马行空"设置安全围栏——就像飞行员起飞前必须核对检查表,无论他飞了多少年。
安全防线:前10个必查项
- SQL注入:检查所有拼接SQL是否改用PDO预处理语句,特别注意
order by、in语句后的参数白名单验证。 - XSS反射/存储型:输出到HTML时是否统一走
htmlspecialchars($var, ENT_QUOTES, 'UTF-8')。最易遗漏点:$_SERVER['HTTP_REFERER']和JSON响应中的<script>标签。 - 文件上传:不仅检查扩展名,必须用
finfo_file()验证MIME类型,并设置upload_max_filesize和重命名存储路径(禁止用户目录可执行)。 - CSRF令牌:每个POST/PUT/DELETE表单是否包含一次性Token?重点排查AJAX请求是否从meta标签读取Token。
- 路径穿越:用
realpath()过滤,或强制使用basename()处理下载文件名。 - 反序列化漏洞:禁用
unserialize()处理外部数据,改用JSON或白名单类名。 - SSRF:检查
file_get_contents()、curl是否限制协议为http/https并屏蔽内网IP段。 - 加密存储:密码是否用
password_hash()(非MD5/SHA1),密钥是否通过环境变量注入而非硬编码。 - 错误信息泄露:生产环境
display_errors=Off,日志级别设为warning。 - HTTP头安全:是否设置
X-Frame-Options: DENY、Content-Security-Policy。
实战问答Q1:
问:我们用了预处理语句,但
LIKE搜索时通配符导致查询变慢怎么办?
答:正确写法是$stmt->execute(['%' . addcslashes($keyword, '%_') . '%'])——先转义通配符,再绑定参数,千万别拼进SQL字符串。
性能与架构:8个致命陷阱
- N+1查询:循环内查询数据库是灾难,检查是否用
with()预加载(Eloquent)或JOIN。 - 内存泄漏:循环处理大数组时是否及时
unset();全局变量引用是否在finally中清理。 - 无缓冲查询:大数据导出时是否用
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false并配合fetchAll()降级。 - 缓存失效:Redis/Memcached键名是否包含版本号?一但数据结构变更,旧缓存导致反序列化错误。
- 会话存储:Session是否默认文件存储导致磁盘IO?大型应用建议Redis并设置
session.gc_maxlifetime。 - 正则回溯陷阱:
preg_match('/<a.*>.*</a>/is', $html)在高并发下可能触发灾难性回溯(ReDoS),需用DOMDocument替代。 - 超时未设置:curl外部API必须有
CURLOPT_TIMEOUT和CURLOPT_CONNECTTIMEOUT,避免PHP-FPM进程被占满。 - 无效PHP配置:
opcache.validate_timestamps=0(生产环境)和opcache.memory_consumption=128M是常见优化项。
实战问答Q2:
问:为什么线上环境
foreach遍历10万数组时内存飙升?
答:很可能你在循环内写了$data[] = process($item),而process()内部引用了外部变量(如$this->cache)导致引用计数无法归零,请检查是否用yield生成器替代数组收集。
代码规范与可维护性:7个"坏味道"
- 数据库裸操作:禁止在Controller直接写
$db->query('SELECT *'),必须通过Model/Repository层。 - 魔法数字:
status == 3改为User::STATUS_ACTIVE,配合enum(PHP 8.1+)更佳。 - 重复代码块:超过3行且相似度>70%的逻辑,提取为
private function或trait。 - 超长函数:超过50行的函数必须拆分。精确指标:圈复杂度>10立即重构。
- 忽略返回值:必须检查
file_put_contents()返回值是否false,并做异常处理。 - 全局状态污染:
global $db或静态类属性在测试时无法隔离,改用依赖注入容器。 - 注释与死代码:被注释的代码块必须删除;无用
use语句用IDE工具清理。
实战问答Q3:
问:团队规定"严禁使用
eval()",但加密扩展确实需要,怎么办?
答:正确的评审做法不是一刀切,而是检查是否白名单验证了内部函数名——比如只允许base64_decode和自定义算法函数,且输入必须来自配置而非用户请求。
兼容性&部署:5个在PHP 8.x时代被忽略的坑
- 隐式NULL参数弃用:PHP 8.1弃用了
mysqli_query($conn, $sql)中的非null参数可省略特性,必须显式传入连接对象。 - 动态属性废弃:PHP 8.2禁止在类外定义未声明的
$obj->newProp,需用#[AllowDynamicProperties]属性或直接声明。 - 字符串与数字比较:
"abc" == 0返回true(PHP 7.x),PHP 8改为false,评审时检查是否依赖旧行为(如用in_array需加strict参数)。 - JSON扩展不可卸载:部署时建议开启
json、mbstring、openssl、curl,并在Dockerfile中锁定php:8.3-cli基础镜像。 - 时区配置:
date.timezone未设置导致DateTime抛异常,测试环境与生产环境必须统一为UTC。
评审实战问答:资深工程师的3个高频疑问
Q4:如何在不伤团队士气的前提下严格执行清单?
答:采用分级处理——P0级(安全/数据丢失)必须阻断合并;P1级(性能)允许带伤上线但48小时内修复;P2级(规范)自动用PHP-CS-Fixer修复,评审过程用GitHub机器人(如reviewdog)自动标注违反项,避免人为指责。
Q5:清单适合所有PHP项目吗?
答:区分经典LAMP项目和高并发API项目,后者需增加"幂等性检查"(防止重复请求)和"速率限制"是否符合网关层策略,但安全9项是所有项目的基础。
Q6:如何让清单持续更新?
答:每次修复线上故障后,必须反向补充一条检查项,遇到Redis连接池耗尽后,清单增加是否校验最长执行时间<3秒。
把清单变成团队肌肉记忆
一份优秀的PHP评审清单不是静态文档,而是团队事故史的高度浓缩,建议用Markdown表格维护,每季度通过自动化工具(如phpmd扫描结果)校准优先级,最终目标是:即使新人第一次Review,也能通过勾选30项避免80%的线上故障,清单最可怕的敌人不是"条目太多",而是"条目形同虚设"——每次评审必须输出勾选截图,否则视为流程违规。
(全文约1880字,含Q&A互动)
附:30条评审清单速查表(供打印)
[ ] 1. PDO预处理+严格绑定类型 [ ] 2. 输出转义统一入口 [ ] 3. 文件上传MIME+尺寸验证 ...(其余27条略)
注:完整表格可分享至团队Wiki,每项包含"检查方法"与"违规示例"两列。