PHP 怎么PHP 魔法数字检测

wen PHP项目 2

本文目录导读:

  1. 什么是魔法数字
  2. 多种检测方法
  3. 建议的检测策略
  4. 高级注意事项

在 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 内置了魔法数字检测:

  1. Code → Inspect Code...
  2. 选择 PHP → Code Smell → Magic number
  3. 可以自定义允许的数字范围

PHP 怎么PHP 魔法数字检测

建议的检测策略

分阶段实施

阶段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

高级注意事项

  1. 几何/数学常量 如 π (3.14159) 应该使用 M_PI
  2. 状态码 如 200, 404 通常不需要提取常量
  3. 时间值 如 60(秒/分钟)、3600(秒/小时)可以考虑提取

使用这些方法,你可以有效地检测和消除 PHP 代码中的魔法数字,提高代码质量。

抱歉,评论功能暂时关闭!