lvandi / php-crap-checker
A CLI tool to fail CI when PHP CRAP score exceeds a configured threshold.
Requires
- php: >=8.3
- ext-simplexml: *
- symfony/console: ^7.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.0
- icanhazstring/composer-unused: ^0.9.6
- infection/infection: ^0.29 || ^0.32
- phpmd/phpmd: ^2.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- rector/rector: ^2.0
Suggests
- ext-pcov: Useful for generating PHPUnit coverage reports with PCOV.
- ext-xdebug: Alternative coverage driver for PHPUnit.
README
A CI quality gate for method-level change risk in PHP projects.
Why this tool exists
Complex code with low test coverage is risky to change. The CRAP score captures this pattern in a single number. crap-check turns that signal into a CI gate: if any method exceeds the threshold, the build fails.
This matters in any workflow where code can be generated, refactored, or expanded quickly — including AI-assisted development — because static analysis and basic tests can pass while change risk quietly accumulates at method level.
What is the CRAP score?
CRAP (Change Risk Anti-Patterns) is a metric that combines cyclomatic complexity and test coverage. A method scores higher when it is both complex and poorly tested. The formula is:
CRAP(m) = comp(m)² × (1 - cov(m)/100)³ + comp(m)
A low CRAP score means the method is either simple, well-tested, or both. A high score is a signal worth investigating — not necessarily a hard blocker, but a starting point for review.
What this package does
- Reads a Crap4J XML report generated by PHPUnit
- Compares each method's CRAP score against a configurable threshold
- Prints the violations sorted by severity
- Exits with a non-zero code so CI fails when the threshold is exceeded
- Diagnoses environment and PHPUnit configuration (
doctorcommand)
What this package does NOT do
- Generate code coverage (that is PHPUnit's job, with PCOV or Xdebug)
- Replace tools like Codecov for overall coverage tracking
- Apply per-method or per-path ignores (planned for a future release)
- Read Clover or other coverage formats
Installation
composer require --dev lvandi/php-crap-checker
Usage
Basic usage
# 1. Generate the Crap4J report with PHPUnit php -d pcov.enabled=1 vendor/bin/phpunit --coverage-crap4j build/crap4j.xml # 2. Check the threshold vendor/bin/crap-check check build/crap4j.xml --threshold=30
The report argument defaults to build/crap4j.xml and --threshold defaults to 30.
Options
| Option | Default | Description |
|---|---|---|
report |
build/crap4j.xml |
Path to the Crap4J XML report |
--threshold |
30 |
Maximum allowed CRAP score (exclusive) |
--max-violations |
(none) | Maximum number of violations before CI fails |
--max-age |
(none) | Maximum report age; accepts minutes (60), or duration strings (30m, 2h) |
--format |
text |
Output format (text, json) |
Usage in GitHub Actions
- name: Run PHPUnit with Crap4J coverage run: php -d pcov.enabled=1 vendor/bin/phpunit --coverage-crap4j build/crap4j.xml - name: Check CRAP threshold run: vendor/bin/crap-check check build/crap4j.xml --threshold=30
Full example with PHP matrix:
jobs: ci: runs-on: ubuntu-latest strategy: matrix: php: ["8.3", "8.4"] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: pcov coverage: pcov - run: composer install --no-interaction --prefer-dist - run: php -d pcov.enabled=1 vendor/bin/phpunit --coverage-crap4j build/crap4j.xml - run: vendor/bin/crap-check check build/crap4j.xml --threshold=30
Usage with Codecov
php-crap-checker and Codecov serve different purposes and work well together:
- name: Run PHPUnit with coverage run: | php -d pcov.enabled=1 vendor/bin/phpunit \ --coverage-clover build/clover.xml \ --coverage-crap4j build/crap4j.xml - name: Check CRAP threshold run: vendor/bin/crap-check check build/crap4j.xml --threshold=30 - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: build/clover.xml fail_ci_if_error: true
Codecov tracks overall and patch coverage. crap-check enforces a per-method CRAP threshold. They complement each other.
Exit codes
| Code | Meaning |
|---|---|
0 |
OK — no violations |
1 |
CRAP threshold exceeded |
2 |
Invalid input (e.g. non-numeric threshold) |
3 |
Report file not found or not readable |
4 |
Invalid XML in report |
5 |
Report valid but contains no methods |
Output examples
No violations
CRAP threshold OK. Max allowed: 30
Analyzed methods: 128
Violations: 0
Violations found
CRAP threshold exceeded. Max allowed: 30
3 violations found:
1) App\Legacy\ReportGenerator::generate()
File: src/Legacy/ReportGenerator.php:10
CRAP: 72.00
Complexity: 18
Coverage: 0.00%
2) App\Service\OrderImporter::import()
File: src/Service/OrderImporter.php:42
CRAP: 46.23
Complexity: 12
Coverage: 41.60%
3) App\Service\ShippingCalculator::calculate()
File: src/Service/ShippingCalculator.php:88
CRAP: 35.10
Complexity: 9
Coverage: 22.00%
Report not found
Report not found: build/crap4j.xml
Generate it with:
php -d pcov.enabled=1 vendor/bin/phpunit --coverage-crap4j build/crap4j.xml
A note on PCOV and Xdebug
PCOV and Xdebug are not required by this package. They are required by PHPUnit to generate the Crap4J report. Once the report exists, crap-check reads the XML and needs no coverage driver.
In CI, install PCOV (faster) or Xdebug only in the step that generates coverage:
- uses: shivammathur/setup-php@v2 with: php-version: "8.3" extensions: pcov coverage: pcov
If you already have a report from a previous step or artifact, you can run crap-check without any coverage extension.
Interpreting a high CRAP score
A high CRAP score on a method is a signal, not a verdict. Before raising the threshold or suppressing the check, consider:
- Add tests: if the method is complex but untested, coverage will bring the score down
- Reduce complexity: extract helper methods or simplify branching logic
- Accept and document: some methods are inherently complex and well-understood — a future baseline feature will allow explicit exceptions
Mechanically writing tests just to lower the number defeats the purpose. Use the score to guide meaningful improvement.
Adopting on a legacy project
If your codebase already has many violations, a zero-tolerance threshold from day one is counterproductive. See docs/ADOPTING.md for gradual adoption strategies: start permissive and tighten over time, cap violations with --max-violations, and how to talk to your team about the metric.
Planned
Future releases may add:
- baseline support (snapshot a list of known violations, block only new ones)
--fail-on=newand--fail-on=worsened- ignore rules for paths and specific methods
- GitHub Actions annotations
Requirements
- PHP
>=8.3 ext-simplexmlsymfony/console^7.0
Development
PHP runs inside Docker. Build the image once (uses your host UID/GID to avoid file permission issues):
make build make install
Common commands:
make test # PHPUnit make qa # PHPUnit + PHPStan (CI gate) make stan # PHPStan level 9 make cs-fix # PHP CS Fixer make infection # Mutation testing
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines on opening issues and submitting pull requests.
To report a security vulnerability, follow the process described in SECURITY.md.
This project is released under the MIT License.