Composer Patches in Laravel: Fix Vendor Code Without a Fork

7 min read Auf Deutsch lesen

A third-party package has a bug. The fix is a single line in vendor/. You change it, everything works, you move on. On the next composer update the line is gone again.

Most Laravel teams hit this wall eventually. Between “hack into vendor/” and “fork the whole package” sits a clean middle ground: a patch that gets re-applied automatically on every Composer run.

Why vendor/ is no place for fixes

vendor/ belongs to Composer, not to you. The folder sits in .gitignore, never gets committed, and is rebuilt from the package sources on every composer install. A change in there lives on your machine only.

The problem surfaces the moment someone else pulls the branch. Their vendor/ has never heard of your fix. CI builds fresh, the fix is missing, the build goes green, the feature is still broken. A bug fix that is not in Git is not a bug fix. It is a time bomb.

Composer Patches: the clean middle ground

The cweagans/composer-patches plugin solves this at the right layer. You keep your fix as a .patch file in the repository. The plugin re-applies that patch after every composer install and composer update, right after the package has been unpacked.

Install it:

composer require cweagans/composer-patches

The mapping from package to patch lives in composer.json. The patch files themselves usually sit in a patches/ folder:

{
    "extra": {
        "patches": {
            "acme/pdf-generator": {
                "Null check when rendering empty pages": "patches/pdf-generator-empty-page.patch"
            }
        }
    }
}

Both the composer.json mapping and the .patch file get committed. Every teammate and every CI environment picks up the fix automatically.

New in 2.0: patches.json and a lockfile

cweagans/composer-patches 2.0 shipped in October 2025. Two changes matter for teams.

First, a dedicated patches.json. Instead of keeping patch definitions in composer.json, you can move them into a separate file. That keeps your composer.lock quiet: adding a patch no longer churns the lockfile.

Second, a patches.lock.json. This file is the counterpart to Composer’s own lockfile. It records a SHA-256 checksum for every applied patch. Every environment now installs a provably identical patch state. A patch that was silently changed or that slipped out of place gets caught instead of sliding through unnoticed.

Under the hood, 2.0 applies patches through Git rather than the system patch tool, so behavior is consistent across operating systems. Your existing extra.patches config from the 1.x days keeps working — the upgrade is backwards-compatible.

Creating a patch

A patch is a unified diff, exactly what git diff produces. You change the file inside vendor/ and capture the change:

git diff --no-color vendor/acme/pdf-generator/src/Renderer.php > patches/pdf-generator-empty-page.patch

Then you revert your change in vendor/ and run composer install. The patch goes back in cleanly.

Diffing by hand gets tedious across several changes. symplify/vendor-patches handles it for you: you edit the vendor files, one command scans vendor/, writes out the diffs, and adds the mapping to your composer.json.

composer require symplify/vendor-patches --dev
vendor/bin/vendor-patches generate

The usual traps

Patches are version-bound. They hang on specific line numbers and environments. After a package update, the same patch may no longer fit.

ProblemCauseFix
Patch stops applying after an updateLines in the package shiftedSet "composer-exit-on-patch-failure": true so the Composer run fails loudly instead of carrying on quietly
Patch can’t find the fileWrong -p/path-strip level; the a/ and b/ prefixes in the diff don’t line up with the pathCheck the diff: generate it relative to the package root, not the project root
Patch grows hugeYou are adding a feature, not fixing a bugDon’t patch — see below

The setting that matters most is composer-exit-on-patch-failure. Without it, Composer swallows a failed patch and keeps building. Then you get a green build without the fix. Exactly the state you set out to avoid.

When not to patch

A patch is meant for small deltas: a null check, a wrong default, a forgotten escape call. The moment you rewrite several methods or add behavior, the patch is the wrong tool.

At that point the fix belongs upstream. Open a pull request against the package. Until it is merged, a patch bridges the wait. After that, it comes back out. A patch is a bridge, not a home.

What to take into your Laravel project

  • keep fixes to third-party packages in Git, not in a local vendor/;
  • one patch stays a small, well-defined delta;
  • on new projects, use patches.json plus the lockfile for a quiet composer.lock;
  • set composer-exit-on-patch-failure: true so stale patches fail loudly;
  • go upstream and open a PR as soon as the patch grows.

If your Laravel project needs to stay clean at spots like these, we build Laravel projects and custom software where package maintenance is not left to chance.

Frequently Asked Questions

Why shouldn't you edit vendor/ directly?

Because the folder belongs to Composer, not to you. It sits in .gitignore, never gets committed, and is rebuilt from the package sources on every composer install. A change in there lives on your machine only. On a team, a colleague pulls the branch without your fix and CI builds fresh without it – the build goes green, the feature is still broken. A bug fix that isn't in Git is not a bug fix.

How do you cleanly patch a Composer package in Laravel?

With the cweagans/composer-patches plugin. You keep your fix as a .patch file in the repository (usually in a patches/ folder) and add the package-to-patch mapping in composer.json. The plugin re-applies the patch after every composer install and composer update, right after the package has been unpacked. You create the .patch file as a unified diff, i.e. with git diff.

What's new in cweagans/composer-patches 2.0?

Three things: a dedicated patches.json, so adding a patch no longer churns the composer.lock; a patches.lock.json that records a SHA-256 checksum per patch so every environment installs a provably identical patch state; and applying patches through Git instead of the system patch tool, which makes behavior consistent across operating systems. Existing extra.patches config keeps working, backwards-compatible.

When should you not patch a package?

A patch is meant for small deltas – a null check, a wrong default, a forgotten escape call. The moment you rewrite several methods or add behavior, the patch is the wrong tool. Then the fix belongs upstream as a pull request against the package; a patch only bridges the wait until that PR is merged. A patch is a bridge, not a home.

Tobias Kokesch
Tobias Kokesch

Sr. Full-Stack Developer

Frontend & Backend. Builds scalable web applications with a focus on clean architecture and maintainability.