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

Backward Compatibility Check Laravel Package

roave/backward-compatibility-check

A CLI tool that compares two versions of a PHP library and flags backward-compatibility breaks. It reads git tags and composer.json autoload paths, then fails CI builds when public API changes would break downstream consumers.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a runtime dependency you wire into a service provider — it's a CLI binary that diffs the public API of two git revisions of your own package. For a Laravel developer, that means it's only worth installing in repositories that are consumed by someone else: a first-party Laravel package (facade, contracts, service provider), an internal shared acme/laravel-support library, or an app that other teams extend via Composer.

Install it as a dev dependency:

composer require --dev roave/backward-compatibility-check

Before the first run, verify the three preconditions the tool silently assumes:

  1. The repo is a git repo with SemVer tags in strict x.y.z form (1.4.2, not v1.4, not release-1.4).
  2. Every source directory is declared under "autoload" (or "autoload-dev") in composer.json — anything outside autoload is invisible to the comparison.
  3. Your work is committed. The tool reads git objects, not your working tree, so uncommitted edits are simply not part of HEAD.

Then run it:

vendor/bin/roave-backward-compatibility-check

With no arguments it finds the most recent minor tag and compares that tag's API against HEAD, exiting non-zero if it finds a break. That's the whole first use case: you are about to tag 1.5.0 and want to know whether you actually need 2.0.0.

Make --help your second stop — it's the authoritative list of flags (--from, --to, --format, and the dependency-installation options change between majors):

vendor/bin/roave-backward-compatibility-check --help

Implementation Patterns

PR gate with proper tag history. The single most common integration. The only non-obvious part for Laravel devs is that actions/checkout defaults to a shallow clone with no tags, which makes the tool pick the wrong base (or none at all):

jobs:
  bc-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # required: tags must be present
      - uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/roave-backward-compatibility-check --format=github-actions

--format=github-actions emits ::error file=...,line=...:: annotations, so a broken signature on src/Contracts/ReportGenerator.php shows up as an inline comment on the diff instead of a wall of console text. Use the Nyholm/roave-bc-check-ga action if you'd rather not manage PHP on the runner.

Changelog generation as a release ritual. Laravel packages usually ship a CHANGELOG.md. Instead of hand-writing the "Changed" section, generate it:

vendor/bin/roave-backward-compatibility-check --format=markdown > bc-report.md

Paste the break list into the changelog entry and, more importantly, into the upgrade guide — that's the text consumers actually read when composer update explodes.

Baseline file for accepted breaks. Deliberate breaks happen (dropping PHP 8.1, renaming a contract method). Don't disable the check — record the exception in .roave-backward-compatibility-check.xml at the repo root:

<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="vendor/roave/backward-compatibility-check/Resources/schema.xsd">
    <baseline>
        <ignored-regex>#\[BC\] CHANGED: The parameter \$a of App\\Contracts\\ReportGenerator\#generate\(\)#</ignored-regex>
    </baseline>
</roave-bc-check>

Treat this file like a psalm/phpstan baseline: it should shrink, never grow silently. Review additions in code review the same way you'd review @phpstan-ignore.

Compare explicit revisions instead of relying on tag detection. Useful when your tags aren't SemVer-shaped, or on a long-lived 2.x branch where "last minor tag" is stale:

vendor/bin/roave-backward-compatibility-check --from=1.4.0 --to=HEAD

Know what it actually checks in a Laravel codebase. It compares class-level API: class/interface/trait/enum existence, final/abstract flags, parent classes and implemented interfaces, public and protected method signatures, parameter names/types/defaults, return types, constants, and (since 8.8.0) enum cases. That covers the parts of a Laravel package consumers actually compile against — contracts, facades' real methods, DTOs, custom cast classes, abstract base classes.

It does not check config/*.php keys, Blade views, published migrations, route names, translation keys, or container binding strings. Those are your package's real API too, and you still need contract tests for them. Renaming config('report-generator.driver') to config('reports.driver') is a breaking change the tool will happily miss.

Gotchas and Tips

Wrong or missing base version is the #1 cause of confusing output. Symptoms: "no BC breaks" on a release that obviously broke something, or breaks reported against code you deleted two releases ago. Cause is almost always a shallow clone or a v-prefixed tag like v1.4.0. Check what it picked before trusting the result, and make sure your tags look like 1.4.0.

It installs dependencies twice. The tool checks out both revisions into temporary directories and runs composer install in each. Consequences: it needs network access and credentials for any private VCS repositories, it's slow (a minute or more on a Laravel app pulling laravel/framework), and it fails outright if the old revision's composer.lock no longer resolves — e.g. an abandoned package, a yanked version, or a PHP constraint the runner's PHP can't satisfy. The practical fix is a Docker escape hatch that pins a compatible PHP:

docker run --rm -v "$(pwd)":/app nyholm/roave-bc-check

A related trap: bumping php or laravel/framework in composer.json can make the two revisions unresolvable against each other, and you'll get an installation error rather than a BC report. Bump the requirement in a dedicated PR, tagged separately, rather than mixed into a feature branch.

Run it on your minimum supported PHP version. The tool reflects on real code; if you develop on PHP 8.3 but support 8.2, syntax introduced in 8.3 in the old revision can't be parsed on a 8.2 runner. Align the BC-check job's PHP version with the php constraint floor in composer.json.

Signature-only, not behaviour. It'll flag adding a required parameter to a method (new in 8.9.0) but it won't tell you that you changed a return type's meaning, tightened a validation rule in a FormRequest, or changed what a service provider binds. It's a safety net for the compile-time contract, not a replacement for a test suite or a semver policy.

Enum additions are flagged as breaks now (8.8.0). Adding a case to a backed enum is technically additive, but consumers doing exhaustive match will fatal. If you add cases on purpose in a minor, expect the report — that's the tool being right, and your release notes need to say so.

protected is API. The tool counts protected methods and properties as part of the surface because subclasses depend on them. Laravel packages with abstract base classes and protected function bootPackage()-style extension points will surface breaks that feel internal. They aren't — document them or move them behind a narrower contract.

Watch for test artifacts leaking in. If you have autoload-dev classes with public API (fixtures, stubs, TestArtifact\TheClass in the schema example), they can appear in reports. Keep fixtures in paths that aren't autoloaded, or baseline them.

XML escaping in baseline regexes. The patterns use #...# delimiters, and every backslash in a PHP FQCN must be doubled, $ in a parameter name must be \$, and # inside a class name breaks the delimiter. Copy the exact string from the tool's output rather than hand-writing it.

Never || true the check. Non-zero exit is the entire contract. If a break is acceptable, baseline it with a regex that names the specific symbol — a broad #.*# ignore turns the whole tool into decoration.

Pin and update deliberately. The package's own minimum PHP and Symfony version requirements move with majors; a Renovate bump of symfony/console in a Laravel app can collide with laravel/framework. Keep it on a ^X.Y constraint in require-dev, upgrade it as its own PR, and re-read --help after the major bumps since flags like the dependency-installation mode have changed between releases.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle