symplify/vendor-patches
Symplify Vendor Patches generates vendor patches for your Composer packages with a single command. Copy a vendor file with a .old suffix, edit the original, then run generate to create patch files and auto-configure cweagans/composer-patches.
Architecture fit. symplify/vendor-patches sits entirely outside the application runtime. It is a dev-time generator: you edit a file in vendor/, preserve the pristine copy as <file>.old, run vendor/bin/vendor-patches generate, and it emits a unified diff into patches/ plus the extra.patches block in composer.json. The actual re-application at install time is performed by cweagans/composer-patches, not by this package. That distinction matters for evaluation: this is a thin, single-purpose code generator, and the operational risk profile is dominated by the Composer plugin it feeds, not by the generator itself. There is no Laravel coupling whatsoever — no service provider, no config file, no container binding — so it fits any Composer-based PHP stack equally.
Integration feasibility. High for the narrow case, medium for the general case. Installing is a two-line Composer command measured in minutes. The harder work is process: deciding when patching is permitted, who owns the patch register, how patches are reviewed, and how they are retired. A team can be productive on day one with a single patch, but a sustainable practice requires a written policy and a CI check. Budget roughly one engineering day for adoption (pilot patch, plugin allow-list, CI verification, runbook) and treat the ongoing cost as an interest payment on technical debt.
Technical risk.
| Risk | Severity | Notes |
|---|---|---|
| Patch silently not applied in production | High | Occurs if cweagans/composer-patches is installed with --dev while deploys run composer install --no-dev. The app boots, the bug is "fixed" in staging, and production is unpatched. This is the single most dangerous failure mode because it has no error signal. |
| Patch drift after dependency upgrade | Medium | Hunk #N FAILED breaks the build loudly, which is safe but disruptive. Each bump of a patched package is a manual reconciliation. |
Workflow state lives only in a gitignored vendor/ |
Medium | There is no ledger of "which files are currently hand-edited." The .old suffix convention is the only marker, and generate will happily patch anything that differs. |
| Silent key mismatch for private/path repos | Medium | Without --resolve-from-directory, the extra.patches key won't match an installed package; patch generates, applies to nothing. |
Composer plugin blocked by allow-plugins |
Low | Deterministic and immediately visible once you know the cause. |
| Generator abandonment / supply chain | Low | MIT, small surface, and the output artifact is a plain diff — exit cost is minutes. |
| cweagans v1→v2 format break | Low–Medium | Existing --- /dev/null patches must be rewritten to --- <file-path>; migration errors look like path-stripping bugs. |
Key questions for the adopting team.
composer install with dev-dependencies excluded, or does anything ship a pre-built vendor/ artifact?cweagans/composer-patches an accepted allow-plugins entry in our Composer policy, and is that policy enforced centrally?patches/ in code review, and is a .patch file treated as reviewable source?path repositories where the installed directory name diverges from composer.json name?vendor/ today, and is that practice documented anywhere?Stack fit. Compatible with Composer 2 and Laravel 10/11/12; the current release targets PHP 8.4, so teams on 8.1–8.3 should pin a compatible version rather than float. It works cleanly with Forge and Envoyer, which run composer install as part of the deploy. Laravel Vapor builds during the deployment pipeline and will also apply patches, but only if the plugin is a production dependency. The one environment to avoid is anything that caches and reuses a vendor/ directory between builds — a stale cache will serve unpatched or wrongly patched code with no error.
Migration path. Do not roll this out as a general capability. Roll it out as a controlled remedy for one specific, currently-blocking upstream defect. The sequence is: reproduce the bug in staging → confirm no configuration-level workaround exists → patch one file → generate → verify on a clean install → ship → open the upstream PR the same day. Then, and only then, write the policy that governs patch #2 onward.
Compatibility notes worth encoding in a runbook.
cweagans/composer-patches must be a non-dev dependency if any environment runs --no-dev.generate is followed by composer patches-relock and composer patches-repatch.composer.json is mutated by generate, so composer update --lock is mandatory; skipping it trips lock-hash validation.-p1. Do not hand-edit prefixes.gpatch and prioritise it on PATH.--patches-file=patches.json if the team wants the mapping as a separate, reviewable artifact rather than buried in extra. Prefer --patches-output if patches/ is already used by a JS toolchain.Sequencing.
rm -rf vendor && composer install and a deploy to staging.allow-plugins entry, confirm the deploy pipeline's Composer invocation, add a CI step that fails on composer.lock hash drift, and add a CI step that re-runs composer install --verbose from clean.Maintenance. The generator itself is essentially zero-maintenance; a dev-dependency bump once or twice a year. The real maintenance load is patch drift. Estimate 30–60 minutes per patched package per upgrade cycle to reinstall clean, re-apply the edit against the new version, re-copy .old, re-generate, re-lock, and re-verify. At five live patches across a fast-moving dependency set, that is a recurring drag on every upgrade sprint. Cap the register and give every patch an expiry review date.
Support. Small ecosystem: ~192 stars, no dependents, a single-maintainer project (Symplify/Tomas Votruba), maturity described only by its README. The narrow scope substantially offsets the bus-factor concern — the tool emits text diffs and nothing else, and the critical path is cweagans/composer-patches, which is far more widely deployed. Escalation for a generator defect realistically means "hand-write the diff"; that escape hatch should be documented rather than discovered during an incident.
Scaling. Patch count scales linearly in effort but superlinearly in cognitive load, because each patch adds a compatibility constraint to every future dependency bump. Practical ceiling for a product team is a handful — three to five — and anything beyond that is a signal to fork the package, vendor it, or block the upgrade instead. Multi-team scaling introduces a second pressure: patches/ and extra.patches in the root composer.json become a merge-conflict hotspot, so assign one owning team for the directory and require a rebase-before-push convention.
Failure modes, ranked by what a TPM needs to watch.
generate sweeps everything that differs from its .old counterpart, so a teammate's leftover debugging edits become committed patch content. Enforce a clean-tree rule before regenerating.Ramp-up. A competent Laravel engineer unfamiliar with the tool needs one to two hours to be productive and a half day to be safe, mostly spent understanding the --no-dev trap and the cweagans v2 relock/repatch sequence. Team-level ramp-up is a short README in the repo plus the patch register. Net TPM verdict: adopt narrowly, budget for drift, and measure success by the rate at which patches are removed rather than added — a healthy register trends toward empty.
How can I help you explore Laravel packages today?