phpcpd-next は PHP コードをスキャンし、ある場所から別の場所へコピー&ペーストされたブロックを報告します。レビューで見逃しやすく、後で同期を保つのが面倒な重複です。

Sebastian Bergmann 氏の phpcpd(アーカイブ済み)の後継として Luciano Federico Pereira 氏が保守しており、同じ phpcpd コマンドでドロップイン置き換えが可能です。

新機能として、逐語的なコピーだけでなく、行の順序が入れ替わった場合や、2 つの同一ブロック間で文が追加・削除された場合も検出します:

  • 3 つの検出エンジン — Rabin-Karp(完全一致)、TokenBag(順序変更対応)、オプトインの接尾辞木(ギャップのある Type-3)、さらに rename-insensitive な --fuzzy マッチング
  • 4 つの出力形式 — コンソールテキスト、PMD-CPD XML、JSON、GitHub Code Scanning 用の SARIF 2.1.0
  • ヘッドレス API — プロセス内で検出を呼び出し可能。PHPUnit トレイトで重複をテストアサーションに変換
  • CI 機能 — 有意義な終了コード、完全な結果キャッシュ、ファイル単位の増分インデックス
  • フレームワークプリセット(Laravel など) — CLI フラグでプリセットのデフォルトを上書き可能
  • PHP 8.5+、Composer ランタイム依存ゼロ、決定的な結果

3 つのエンジンをデフォルトで同時に実行

ほとんどのコピー/ペースト検出ツールは完全一致の重複しか検出しません。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.

GitHub Code Scanning 用の SARIF 出力

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() 呼び出しでプロセス内検出が可能です。シェル呼び出しやレポートファイルは不要です:

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";

}

同梱のトレイトでテスト化でき、重複をクローン位置付きで失敗させるリグレッションチェックにできます:

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 ヘルパーファイルを除外します:

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

ソースと完全なドキュメントは GitHub で確認できます。