# phpunit/php-code-coverage: the engine behind PHPUnit's coverage reports

> A look at the library PHPUnit uses to collect, filter, process and render PHP code coverage, what its API actually does, and where it stops being the right tool.

**sebastianbergmann/php-code-coverage** — Library that provides collection, processing, and rendering functionality for PHP code coverage information.

- Repository: https://github.com/sebastianbergmann/php-code-coverage
- Stars: 8,928 · Forks: 385
- Language: PHP
- License: BSD-3-Clause
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/sebastianbergmann-php-code-coverage

## What phpunit/php-code-coverage is actually responsible for

The README is one sentence long on this point: the library "provides collection, processing, and rendering functionality for PHP code coverage information." That is the whole scope. It is not a test framework, it does not discover tests, and it does not assert anything. It sits underneath PHPUnit and turns a running process into coverage data, then turns that data into a report.

The audience is narrow and specific. You are writing a test runner, a CI service, a static analysis pipeline or a custom dashboard that needs to know which lines, branches or paths executed. You are comfortable constructing objects in PHP rather than passing flags to a binary. If you only want a coverage number for your application, you are not the target reader of this repository: you want PHPUnit, which depends on this package and exposes it through command line options.

The BSD-3-Clause licence is permissive and the package is published on Packagist as phpunit/php-code-coverage. The last push to the default branch was on 2026-09-21, and the most recent release listed is 14.3.3 from 2026-09-10.

## Filter, driver, coverage object: the three objects you build first

The usage example in the README shows the shape of the API. You build a Filter, tell it which files to include, then construct a CodeCoverage object from a driver selected for that filter. The Filter is not a convenience: it is what tells the driver which files to instrument, and the README passes explicit absolute paths to includeFiles(). There is no glob pattern in the example, and no directory walk. If you want directory-level filtering you will be reading the source, not the README.

The driver comes from DriverSelector via forLineCoverage($filter). That call is where the runtime dependency lives. PHP needs an extension that can report line coverage, and the selector picks one. The README does not list which drivers exist, does not say what happens when none is available, and does not document a configuration option to choose one. That is the single thinnest part of the documentation relative to how much can go wrong at runtime.

Once the CodeCoverage object exists, start('<name of test>') and stop() bracket the code you want measured. The name is a label, and the README's example uses the literal string '<name of test>', which suggests one coverage object can accumulate multiple named runs. The README does not state whether repeated start/stop cycles merge, overwrite or append, so treat that as something to confirm in the source before you build a long-lived collector around it.

## Rendering is a separate facade, and serialized data is a first-class input

Two usage examples appear in the README, and the second is the more interesting one. Instead of rendering from a live CodeCoverage object, you can unserialize a coverage file and render from that. The classes are Report\Facade and Serialization\Unserializer, and the README's example calls unSerialize('/path/to/coverage.php') before passing the result to ReportFacade::fromSerializedData($data).

That split matters architecturally. Collection and rendering are decoupled, so a process that gathers coverage does not have to be the process that writes the report. A CI job can produce one file and a separate step can render it. The README does not document the file format beyond the .php extension in the path, and does not state whether the serialized format is versioned or stable across releases. If you persist coverage files between pipeline stages, that is the thing to verify.

Both examples end with renderOpenClover('/tmp/openclover.xml'). Only the OpenClover renderer is named in the README. The related searches people run against this project include "phpunit --coverage-html" and "phpunit show code coverage", which are PHPUnit-level features, not this library's API. Whether this package exposes an HTML renderer through Report\Facade is not stated in the README; check the Report directory in src/ before assuming it does.

## Installing phpunit/php-code-coverage and running a first collection

Installation is a single Composer command. The README gives both the regular and the development-time form; the second is the one most projects want, since coverage belongs to the test suite rather than to production.

```bash
composer require --dev phpunit/php-code-coverage
```

After Composer finishes, the package is available under the SebastianBergmann\CodeCoverage namespace. The README's first example is the smallest complete program it documents, so it is the right starting point. Save it as a script, point the filter at real files in your project, and run it with the PHP binary.

```php
<?php declare(strict_types=1);
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector as DriverSelector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\Facade as ReportFacade;

$filter = new Filter;
$filter->includeFiles(['/path/to/file.php']);

$coverage = new CodeCoverage(
    (new DriverSelector)->forLineCoverage($filter),
    $filter,
);

$coverage->start('demo');
// code under measurement goes here
$coverage->stop();

ReportFacade::fromObject($coverage)->renderOpenClover('/tmp/openclover.xml');
```

What you should see is /tmp/openclover.xml on disk after the script exits. If the driver selector cannot find a usable driver, the script fails at the CodeCoverage constructor rather than at the report call, so a clean run to the XML file is your signal that the environment is set up. The README does not document a way to catch or inspect that failure, which is the first thing to check if the script dies early.

## The driver requirement is the limitation that bites first

The related searches include "php no code coverage driver available", which is the error PHP users hit when the runtime has nothing that can report line coverage. This library does not remove that requirement; it consumes whatever the selector finds. The README documents no bundled fallback driver, no pure-PHP driver, and no configuration to point at a specific one.

