Data Patches

A data patch is one class that changes data once; Magento records its name in patch_list, and setup:upgrade is what runs it.

Syntaxapp/code/Vendor/Module/Setup/Patch/Data/PatchName.php implements Magento\Framework\Setup\Patch\DataPatchInterface

A data patch is a PHP class that changes the contents of the database once. It inserts a CMS block, corrects a configuration value, or fills in a column that a schema change just added. It lives in one fixed place, app/code/Vendor/Module/Setup/Patch/Data/, and implements Magento\Framework\Setup\Patch\DataPatchInterface. Nothing registers it: Magento reads that directory for every enabled module.

How Magento knows it already ran

When a patch finishes, Magento inserts its fully qualified class name into the patch_list table. That table has two columns, patch_id and patch_name. On the next run the name is found and the patch is skipped. There is no version number and no module column, only the class name.

Patches are applied by bin/magento setup:upgrade, or by bin/magento setup:db-data:upgrade on its own. Adding the file changes nothing until one of those runs. bin/magento setup:db:status tells you whether anything is pending.

What they replaced

Magento 2.3 introduced patches. Before that a module shipped InstallData.php and UpgradeData.php, and Magento compared the module's setup_version with the setup_module table to decide what to run. Patches replaced both mechanisms. setup_version in module.xml is no longer the trigger for anything, and a new module does not need the attribute at all.

What trips people up

A data patch runs inside a single database transaction. If it throws, everything rolls back and no row reaches patch_list, so fixing the cause and running setup:upgrade again is safe. To force a re-run on a development machine, delete the row from patch_list.

Example

Example · php
<?php
// app/code/Acme/Shipping/Setup/Patch/Data/DisableFlatRateCarrier.php
declare(strict_types=1);

namespace Acme\Shipping\Setup\Patch\Data;

use Magento\Framework\App\Config\Storage\WriterInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class DisableFlatRateCarrier implements DataPatchInterface
{
    private WriterInterface $configWriter;

    public function __construct(WriterInterface $configWriter)
    {
        $this->configWriter = $configWriter;
    }

    public function apply(): self
    {
        // WriterInterface::save($path, $value, $scope = 'default', $scopeId = 0)
        $this->configWriter->save('carriers/flatrate/active', '0');

        return $this;
    }

    public static function getDependencies(): array
    {
        return [];
    }

    public function getAliases(): array
    {
        return [];
    }
}

// Apply it:
//   bin/magento setup:upgrade
//   bin/magento cache:flush config
//
// setup:upgrade writes one row into core_config_data and inserts
// Acme\Shipping\Setup\Patch\Data\DisableFlatRateCarrier into patch_list.
// A second setup:upgrade finds that row and skips the patch.
// Without the cache flush, the admin and storefront keep reading the
// old value from the config cache.

When to use it

  • Switch off a shipping carrier on every existing install without asking merchants to click through the admin.
  • Insert the default rows a new module needs so the storefront is not empty on first load.
  • Fill in a column that db_schema.xml has just added to an existing table.
  • Check on a staging database whether a colleague's patch has already been applied.

More examples

Inspecting and resetting patch history

patch_list is the only state behind patches, so this is how you answer "did my patch run". Reach for the DELETE while developing, when you want to re-apply a patch you just edited. Never do it on production to replay a patch: the patch will do its work a second time. Note the doubled backslashes, because the stored value is a PHP namespace.

Example · sql
-- The whole record of which patches have run.
SELECT patch_id, patch_name
FROM patch_list
WHERE patch_name LIKE 'Acme\\\\%'
ORDER BY patch_id;

-- Force one patch to run again on a development database.
DELETE FROM patch_list
WHERE patch_name = 'Acme\\Shipping\\Setup\\Patch\\Data\\DisableFlatRateCarrier';

module.xml in 2.4.9 has no setup_version

This is what a module declaration looks like once patches and declarative schema are the only setup mechanisms: no setup_version attribute. Older modules still carry one, and it is harmless, but it no longer decides whether anything runs. The sequence element is about load order, not versions, and it matters when your patch expects another module's data to exist already. Changing module.xml takes effect on bin/magento setup:upgrade.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Acme/Shipping/etc/module.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Acme_Shipping">
        <sequence>
            <module name="Magento_OfflineShipping"/>
        </sequence>
    </module>
</config>

Discussion

  • Be the first to comment on this lesson.