PHPCPD (PHP CLI)
Goal
PHPCPD reads a codebase's PHP files and reports the blocks that repeat elsewhere almost unchanged — code that could be one function instead of several copies drifting apart. With it installed, a duplication claim about a PHP CLI project's library can be checked rather than asserted.
The original sebastian/phpcpd is abandoned; Packagist marks it abandoned and
suggests no replacement, and its own README says the repository is kept only for
archival purposes. systemsdk/phpcpd is the actively maintained fork: its
composer.json still lists Sebastian Bergmann as lead author alongside the
maintaining developer, and its README describes it as a continuation of the
abandoned project. Install this fork, not the original.
Install
composer require --dev systemsdk/phpcpd
The package registers no Composer plugin and needs no allow-plugins entry.
Composer resolves the required version against whatever PHP runs it, so the release
it picks depends on that PHP's version: the latest release, 9.1.0, requires PHP 8.4
or later; the 8.x line (8.0.0 through 8.3.0) requires PHP 8.3 or later; and 7.0.1
requires PHP 8.1 or later. A project on an older PHP floor pins the release its
floor supports rather than taking the latest.
Run
vendor/bin/phpcpd --version
Run the binary directly, not through php: a missing vendor/bin/phpcpd
then exits 127, the exit code that says a tool is absent rather than merely
failing. Prints the installed version and exits 0 otherwise. If
vendor/bin/phpcpd is absent, the shell exits 127: install, then run it
again.
PHPCPD scans directories, not individual files — a file named on its command line
produces No files found to scan and exits 1. A real scan names the directory to
check, and its default suffix, .php, is enough for a library with no other PHP
file extensions to include:
php vendor/bin/phpcpd src
Findings print to stdout as a list of duplicated blocks with their files and line ranges. The exit status is non-zero when a clone was found, so a caller can branch on it without reading the text — the same non-zero status a missing scan target produces, so an empty scope reads as unmet rather than as clean.