本文目录导读:
在 PHP 中,“魔法数字”(Magic Numbers)通常指代码中直接出现的、没有明确含义的硬编码数值,检测魔法数字的主要目的是提高代码的可维护性和可读性。
什么是魔法数字
// ❌ 魔法数字示例
if ($user->getAge() >= 18) { // 18 是什么意思?
echo "成年人";
}
// ✅ 好的做法
const LEGAL_AGE = 18;
if ($user->getAge() >= LEGAL_AGE) {
echo "成年人";
}
多种检测方法
手动代码审查
最直接但最耗时的方法,适用于小型项目。
使用静态分析工具
PHPStan(推荐)
安装:
composer require --dev phpstan/phpstan
配置 phpstan.neon:
parameters:
level: 8
magicNumberRules:
# 允许的数字范围(0, 1, -1 通常不算魔法数字)
allowedNumericValues: [0, 1, -1, 100]
运行:
./vendor/bin/phpstan analyse src/
检测示例:
class UserController {
public function calculateDiscount($price) {
return $price * 0.9; // PHPStan 会警告:发现魔法数字 0.9
}
}
Phan
composer require --dev phan/phan
配置 .phan/config.php :
<?php
return [
'suppress_issue_types' => [
// 可以设置忽略某些数字
],
'should_check_for_magic_numbers' => true,
];
使用代码嗅探器 (PHP_CodeSniffer)
安装:
composer require --dev squizlabs/php_codesniffer
创建自定义规则 phpcs.xml:
<?xml version="1.0"?>
<ruleset name="MagicNumberDetector">
<rule ref="Generic.NamingConventions.ConstructorName"/>
<!-- 自定义魔法数字检测规则 -->
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<!-- 允许常见的安全数字 -->
<config name="allowed_magic_numbers" value="0,1,2,-1"/>
</ruleset>
使用 PHP 内置 Tokenizer 自制检测器
创建一个简单的检测脚本 detect_magic.php:
<?php
class MagicNumberDetector {
private $allowed = [0, 1, -1];
private $foundIssues = [];
public function analyze($filePath) {
$code = file_get_contents($filePath);
$tokens = token_get_all($code);
foreach ($tokens as $index => $token) {
if ($this->isMagicNumber($token, $tokens, $index)) {
$this->foundIssues[] = [
'file' => $filePath,
'line' => $token[2],
'value' => $token[1],
'suggestion' => "Replace '{$token[1]}' with a named constant"
];
}
}
}
private function isMagicNumber($token, $tokens, $index) {
// 只检查数字字面量(T_LNUMBER 或 T_DNUMBER)
if ($token[0] !== T_LNUMBER && $token[0] !== T_DNUMBER) {
return false;
}
$value = (int)$token[1];
// 允许常见数字
if (in_array($value, $this->allowed)) {
return false;
}
// 检查上下文:数字前不能是特定关键字
$prevToken = $this->getPrevToken($tokens, $index);
$nextToken = $this->getNextToken($tokens, $index);
// 忽略数组索引、case 语句、const 定义等
if ($this->isInAcceptedContext($prevToken, $nextToken, $tokens, $index)) {
return false;
}
return true;
}
private function getPrevToken($tokens, $index) {
for ($i = $index - 1; $i >= 0; $i--) {
if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_WHITESPACE) {
return $tokens[$i];
}
}
return null;
}
private function getNextToken($tokens, $index) {
for ($i = $index + 1; $i < count($tokens); $i++) {
if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_WHITESPACE) {
return $tokens[$i];
}
}
return null;
}
private function isInAcceptedContext($prevToken, $nextToken, $tokens, $index) {
// if ($x == 0) 或 if ($x === 0)
if ($prevToken && is_array($prevToken)) {
if (in_array($prevToken[0], [T_IS_EQUAL, T_IS_IDENTICAL, T_IS_NOT_EQUAL, T_IS_NOT_IDENTICAL])) {
return true;
}
}
// 数组定义 [1, 2, 3]
if ($prevToken && $prevToken === '[' || $prevToken === ',') {
return true;
}
// foreach 循环
if ($prevToken && is_array($prevToken) && $prevToken[0] === T_AS) {
return true;
}
// const 定义
if ($nextToken && is_array($nextToken) && $nextToken[0] === T_STRING &&
strtolower($nextToken[1]) === 'const') {
return true;
}
return false;
}
}
// 使用示例
$detector = new MagicNumberDetector();
$detector->analyze('/path/to/your/file.php');
foreach ($detector->getFoundIssues() as $issue) {
echo "Found magic number '{$issue['value']}' in {$issue['file']}:{$issue['line']}\n";
echo "Suggestion: {$issue['suggestion']}\n\n";
}
IDE 集成(以 PHPStorm 为例)
PHPStorm 内置了魔法数字检测:
- Code → Inspect Code...
- 选择 PHP → Code Smell → Magic number
- 可以自定义允许的数字范围

建议的检测策略
分阶段实施
阶段1:只检测 3 以上的数字
阶段2:检测所有非 0,1,-1 的数字
阶段3:严格检测,包括浮点数
白名单机制
// phpstan.neon
parameters:
magicNumberRules:
allowedValues:
- 0
- 1
- -1
- 60 # 60秒 = 1分钟
- 100 # 百分比
- 24 # 24小时
CI/CD 集成
在 .gitlab-ci.yml 或 .github/workflows 中添加:
code-quality:
script:
- ./vendor/bin/phpstan analyse --level=max
only:
- merge_requests
高级注意事项
- 几何/数学常量 如 π (3.14159) 应该使用
M_PI - 状态码 如 200, 404 通常不需要提取常量
- 时间值 如 60(秒/分钟)、3600(秒/小时)可以考虑提取
使用这些方法,你可以有效地检测和消除 PHP 代码中的魔法数字,提高代码质量。