PHPMD (Drupal)
Goal
PHPMD reads PHP against a fixed set of rules and reports the shape problems a
standards checker does not look for — a method with too many lines or too many
branches, a class with too many dependencies, a public property that could be
private. codesize measures size and complexity; design measures coupling and
class shape. With it installed, a claim that a class or method has grown too large
to review can be checked rather than asserted.
Install
ddev composer require --dev phpmd/phpmd
PHPMD registers no Composer plugin, so there is no allow-plugins step and no
separate registration command — the package is ready to run once required.
PHPMD's latest release, 2.15.0 (2023-12-11), runs on PHP 8.3 but its parser
cannot read PHP 8.4 syntax it has never had to handle: new without
parentheses, asymmetric visibility (protected(set)), and property hooks each
make it error out on the file rather than report on it (phpmd/phpmd issues
1219, #1237, #1271). A scan over code using any of them fails, not a clean
report. The fix lives only on the unreleased 3.x branch, whose CLI drops the
positional phpmd <paths> <format> <ruleset> form for named flags —
phpmd analyze <paths> --format <format> --ruleset <ruleset> — so the text
example below, and the design-metrics row in
checks, change once 3.0 ships.
Run
ddev exec vendor/bin/phpmd --version
PHPMD's own usage is phpmd <paths> <report-format> <ruleset(s)>, all three
positional and required, so a bare --version is the only invocation that proves
the tool is present without also naming a scope:
ddev exec vendor/bin/phpmd web/modules/custom text codesize,design --suffixes php,module,inc,install,profile,theme,engine
--suffixes is not optional, for the same reason --extensions is not optional
on the phpcs row: without it a .module or .install file named on the command
line is skipped in silence. Findings print to stdout in the chosen format. PHPMD
2.15.0 exits 2 when it reports a violation, 1 when it cannot run at all (a path
that does not exist), and 0 when clean — both non-zero exits are findings, not
one success and one failure.
If the command itself is not found, the package is absent: install, then run it again.