phpcpd-next: A Modern Copy/Paste Detector for PHP 8.5+
Code duplication is one of those problems that sneaks past review and compounds quietly over time. phpcpd-next is a CLI tool that scans your PHP codebase and reports duplicated blocks — including cases that a simple text diff would miss.
Maintained by Luciano Federico Pereira as a successor to Sebastian Bergmann's archived phpcpd, it keeps the same phpcpd command so existing scripts and CI pipelines need no changes.
Three Detection Engines
Most copy/paste detectors only catch word-for-word duplicates. phpcpd-next ships three engines:
- Rabin-Karp — exact contiguous matches, fast by default
- TokenBag — order-invariant overlap, catches shuffled statements
- Suffix tree — opt-in gapped Type-3 clones, where a statement was inserted or removed between otherwise identical blocks
Rabin-Karp and TokenBag run together on every default scan. The suffix-tree engine is opt-in:
# Default: exact + reordered detection
phpcpd src/
# Rabin-Karp only
phpcpd --rk src/
# Gapped clones via suffix tree
phpcpd --algorithm=suffixtree src/
Console output points at the duplicated ranges and suggests a refactor:
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.
SARIF Output for GitHub Code Scanning
phpcpd-next writes four output formats: console text, PMD-CPD XML, JSON, and SARIF 2.1.0. The SARIF format integrates directly with GitHub Code Scanning, surfacing clones in the Security tab. Diverged clones map to warning severity; exact clones map to 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
Headless API and PHPUnit Assertions
Detection can run in-process via a static detect() call — no subprocess, no report files:
use LucianoPereira\PhpcpdNext\Phpcpd;
$clones = Phpcpd::detect(
paths: 'app',
minTokens: 60,
algorithm: null, // Rabin-Karp + TokenBag
preset: 'laravel',
);
A bundled PHPUnit trait turns duplication into a regression test:
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);
}
}
Incremental Caching for CI
For larger codebases, --cache stores results keyed by a configuration fingerprint and file-manifest hash. --incremental goes further, re-tokenizing only changed files and reusing the rest from a per-file index (Rabin-Karp only):
- 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/
Laravel Preset and Installation
The tool requires PHP 8.5+, ext-dom, and ext-mbstring. Install it as a dev dependency:
composer require --dev phpcpd-next/phpcpd
vendor/bin/phpcpd src/
A built-in Laravel preset scans app, routes, database, and config while automatically excluding vendor code, Blade views, migrations, and IDE-helper files:
vendor/bin/phpcpd --preset=laravel app/Services --min-tokens=60
Key Takeaways
- Drop-in replacement for the archived
phpcpdwith the same CLI command - Three engines: exact (Rabin-Karp), reordered (TokenBag), and gapped (suffix tree)
- SARIF 2.1.0 output integrates with GitHub Code Scanning out of the box
- PHPUnit trait makes duplication a first-class test assertion
- Incremental indexing keeps CI scans fast on large codebases
- Zero Composer runtime dependencies; requires PHP 8.5+
- Built-in Laravel preset with sensible exclusions
Source: Laravel News — A Copy/Paste Detector CLI for PHP 8.5+