phpcpd-next 扫描您的 PHP 代码并报告从一处复制粘贴到另一处的代码块——这类重复在代码审查中容易被遗漏,后续同步维护时却会带来很大困扰。

该项目由 Luciano Federico Pereira 维护,作为 Sebastian Bergmann 的 phpcpd(已归档)的继任者,并保持与原有 phpcpd 命令的直接替换兼容。

其新增功能不仅能检测逐字复制,还能标记行顺序被重新排列、或在两个原本相同的代码块之间添加或删除了语句的重复:

  • 三种检测引擎 — Rabin-Karp(精确匹配)、TokenBag(重排序匹配)和可选的后缀树(间隙 Type-3 匹配),并在此基础上提供对重命名不敏感的 --fuzzy 匹配
  • 四种输出格式 — 控制台文本、PMD-CPD XML、JSON 以及适用于 GitHub Code Scanning 的 SARIF 2.1.0
  • 无头 API 支持进程内调用检测,同时提供一个 PHPUnit trait,可将重复检测转化为测试断言
  • CI 特性 — 有意义的退出码、完整的结果缓存以及按文件的增量索引
  • 框架预设 包含 Laravel,支持通过 CLI 标志覆盖预设默认值
  • PHP 8.5+,零 Composer 运行时依赖,并产生确定性结果

三种引擎,默认同时运行

大多数复制/粘贴检测器仅能发现精确重复。phpcpd-next 默认每次运行时同时执行 Rabin-Karp(精确连续匹配)和 TokenBag(顺序无关的重叠检测,即使语句顺序被打乱仍能识别)。用于检测间隙克隆(在原本相同的代码块之间插入或删除了语句)的后缀树引擎为可选功能:

# 默认:精确 + 重排序检测

phpcpd src/

# 仅 Rabin-Karp(更快,无重排序检测)

phpcpd --rk src/

# 通过后缀树检测间隙 Type-3 克隆

phpcpd --algorithm=suffixtree src/

控制台输出会指出重复的范围,并建议进行重构,而非仅列出行号:

Found 2 code clones with 21 duplicated lines in 2 files:

- app/Services/Billing.php:12-33 (21 lines)

app/Services/Invoicing.php:40-61

→ Consider extracting the shared lines into a reusable method or constant.

37.50% duplicated lines out of 56 total lines of code.

SARIF 输出用于 GitHub Code Scanning

除了 PMD-CPD XML 和 JSON,phpcpd-next 还可输出 SARIF 2.1.0,使克隆在 GitHub Security 标签页中显示。不一致(已分化)的克隆映射为 warning 严重性,精确克隆映射为 note

- name: Detect duplicated code

run: vendor/bin/phpcpd --log-sarif=phpcpd.sarif src/ || true

- name: Upload results

uses: github/codeql-action/upload-sarif@v3

with:

sarif_file: phpcpd.sarif

无头 API 和 PHPUnit 断言

除 CLI 外,检测还可通过静态 detect() 调用在进程内运行——无需 shell 调用,也无需生成报告文件:

use LucianoPereira\PhpcpdNext\Phpcpd;

$clones = Phpcpd::detect(

paths: 'app',

minTokens: 60,

algorithm: null, // null = Rabin-Karp + TokenBag

preset: 'laravel',

);

foreach ($clones as $clone) {

echo $clone->numberOfLines(), " lines\n";

}

附带的 trait 可将其转化为测试,使重复检测成为一个回归检查,并在发现克隆位置时失败:

use LucianoPereira\PhpcpdNext\PHPUnit\AssertNoDuplication;

use PHPUnit\Framework\TestCase;

final class DuplicationTest extends TestCase

{

use AssertNoDuplication;

public function test_app_is_dry(): void

{

$this->assertNoDuplication(__DIR__ . '/../app', minTokens: 70);

}

}

适用于 CI 的增量缓存

对于大型代码库,--cache 会根据配置指纹和文件清单哈希存储结果,当文件未发生变化时重放缓存结果。--incremental 更进一步,仅对修改过的文件重新进行词法分析,并从每文件索引中复用其余部分(仅限 Rabin-Karp),输出类似 (incremental index: 412 reused, 3 scanned) 的摘要:

- uses: actions/cache@v4

with:

path: .phpcpd-cache

key: phpcpd-${{ hashFiles('**/*.php') }}

restore-keys: phpcpd-

- run: vendor/bin/phpcpd --incremental --cache-dir .phpcpd-cache src/

安装

该工具需要 PHP 8.5+、ext-domext-mbstring,并作为开发依赖安装:

composer require --dev phpcpd-next/phpcpd

vendor/bin/phpcpd src/

Laravel 预设会扫描 approutesdatabaseconfig,同时排除 vendor 代码、Blade 视图、迁移文件和 IDE-helper 文件:

vendor/bin/phpcpd --preset=laravel app/Services --min-tokens=60

您可以在 GitHub 上找到源代码和完整文档。