rgarciar1931 / vendor-patches
Framework-agnostic CLI and Composer plugin to apply, revert and track the status of git patches against vendor/ dependencies in any PHP project.
Package info
github.com/rgarciar1931/vendor-patches
Type:composer-plugin
pkg:composer/rgarciar1931/vendor-patches
Requires
- php: ^8.1
- composer-plugin-api: ^2.0
- symfony/console: ^6.4 || ^7.0
- symfony/filesystem: ^6.4 || ^7.0
- symfony/process: ^6.4 || ^7.0
Requires (Dev)
- composer/composer: ^2.7
- friendsofphp/php-cs-fixer: ^3.59
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-07-17 11:31:35 UTC
README
A framework-agnostic Composer CLI and plugin to apply, revert, and track the status of
git apply-based patches against vendor/ (or any other) files in a PHP project — including dependency ordering and a transactional
apply command.
Why
Third-party dependencies sometimes need a local hotfix before an upstream release is available.
Keeping those hotfixes as plain .patch files, applied by hand, doesn't scale: there's no record
of what's applied, no ordering between patches that depend on each other, and no safe way to
apply "all of them" without risking a half-patched tree if one fails partway through.
vendor-patches formalizes that workflow with a small JSON manifest and three CLI commands.
Installation
composer require rgarciar1931/vendor-patches
This installs the vendor-patches binary at vendor/bin/vendor-patches and registers a
Composer plugin that applies any pending patches automatically after composer install/update
(see Auto-apply configuration to disable or tune this).
Quick start
-
Create a manifest at
patches/patches.jsonin your project root:{ "patches": [ { "id": "curl-100-continue-status", "name": "Fix cURL 100-continue real HTTP status", "description": "Corrects HTTP status parsing when a server replies with an interim 100-continue header.", "type": "fix", "sortOrder": 10, "dependencies": [], "file": "curl-100-continue-real-status.patch", "target": ".", "strip": 1 } ] } -
Put the corresponding
.patchfile next to it (patches/curl-100-continue-real-status.patch), generated the normal way withgit diff(orgit diff --no-index) so it has standarda//b/path prefixes. -
Check status, apply, and revert:
vendor/bin/vendor-patches patch:status vendor/bin/vendor-patches patch:apply --all vendor/bin/vendor-patches patch:revert curl-100-continue-status
Commands
patch:apply [ids...] [--all] [--no-cascade] [--yes] [--manifest=] [--project-root=]
Applies one, several, or (--all) every patch, in dependency order.
- If a requested patch depends on one that isn't applied yet, it's automatically pulled into
the plan and applied first. Pass
--no-cascadeto instead fail fast, naming the missing dependency. - Transactional: if any patch in the plan fails (a real
git applyfailure, or a detected conflict), everything applied earlier in that same run is immediately reverted, in reverse order, before the command exits non-zero. A failed run never leaves the tree partially patched. - Prompts for confirmation with the full expanded plan before touching anything, unless
--yesis passed or the session isn't interactive.
patch:revert [ids...] [--all] [--no-cascade] [--yes] [--manifest=] [--project-root=]
Reverts one, several, or (--all) every applied patch.
- If a requested patch has applied dependents, they're automatically reverted first (cascade).
Pass
--no-cascadeto instead fail fast, naming the blocking dependent. - Unlike
patch:apply, a failure here is reported per-patch rather than triggering a rollback (reverting is already the "undo" operation) — but any patch that transitively depends on a failed one is skipped rather than reverted, to avoid leaving a dependent applied on top of a reverted prerequisite.
patch:status [--type=] [--manifest=] [--project-root=]
Shows every patch's live status (applied, not_applied, or conflict), computed fresh each
run via git apply --check / --check --reverse — nothing is cached or persisted, so status is
always accurate even if a patch was applied or reverted outside this tool. --type filters by
the patch's type field.
Manifest schema
Default location: patches/patches.json in the project root (configurable, see below). See
resources/manifest.schema.json for the full JSON Schema.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string | yes | — | Unique id, ^[a-z0-9][a-z0-9._-]*$. Used on the CLI and as the dependency-graph key. |
name |
string | yes | — | Short human title. |
description |
string | no | "" |
Longer free-text explanation. |
type |
string | no | "fix" |
Free-form category (fix, security, performance, compatibility, ...). |
sortOrder |
integer | no | 0 |
Tiebreaker within the dependency-respecting order. |
dependencies |
string[] | no | [] |
Ids of other patches that must be applied first. |
file |
string | yes | — | Path to the .patch file, relative to the manifest's own directory. |
target |
string | no | "." |
Base directory (relative to the project root) used as the cwd for git apply. |
strip |
integer | no | 1 |
The -p<N> level passed to git apply. |
Auto-apply configuration
The Composer plugin runs patch:apply --all non-interactively after composer install/update.
Configure it via environment variables (highest precedence) or extra.vendor-patches in your
project's composer.json:
{
"extra": {
"vendor-patches": {
"auto-apply": true,
"manifest": "patches/patches.json",
"strict": false
}
}
}
| Setting | Env var | extra.vendor-patches key |
Default | Meaning |
|---|---|---|---|---|
| Auto-apply | VENDOR_PATCHES_AUTO_APPLY |
auto-apply |
true |
Whether the plugin runs at all. |
| Manifest path | VENDOR_PATCHES_MANIFEST |
manifest |
patches/patches.json |
Manifest location, relative to the project root (or absolute). |
| Strict | VENDOR_PATCHES_STRICT |
strict |
false |
Whether a failed auto-apply aborts composer install/update (non-zero exit) or just warns and continues. |
Env vars accept 1/true/on/yes or 0/false/off/no (case-insensitive). Manual CLI usage always
works regardless of these settings — they only gate the automatic post-install/update hook.
If no manifest file exists at the resolved path, the plugin silently does nothing.
Requirements and known limitations
- Patches must be standard
git diff/git format-patch-style unified diffs witha//b/path prefixes — rawdiff -uoutput without those prefixes won't apply even if content-equivalent. targetmust be inside a git working tree; there's no fallback (e.g. a plainpatchCLI driver) for non-git directories in this version.- Only tested on Linux/macOS; Windows should work via Symfony Process but isn't CI-verified yet.
- Manifest and patch files are only ever read locally — this tool never fetches patch content from a remote source.
Development
composer install composer test # PHPUnit (unit + integration) composer phpstan # Static analysis (level 6) composer cs-check # PHP CS Fixer, dry-run
License
MIT, see LICENSE.