Patch it!
To patch files from the vendor directory – as the headline suggests – you will need two things:
- A composer plugin, called composer-patches
- A folder where you store your patches. (eg.
patches
)
So how does it work? First, add the composer plugin by running
composer require cweagans/composer-patches
Then create a new folder called patches
in the project root folder (so on the same level as your composer.json
). This is where you will add your patches, that you want applied to files in vendor.
So where do you get patch files from? You can either create them yourself, download them from Github, or maybe the support staff of your favorite sent them to you directly.
Manual Diff
Creating a Diff file is quite easy. Let’s say the file that needs a fix is vendor/company/project/README.md
.
First, let’s make a copy of the original file before editing it.
cp vendor/company/project/README.md vendor/company/project/README.md.old
Now make your modifications to the file. When done, create the diff:
diff -Naur vendor/company/project/README.md.old vendor/company/project/README.md > patches/company-project.diff
Github
On Github, you have the option to append .diff
or .patch
to the URL of a a single commit which will give you a pre-made patch file. Of course, this requires that you or someone else has created the patch that your looking for.
Support Staff
Let’s say someone reports a bug to a CMS vendor, they fix the bug but haven’t made a bugfix release yet. They might already be able to provide you with a patch file prior to the patch being released.
Let’s continue
Now, all you need is to tell Composer in your composer.json which file is supposed to fix which package:
{
"extra": {
"patches": {
"company/project": {
"This is the description of the patch": "patches/company-project.diff",
"This is another patch": "patches/another.patch"
}
}
}
}
Finally, run composer install
.
- Pro
- Solid method to override all sorts of files from vendor(not just PHP classes)
- Not an entire file is overwritten, just the lines that need a fix.
- Once there are new core package releases, the patch will be re-applied. If there’s an error, you get a warning message
- Contra
- On each update the core package is removed, in order to re-apply the patch
Autoload it!
Another quick way to override PHP classes from the vendor folder is to simply exclude a class from autoloading and then add your custom version of the file.
{
"autoload": {
"psr-4": {
"App\\": "app/"
},
"exclude-from-classmap": ["vendor/company/project/SomeClass.php"],
"files": ["patches/SomeClass.php"]
}
}
Remember that the namespace within patches/SomeClass.php
needs to match whatever the original was. Now when you run composer dump-autoload
the original class will be replaced in its entirety with your file.
Since some people might be using Symfony, I should mention that this feature is built into the framework directly, there. How it works there is, you define a service definition for the class in config/services.yaml
like this:
Vendor\Namespace\ClassName:
class: App\Override\Namespace\ClassName
another.service.id:
class: App\Override\AnotherNamespace\ServiceName
This tells Symfony to load your custom class whenever the Vendor\Namespace\ClassName is to be instantiated, or a service.id is being resolved to its class. While the classes don’t have the same namespace, they do need the same footprint (eg: implement the same interface, etc.)
- Pro
- Fast and solid method to override PHP classes.
- Fast. No need for patches or diffs
- Works in any Composer-based project
- No need for composer extensions
- Contra
- only works with PHP classes
- the complete PHP Class needs to be maintained and will require manual updates
- You don’t get any feedback or indications as to when the original file has modifications in a new release of the vendor package. Things could simply break.
Preserve it!
Another option is to use drupal-composer/preserve-paths
which is a Composer plugin as well. It’s description reads “A composer plugin for keeping specified files and directories when installing/updating new composer packages.” and that is exactly what it does. Before a composer update or install is executed, it looks into your composer file under extras > preserve-paths, creates temporary backups of these files, and after the composer install/update is finished, it restores the backed-up files in its place.
{
"extra": {
"preserve-paths": [
"vendor/company/project/README.md",
"vendor/another/example/filename.php
]
}
}
One thing to keep in mind here, is for the preserve-paths plugin to work, you will need the file that is supposed to be preserved under version control. Which is why it works great with WordPress or Drupal for example, where you can define install-paths for specific package types (read more about how I use this plugin with WordPress here).
If you need to override vendor files, though, the only viable solution is to include the vendor folder in your repository, by removing the vendor/
entry in your .gitignore
file.
- Pro
- Easy to use and configure
- Edit the file directly in place, no need for a patches folder.
- Works with any file, not just PHP classes. (eg: wp-config.php, .env)
- Contra
- the complete be maintained and will require manual updates
- You don’t get any feedback or indications as to when the original file has modifications in a new release of the vendor package. Things could simply break.
- The complete vendor folder needs to be under version control, which makes your repository bigger.
- If the composer install/update fails for some reason, chances are, that the part where your modified file is to be restored might fail, too. This is just something to look out for.
Fork it!
When you find a bug in a vendor package and are able to fix it yourself, you should definitely go down this route. Contributing a bugfix to a vendor package doesn’t only make your live better but everyone that potentially runs into the bug you discovered can benefit from your improvement.
Follow the contribution guidelines on Github for an in-depth tutorial on how this works.
If a package is actively maintained, your Pull Request might require changes, or might just get accepted as is, which should result a new Bugfix release. If the PR is not accepted in a timely fashion, you still have the possibility to temporarily switch from the original package to your forked and fixed repository.
Now, in your project which used the buggy package, remove that one with composer remove vendor/buggy-package
and then add an entry to the repositories section:
"repositories" : [{
"type" : "package",
"package" : {
"name" : "your-vendor/package-name",
"source" : {
"url" : "https://github.com/your-vendor/package-name",
"type" : "git",
"reference" : "dev-branch-name-with-fix"
}
}
}]
Finally install your fork: composer require your-vendor/package-name
- Pro
- You’re actually making a contribution to a fellow open source package making the internet a better place not just for you but for everyone.
- Contra
- Switching between original and forked repository is a bit of a hassle.
- Switching back to the original package is required after your PR was merged and released if you want to new releases.
- Sometimes you don’t have the time to wait for a PR being merged and released
Decorate it!
And for the sake of completeness, there’s Symfony Decorators, which is probably the most powerful way to modify parts of a vendor package Service Class. There’s extensive documentation on this.
It comes with its own pitfalls and gotchas, if a vendor package uses the buggy class by referencing the concrete class instead of an Interface, for example.