← back

Apply Patches to Vendor Libraries using Composer

hamburg

When working with PHP packages, you'll occasionally encounter bugs or missing features in dependencies that you cannot modify directly, because they live in the vendor/ directory, which is regenerated on every install.

However, there is a way to patch vendor files using Composer patches, not manual edits. This ensures your fixes are consistent, repeatable, and version-controlled.

Why You Should Not Edit Vendor Files Directly

The vendor/ directory is managed entirely by Composer, meaning:

Instead, create patch files and instruct Composer to apply them automatically.

Requirements

To apply patches, you'll need the Composer Patches plugin:

composer require cweagans/composer-patches:^2.0

This plugin lets you attach patch files to package versions in your composer.json.

Step 1: Make Your Patch File

Composer installs packages from dist (a zip archive), so there is no Git history inside vendor/ to diff against. Create one first:

cd vendor/vendor-name/package-name
git init
git add .
git commit -m "Original package"

Now make the change you want directly in the vendor files, and write the diff into a patches/ directory in your project root:

git diff > ../../../patches/fix-bug.patch

If your change adds new files, stage them first so they end up in the diff:

git add -A
git diff --cached > ../../../patches/feature.patch

Using Git here is not incidental. Patches are applied with patch -p1 from the package root, which strips exactly one leading path component, and git diff writes its paths with the a/ and b/ prefixes that -p1 expects.

If you'd rather use plain diff, compare two copies of the package directory so the output carries that same leading component:

diff -ruN original/ modified/ > patches/feature.patch

What will not work is diffing a single file, because its paths have no leading directory for -p1 to strip:

diff -u OriginalFile.php ModifiedFile.php > fix-bug.patch

Step 2: Register the Patch in composer.json

Add a "patches" section under "extra":

{
  "extra": {
    "patches": {
      "vendor-name/package-name": {
        "Fix bug in package-name": "patches/fix-bug.patch"
      }
    }
  }
}

This tells Composer:

Step 3: Apply the Patch with Composer

Run:

composer install

or

composer update vendor-name/package-name

The patches plugin will:

Step 4: Commit

You should commit the patch file and the changes to composer.json and composer.lock.

Example Folder Structure

project/

├── patches/
   └── fix-bug.patch

├── composer.json
├── composer.lock
└── vendor/

Troubleshooting

Patch fails to apply

Start with the plugin's own diagnostics, which check for the most common misconfigurations:

composer patches-doctor

To see the full output of the patching process, run:

composer install -vvv

Common causes:

The patch never runs at all

Since Composer 2.2, plugins only execute if they are explicitly allowed:

{
  "config": {
    "allow-plugins": {
      "cweagans/composer-patches": true
    }
  }
}

composer require asks about this interactively and records your answer, so it usually just works on your machine. If that entry never gets committed, CI runs non-interactively, the plugin is skipped without complaint, and no patch is ever applied. This is the most common reason a patch works locally but not on the build server.

Patch edited but changes not visible

Patches are applied when a package is installed, so editing a .patch file does nothing to a package already sitting in vendor/. Force a re-apply:

composer patches-repatch

If you changed which patches are defined rather than their contents, refresh the lock file first:

composer patches-relock

If the patch is applied and you still see the old behaviour, clear your framework caches before digging any further.

Alternative: Use a Fork (When Patches Aren't Enough)

Patches work best for small, surgical changes. If your fix spans many files, or you need to keep it in step with upstream over time, fork the package instead.

Point Composer at your fork and require the branch your fix lives on:

{
  "repositories": [
    {
      "type": "vcs",
      "url": "https://github.com/yourname/package-name"
    }
  ],
  "require": {
    "vendor/package-name": "dev-my-fix"
  }
}

The repositories entry on its own changes nothing. Composer only resolves to the fork once the constraint in require points at a branch that exists there.

Keep in mind that a fork is a standing maintenance cost, as every upstream release becomes yours to merge. If all you need is a local copy to develop against, path repositories are the lighter option.

Whichever route you take, open a pull request upstream. Once the fix is released, you get to delete the patch or the fork entirely.

Summary

Composer patches keep vendor fixes reproducible: capture the change as a patch, register it in composer.json, and let Composer re-apply it on every install. The vendor/ directory stays disposable, which is exactly what it is meant to be.

References