← Back to blog

Patching Magento 2 Core with Composer Patches

Rodrigo Biazzi

Sooner or later every Magento project needs to change code it does not own. A bug in a core module is fixed in a later release you cannot upgrade to yet. A third party extension has a typo in a SQL query. A private method has no plugin point. Editing the file in vendor/ fixes it for about as long as it takes someone to run composer install.

Patches are the honest answer to that problem. The change lives in a file in the repository, Composer applies it every time it installs the package, and the build fails loudly when the patch stops applying. This article covers the two tools used for this in Magento 2: cweagans/composer-patches for your own patches and the Quality Patches Tool for patches published by Adobe.

When a patch is the right tool

A patch is a last resort, not a first one. Before writing one, check whether the behavior can be changed through normal extension points:

  • A plugin on a public method
  • An observer on an event fired nearby
  • A preference for a class, or a di.xml argument replacement
  • A template or JS override in your theme, or a RequireJS mixin

The comparison of those options is in plugins vs observers vs preferences. A patch makes sense when the code is in a private or protected method that a preference would force you to copy whole, when it is a small fix to a long method, or when the fix is already published upstream and you only need it until the next upgrade.

Setting up composer-patches

Install the plugin as a project dependency:

composer require cweagans/composer-patches

Composer will ask whether to allow the plugin. Allow it, which adds it to config.allow-plugins in composer.json. Patches are declared under extra.patches, keyed by package name:

{
    "extra": {
        "patches": {
            "magento/module-catalog": {
                "Fix wrong store filter in product collection": "patches/composer/magento-module-catalog-store-filter.patch"
            },
            "vendor-name/module-shipping-rates": {
                "Fix null check in rate calculation": "patches/composer/vendor-shipping-null-check.patch"
            }
        }
    }
}

Composer applies each patch right after it installs or updates the matching package. The description is free text, so write something a colleague can understand without opening the file. A link to the GitHub issue or the support ticket helps a lot when someone later wonders if the patch is still needed.

Version 1 and version 2 of the plugin behave differently in a few places. Version 2 keeps a patches.lock.json file and adds commands such as composer patches-relock and composer patches-repatch. Version 1 uses the option composer-exit-on-patch-failure in extra to make a failed patch stop the install. Check which one your project uses and read its documentation, because the defaults around failures are not the same.

Failing loudly

The worst outcome is a patch that silently stops applying after an upgrade. The site deploys, the bug comes back, and nobody connects the two. Whatever plugin version you use, make sure a patch that does not apply breaks the build. With version 1 that means setting:

{
    "extra": {
        "composer-exit-on-patch-failure": true
    }
}

Your CI pipeline should run a clean composer install on every change. That is where a broken patch will show up first. See CI/CD with GitHub Actions for a pipeline that does this.

Creating a clean patch file

The patch must use paths relative to the package root, not the project root. For magento/module-catalog, a path like Model/Product.php is correct, and vendor/magento/module-catalog/Model/Product.php is not.

A reliable way to produce that is to initialize a temporary Git repository inside the package folder:

cd vendor/magento/module-catalog
git init -q && git add -A && git commit -qm "original"

# edit the file(s) you need to change

git diff > ../../../patches/composer/magento-module-catalog-store-filter.patch
rm -rf .git
cd -

The resulting diff has a/Model/Product.php and b/Model/Product.php paths, which the plugin applies with the usual patch levels. Then reinstall the package to confirm the patch applies on a clean copy:

rm -rf vendor/magento/module-catalog
composer install

Patches from the magento2 GitHub repository

A commit on GitHub for magento/magento2 uses monorepo paths like app/code/Magento/Catalog/Model/Product.php. Those paths do not exist in a Composer installation. Download the commit as a diff, then replace the app/code/Magento/Catalog/ prefix so the paths start from the module root, and keep only the hunks for one package per patch file. A patch that touches two packages has to be split into two entries in composer.json.

Keep patches small

One patch per fix. A single patch that touches ten things is impossible to drop partially after an upgrade. Small patches with clear descriptions can be removed one at a time when the upstream fix arrives.

The Quality Patches Tool

Adobe publishes fixes for known issues as individual patches with IDs. The Quality Patches Tool, distributed as magento/quality-patches and used through magento/magento-cloud-patches on Adobe Commerce Cloud, knows which patches exist and which fit your installed version.

composer require magento/quality-patches
vendor/bin/magento-patches status
vendor/bin/magento-patches apply ACSD-12345

status lists the patches available for your version and whether they are applied. apply takes one or more patch IDs. The ID above is a placeholder, so use the ones listed by status or referenced in Adobe support articles. The tool also has a revert command.

On Adobe Commerce Cloud, patches are usually declared in .magento.env.yaml under the QUALITY_PATCHES variable of the build stage, so they are applied on every build. On other hosting, run the apply command as part of your build script, after composer install and before setup:di:compile. Patches applied this way are not in Git, so the build step has to run on every deploy.

Before applying anything, read the patch description and check that it targets your actual problem. Some patches change behavior that other extensions rely on.

Where to keep patches in the repository

A simple structure that scales:

  • patches/composer/ for files referenced from composer.json
  • A short README in that folder listing each patch, why it exists and the condition for removing it
  • File names that start with the package name, so they sort together

Some teams prefer to keep the patch list in a separate JSON file referenced by extra.patches-file. That works with version 1 of the plugin and keeps composer.json shorter. Either way, the list must be under version control.

Patches and upgrades

Every Magento upgrade starts with the patch list. For each entry:

  1. Check whether the fix is part of the new release. Release notes and the linked GitHub issue usually tell you.
  2. If it is, remove the entry and the file.
  3. If it is not, run composer install and see whether it still applies.
  4. If it does not apply, recreate it from the new code using the steps above, then test the original bug again.

The 2.4.8 upgrade checklist includes this review as one of the first steps, because a failing patch blocks the whole Composer update.

Patching third party extensions

The same mechanism works for extensions installed through Composer. Before patching one, report the problem to the vendor. If they ship a fix, you can drop the patch at the next update. If the extension is in app/code rather than vendor, there is nothing for Composer to patch, and the change is just a normal commit, which is one more reason to install extensions through Composer when the vendor offers it.

What not to patch

Avoid patching generated code in generated/, compiled static files in pub/static, or database schema through a patch file. Those are build outputs or runtime state. Fix the source that generates them instead. Also avoid patches that change public method signatures. Other modules may depend on them, and the next person reading the code will not expect a core interface to differ from the documentation.

FAQ

Can I just edit files in vendor/magento?

No. Any composer install or update can overwrite them, and the change is invisible in code review. Put the change in a patch file applied by Composer, or move it into a module with a plugin or preference.

What is the difference between composer patches and the Quality Patches Tool?

composer-patches applies any patch file you add to composer.json. The Quality Patches Tool applies patches published by Adobe for known Magento issues, identified by an ID, and checks whether they fit your installed version.

My patch fails after an upgrade. What now?

Check whether the fix is already in the new release and drop the patch if so. If not, apply the old change by hand to the new vendor code and regenerate the patch from a clean diff.

Are patches better than plugins?

Use a plugin, observer or preference when the extension points allow it. A patch is for cases where the code you need to change is private, inside a long method, or in a template or JS file with no clean override.

I am available for freelance Magento and Adobe Commerce work, including upgrade and patch maintenance. If you need help with this, get in touch.

Related articles