The consequence is that phpunit/php-code-coverage is not portable to every PHP install by virtue of being a Composer package. A machine can install it cleanly and still be unable to collect anything. That is a real constraint for container images and shared CI runners where you do not control the extension set.

The second limitation is scope. This is a library, not a tool. There is no binary, no CLI, no configuration file format documented in the README, and no report viewer. If your team wants a coverage percentage in a pull request, this package is an implementation detail of whatever produces it. Choosing it directly means writing the harness yourself, including the part that decides which files count, which the README leaves entirely to your includeFiles() calls.

## How it differs from running PHPUnit with a coverage flag

The obvious alternative is PHPUnit itself, which depends on this package and exposes coverage through options such as --coverage-html and --coverage-clover. The difference is not quality, it is control. PHPUnit decides which files to instrument based on its own configuration, runs your tests, and writes the report at the end of the run. You get a result with almost no code.

Using phpunit/php-code-coverage directly inverts that. You own the filter, you own the start and stop boundaries, and you own the rendering call. In exchange you can measure something that is not a PHPUnit test suite: a script, a fixture loader, a long-running worker, or a subset of a run that you want labelled separately. The README's start('<name of test>') signature is the evidence for that use case, since the label is yours to choose.

A second alternative is Xdebug's own coverage output, which produces data without this library. The trade-off is that Xdebug gives you raw coverage and no processing layer. The Filter, the CodeCoverage object and the Report facade are what this project adds on top: file selection, accumulation across runs, and a renderer. If you only need raw line hits and are already parsing them yourself, the extra layer buys you little.

## Maintenance, upgrades and licence

The repository is not archived, and the last push to main was on 2026-09-21. Releases listed are 14.3.3 on 2026-09-10, 14.3.2 on 2026-09-04 and 14.3.1 on 2026-08-16. The version numbers move in the major-minor-patch pattern, and a ChangeLog-14.4.md file sits at the top level of the repository, which indicates the next minor line is already in preparation. The presence of renovate.json and a .phive/ directory suggests dependency updates and pinned tooling are part of the maintenance routine.

Upgrade cost is dominated by the major version, not the patch releases. Patch releases within 14.3.x should be routine for a library that is consumed transitively through PHPUnit. A major bump is where you would expect API movement, and the README's usage examples are the surface you would need to re-check against the new version. The README does not document a deprecation policy or a supported-version window, so the ChangeLog files in the repository are the practical source for what changed.

The licence is BSD-3-Clause. That is permissive and permits commercial use and redistribution with the licence text retained; it is not a copyleft licence and does not require you to publish your own source. This is a description of the licence identifier, not legal advice, and the LICENSE file at the repository root is the authoritative text.

## Conclusion

Adopt it if you are building a PHP test runner, a CI coverage service or a custom reporting pipeline that needs coverage data as objects rather than as a rendered HTML tree. Do not adopt it as a replacement for running PHPUnit: it is the engine, not the runner, and the README shows no test discovery, no assertions and no CLI. Before you commit, verify which driver the Selector returns on your machine, because the README documents no fallback when none is present, and check the Report\Facade methods in the source for the output format you need.

## FAQ

### How do I install phpunit/php-code-coverage?

Add it with Composer, using the development-time form if you only need it for your test suite. The README gives both composer require phpunit/php-code-coverage and composer require --dev phpunit/php-code-coverage.

### How do I generate a code coverage report with phpunit/php-code-coverage?

Build a Filter with includeFiles(), construct a CodeCoverage object using the driver returned by DriverSelector::forLineCoverage(), bracket the code with start() and stop(), then call ReportFacade::fromObject($coverage)->renderOpenClover() with an output path. The README shows exactly this sequence.

### Can I render a report from coverage data that was saved earlier?

Yes. The README's second example unserializes a file with the Serialization\Unserializer class and passes the result to ReportFacade::fromSerializedData() before rendering OpenClover output.

### What does it mean when no code coverage driver is available?

The driver is chosen by DriverSelector::forLineCoverage() from what the PHP runtime offers, and the README documents no fallback and no way to configure a specific driver. If nothing usable is present, construction of the CodeCoverage object is where the run fails.

### Is phpunit/php-code-coverage the same thing as PHPUnit?

No. It is a library that provides collection, processing and rendering of coverage data, and PHPUnit is the test runner that uses it. It has no test discovery, no assertions and no command line interface of its own.

## Sources

- [Issues](https://github.com/sebastianbergmann/php-code-coverage/issues)
- [License: BSD-3-Clause](https://github.com/sebastianbergmann/php-code-coverage/blob/main/LICENSE)
- [README](https://github.com/sebastianbergmann/php-code-coverage/blob/main/README.md)
- [Releases](https://github.com/sebastianbergmann/php-code-coverage/releases)
- [sebastianbergmann/php-code-coverage on GitHub](https://github.com/sebastianbergmann/php-code-coverage)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sebastianbergmann-php-code-coverage
