Structure belongs in declarative schema; data changes belong in patches. Adding an attribute, seeding config values, migrating rows - anything that writes data at install or upgrade time is a patch. The patch system replaced the old UpgradeData.php version-maze with explicit, dependency-ordered classes. Here is how to write them well.
The Anatomy
namespace Acme\Locations\Setup\Patch\Data;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
class AddDefaultLocation implements DataPatchInterface, PatchRevertableInterface
{
public function __construct(
private ModuleDataSetupInterface $setup
) {}
public function apply(): void
{
$this->setup->startSetup();
// insert default location row
$this->setup->endSetup();
}
public function revert(): void
{
// remove the row again (module uninstall)
}
public static function getDependencies(): array
{
return [CreateLocationTableConfig::class];
}
public function getAliases(): array
{
return [];
}
}
Key mechanics:
- Patches run once - recorded in the
patch_listtable getDependencies()declares order: your patch runs after its dependencies. This replaces version-number ordering entirelyapply()wraps writes instartSetup/endSetup(transaction and foreign-key handling)PatchRevertableInterface::revert()runs on module uninstall viasetup:uninstall- implement it when the patch’s data should not outlive the module
The Discipline
Idempotency: patches should be safe to reason about as run-once, but defensive checks (“does this attribute already exist?”) save you when a database was built by import rather than setup:upgrade.
One concern per patch: a patch adding an attribute and a patch seeding its default values are two patches with a dependency - not one patch doing both. Small patches are auditable and orderable; big patches are neither.
No schema in data patches: table and column creation is db_schema.xml’s job. A data patch may reference schema from its dependencies, never create it (schema patches exist for legacy reasons; declarative schema is the modern path).
Legacy Scripts and Migration
Old modules with InstallData/UpgradeData still run - Magento executes them before patches. When modernising: leave old scripts in place for existing installs (they are recorded as run), and write all new changes as patches. The ModuleDataSetupInterface API is the same in both, so the migration is about structure, not relearning.
Operational Notes
bin/magento setup:upgraderuns pending patches;bin/magento module:statusand thepatch_listtable tell you what has applied- A failed patch stops the run and can leave
setup:upgradehalf-applied - test patches against a production-like database, especially data-heavy ones (backfills over millions of rows should be batched within the patch, not one heroic UPDATE)
Patches are small, ordered, honest units of data change. Write them that way and upgrades stop being the part of the release everyone holds their breath through.