Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Code Coverage Checker Laravel Package

handcraftedinthealps/code-coverage-checker

A small CLI tool for PHPUnit that reads a --coverage-php report and fails when line, class, or method coverage falls below a threshold you set. Limit checks to specific directories if needed, making it easy to gate CI builds on real coverage numbers.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a single-purpose CLI gate: it reads a PHPUnit --coverage-php report (a serialized PHP_CodeCoverage object, not Clover/XML) and exits non-zero if any measured unit falls below your threshold. There is no config file, no Artisan command, no service provider — you drive it entirely with arguments.

1. Install as a dev dependency

composer require --dev handcraftedinthealps/code-coverage-checker

2. Make PHPUnit produce the right report format

The common mistake is pointing the checker at coverage.clover or coverage.xml. It only understands the PHP report. In phpunit.xml:

<coverage>
    <include>
        <directory suffix=".php">./app</directory>
    </include>
    <exclude>
        <directory>./vendor</directory>
        <directory>./app/Console/Kernel.php</directory>
    </exclude>
    <!-- PHPUnit 9 only -->
    <report>
        <php outputFile="reports/coverage.php"/>
    </report>
</coverage>

Then generate it:

XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-php reports/coverage.php

In a Laravel app prefer calling vendor/bin/phpunit directly here — php artisan test is a thin wrapper, but coverage-report flags are easier to reason about when you pass them straight to PHPUnit.

3. Run the check

vendor/bin/code-coverage-checker reports/coverage.php line 90.0

Arguments are positional: report, metric (line | method | class), threshold as a string, then zero or more directory filters.

First use case: add it as your CI gate after the test job, starting at your current coverage minus a small buffer, then ratchet upward. Don't start at 90% if you're at 61% — you'll block every PR on day one. Start at 60%, merge the wiring, then raise the number in follow-up PRs.

Implementation Patterns

Composer script as the single source of truth. Because there is no config file, the threshold lives in whatever command you type. Centralise it so the value can't drift between CI, local runs, and docs:

"scripts": {
    "test:coverage": [
        "@putenv XDEBUG_MODE=coverage",
        "phpunit --coverage-php reports/coverage.php"
    ],
    "coverage:check": [
        "@test:coverage",
        "code-coverage-checker reports/coverage.php line 85.0 app"
    ]
}

Then composer coverage:check locally and composer coverage:check in CI — identical behaviour, one number to bump.

Tiered thresholds via repeated invocations. The tool takes a single threshold per run, so express granularity as multiple calls. Fail fast on the strictest gate first:

set -e
vendor/bin/code-coverage-checker reports/coverage.php line 95.0 app/Domain
vendor/bin/code-coverage-checker reports/coverage.php line 85.0 app/Http
vendor/bin/code-coverage-checker reports/coverage.php line 75.0 app

set -e matters: without it a bash script keeps going after the first failure and your CI log ends on a success line while the job is actually red.

CI job shape (GitHub Actions). Keep coverage generation and checking in the same job so the report file exists:

- name: Unit tests with coverage
  run: vendor/bin/phpunit --coverage-php reports/coverage.php
- name: Enforce coverage threshold
  run: vendor/bin/code-coverage-checker reports/coverage.php line 85.0 app

Since 0.2.9 the failure output lists the individual files below threshold, which is what you paste into a PR comment or a Slack notification. Capture stdout on failure and post it — no extra parsing needed.

Pre-commit / pre-push hook. Coverage runs are slow (Xdebug, full suite), so a pre-push hook is usually a better trade than pre-commit. Keep it opt-in per developer:

#!/usr/bin/env sh
# .git/hooks/pre-push
composer coverage:check || {
  echo "Coverage below threshold — push blocked. Use 'git push --no-verify' to override."
  exit 1
}

Artisan wrapper, if your team lives in artisan. A thin command keeps the ergonomics consistent with the rest of the app. It should do nothing but shell out — resist the urge to reimplement the parsing:

public function handle(): int
{
    $report = base_path('reports/coverage.php');

    $generate = Process::fromShellCommandline(
        'XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-php '.escapeshellarg($report),
        base_path(), null, null, 900
    );
    $generate->mustRun();

    $check = Process::fromShellCommandline(
        'vendor/bin/code-coverage-checker '.escapeshellarg($report).' line 85.0 app',
        base_path()
    );
    $check->run();
    $this->output->write($check->getOutput());

    return $check->getExitCode();
}

