Quick Links

Using third-party packages can accelerate your project's development. Sometimes, you might need to add extra functionality or fix a critical bug. Here's how to apply patches to PHP packages installed by Composer.

When to Patch a Package

First consider whether it's appropriate to create an in-project patch. A patch should always be a minor change. If you need to add extensive new functionality, you should open an issue against the package or fork it yourself. This will help avoid conflicts as the package evolves.

You should think carefully about whether the change you require is truly specific to your project. Dependency patches typically affect only a few lines of code. They address bugs and issues that would otherwise prevent you from using the package. Most patches are short-term in nature. They should be replaced later by at-source fixes within the package's codebase.

Ultimately, you need to measure your intentions. Are you addressing an immediate issue with a package, or extending the package's functionality to align with your codebase? If it's the latter, you should consider either contributing to the package, or wrapping it with extra classes and functions that are first-class citizens in your project.

Creating a Composer Patch

Once you've made the call to patch, you can start to prepare your project. Composer doesn't have built-in patch support, so we'll use the popular simplify/vendor-patches project to add it. This provides an intuitive interface over

        cweagans/composer-patches
    

to help you create new patches.

composer require --dev symplify/vendor-patches

Make sure the package you need to patch is installed in your project:

composer require example/broken-package

Next, open the problematic file in your code editor. You'll find it within the vendor directory. In our example, we need to edit vendor/example/broken-package/src/Broken.php:

<?php
    

class Broken {

public function __construct(string|int $foo) {

if (is_string($foo)) {

echo "Valid value!";

}

}

}

?>

The Broken class is expected to accept both strings and integers, using PHP 8's union types. Unfortunately, looking at the source shows it actually only accepts strings.

In our fictitious example, the package maintainer has acknowledged the issue but hasn't yet created a new release. Let's patch our way around the problem in the meantime.

Copy the broken file, unaltered, and add a .old suffix:

cp vendor/example/broken-project/src/Broken.php vendor/example/broken-project/src/Broken.php.old

Make sure you don't edit the .old file!

Next, edit the original file so it functions correctly in your codebase. As you're editing in-place, the changes take effect immediately. Verify your codebase now behaves as expected.

<?php
    

class Broken {

public function __construct(string|int $foo) {

if (is_string($foo)) {

echo "Valid value!";

}

else if (is_int($foo)) {

echo "Also valid!";

}

}

}

?>

Creating the Patch File

You can now use the symplify/vendor-patches project to create a patch file for your fix. The package provides a vendor-patches binary which automatically scans your vendor directory to find the changes you've made.

vendor/bin/vendor-patches generate

Running the command will generate a diff for you. It'll be saved to the patches directory at the root of your project. The diff is calculated by comparing the .php and .php.old files you've created.

You can run the command again to discover any new patches you add. Each changed file gets its own patch within the patches directory.

Automatic Patching

Within your composer.json file, you'll see a new section has been added:

{
    

"extra": {

"patches": {

"example/broken-package": [

"patches/example-broken-package-src-broken-php.patch"

]

}

}

}

The patches object maps the names of installed packages to an array of patch files to apply. These patches will be applied automatically whenever you composer install.

An installation script is registered by simplify/vendor-patches. It gets invoked after each dependency is installed. The script looks to see if any patches have been defined for the package. It'll automatically apply any it finds.

Screenshot of patches being applied during a Composer install

You'll see lines in the composer install output indicating when patches have been applied. If an error is reported, you can use composer install --verbose to get more information on why the patch was skipped. This is often due to a package update fixing the issue you patched, making your patch file redundant.

Removing Patches

You can temporarily remove a patch by deleting its line from composer.json. This will stop the patch being applied when you composer install.

To permanently delete a patch from your project, remove its composer.json line. You can then remove its .patch file from your project's patches directory.

If you want to revert a patch you've applied locally, it's simplest to follow the steps above to delete or disable the patch. You can then remove the package from the vendor directory and run composer install to get back to a clean slate.