Return the child exit code verbatim so php artisan coverage:check && git push behaves.

Legacy-code ratcheting. On an inherited codebase, don't gate the whole app/ directory. Gate app/Domain (or a newly created module namespace) at a high threshold while it's small, and let the rest of the app catch up. The directory-filter argument is the mechanism for this — it's the package's main differentiator over a plain --min in PHPUnit.

Pair with a reporting service, don't replace it. This tool answers "may this merge?"; Coveralls/Codecov/Sonar answer "what changed and why?". Run this as the hard gate and upload the Clover report separately for the human-readable diff. Generate both files from one PHPUnit run via a second <report><clover .../></report> entry.

Gotchas and Tips

It cannot read Clover, Cobertura, or XML. Only the --coverage-php artefact. If you get an "unserialize"/fatal error, you pointed it at the wrong file. Likewise, a report from a different PHP/PHPUnit version than the checker's runtime can fail to unserialize — keep generation and checking in the same CI image.

Uncovered files may be missing from the report entirely. This is the biggest silent-failure mode: PHPUnit historically only listed files that were loaded during the run, so never-executed classes vanished and the percentage looked better than it was. In PHPUnit 9, set <coverage processUncoveredFiles="true">; in PHPUnit 10+ the <source> include list is honoured and the option is gone. Verify by asserting that a file you know is untested actually appears in the failure list.

The threshold is a string, and it's positional. line 96 and line "96.0" both work, but argument order is fixed — putting the threshold before the metric silently misbehaves rather than erroring helpfully. Always quote the decimal in scripts; unquoted 96.0 is fine in bash but not across all shells/CI YAML parsing.

class and method are much harsher than they sound. A method counts as covered only if all its lines are covered; a class counts only if all its methods are. A 90% line gate and a 90% method gate are not remotely comparable in difficulty — measure both before committing to numbers, and expect method/class thresholds to sit 10–20 points below line coverage.

Zero-dependency, low-traffic package. Six stars, zero dependents, last release 0.2.9 — it's maintained but not heavily exercised outside the author's projects. Two practical consequences: (a) pin the version explicitly in composer.json rather than floating, and (b) wrap it in your own composer script so that if you ever need to swap it for phpunit --coverage-text parsing or a five-line custom script, the change is in one file your team already knows.

Symfony constraint churn. Versions 0.2.6–0.2.8 each only widened the symfony/* range. If composer require refuses to resolve on a Laravel 10/11 project, check whether you're on the newest release — older tags have a hard cap that can conflict with your Symfony components. There is no symfony/phpunit-bridge requirement to satisfy for the checker itself; the bridge support is about the report format the bridge produces.

Exit codes are the contract. 0 = pass, non-zero = fail. Because of that, never wrap the call in something that swallows the code (|| true, | tee, missing set -e, or an Artisan command that returns 0 unconditionally). Pipe through tee and use set -o pipefail if you need to keep the log.

Debug a suspicious pass by inspecting the report directly. The report is a serialized PHP object, so a quick sanity check on what's actually inside is:

php -r '$c = unserialize(file_get_contents("reports/coverage.php"));
echo $c->getNumberOfClasses(), " classes, ", $c->getNumberOfMethods(), " methods\n";'

If counts are lower than you expect, your phpunit.xml include/exclude rules are the cause — the checker faithfully reports what PHPUnit gave it.

Directory filters match the paths as they appear in the report. If a filter produces an unexpectedly empty result, print a few keys from $c->getData()->lineCoverage() (or a similar data accessor) to see whether paths are absolute or project-relative, and pass your filters in that form. Trailing slashes are a common source of silent non-matches — verify your first run actually measured something by comparing it against the unfiltered run.

Xdebug overhead is the real cost, not the checker. XDEBUG_MODE=coverage can triple suite runtime. PCOV is considerably faster and is fine for this workflow; add pcov to the CI image and skip Xdebug entirely if you only generate coverage and never step-debug in that job.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support