Dependencies and Ordering

Declare order with getDependencies, because an undeclared dependency works on every upgrade and breaks only on a fresh install.

Syntaxpublic static function getDependencies(): array // ['Vendor\\Module\\Setup\\Patch\\Data\\EarlierPatch']

Patches are not applied in file order, alphabetical order, or the order you wrote them. Magento builds a graph from each patch's static getDependencies() and applies them in an order that satisfies it. Patches with no declared relationship may run in any order at all.

Declaring a dependency

Return the dependency's class names from getDependencies(). The patches may belong to another module; the names are resolved as classes, not as files in your directory. When the dependency lives in another module, also add that module to the <sequence> element of your module.xml, so Magento loads and sets up that module before yours.

The failure only a fresh install shows

This is the trap. On your machine the earlier patch ran weeks ago and its row sits in patch_list, so it is skipped and your new patch always sees the data it needs. Order never seems to matter. On a fresh install every patch runs in one pass, the graph decides the order, and a patch that quietly assumed a predecessor now runs first and finds nothing. The bug reaches you as "works on staging, empty on a new environment".

So test on a clean database, not only on an upgrade. Reproduce it cheaply by deleting both rows from patch_list and running bin/magento setup:upgrade again.

Cycles

Two patches that depend on each other, directly or through a chain, stop the install with a LogicException reading Cyclomatic dependency during patch installation. Break the cycle by splitting the patch that needs both halves.

Example

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

namespace Acme\Loyalty\Setup\Patch\Data;

use Magento\Customer\Setup\Patch\Data\DefaultCustomerGroupsAndAttributes;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class AssignTiersToGroups implements DataPatchInterface
{
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup
    ) {
    }

    public function apply(): self
    {
        $connection = $this->moduleDataSetup->getConnection();

        $generalGroupId = (int) $connection->fetchOne(
            $connection->select()
                ->from($this->moduleDataSetup->getTable('customer_group'), 'customer_group_id')
                ->where('customer_group_code = ?', 'General')
        );

        $connection->update(
            $this->moduleDataSetup->getTable('acme_loyalty_tier'),
            ['customer_group_id' => $generalGroupId],
            ['code = ?' => 'bronze']
        );

        return $this;
    }

    public static function getDependencies(): array
    {
        // ::class is a compile-time string. These two arrays are identical:
        //   [DefaultCustomerGroupsAndAttributes::class]
        //   ['Magento\\Customer\\Setup\\Patch\\Data\\DefaultCustomerGroupsAndAttributes']
        return [
            AddDefaultTiers::class,
            DefaultCustomerGroupsAndAttributes::class,
        ];
    }

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

// bin/magento setup:upgrade
// Both named patches are applied first, in this install and in every
// fresh one. Drop the second entry and the lookup returns 0 on a new
// database, because the General group does not exist yet.

When to use it

  • Make a patch that reads customer groups run after the Magento patch that creates them.
  • Diagnose a module that installs correctly on staging but produces empty data on a new environment.
  • Break a cyclomatic dependency reported by setup:upgrade after merging two feature branches.
  • Order a backfill patch after the schema change that added the column it fills.

More examples

Sequence makes the other module set up first

getDependencies() orders patches; sequence orders modules. Add the entry whenever your patches read another module's tables or data, so that module's own setup runs first and your module loads after it. Both are needed for a cross-module dependency: the sequence alone does not order individual patches, and the dependency alone leaves module load order to chance. The file is read on bin/magento setup:upgrade.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Acme/Loyalty/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_Loyalty">
        <sequence>
            <module name="Magento_Customer"/>
        </sequence>
    </module>
</config>

Reproducing a fresh install without reinstalling

The cheapest test for undeclared ordering, short of installing Magento again. Truncate or reset the rows the patches wrote first, otherwise the second run sees data the fresh install would not have. patch_id is an auto-increment, so the first query also shows roughly what ran together during the last setup:upgrade. Do this only on a development database.

Example · sql
-- Read the order patches were actually applied in.
SELECT patch_id, patch_name FROM patch_list ORDER BY patch_id;

-- Replay a pair of patches as a fresh install would see them.
DELETE FROM patch_list
WHERE patch_name IN (
    'Acme\\Loyalty\\Setup\\Patch\\Data\\AddDefaultTiers',
    'Acme\\Loyalty\\Setup\\Patch\\Data\\AssignTiersToGroups'
);
-- then: bin/magento setup:upgrade

The dependency that belongs inside one patch

Reach for this when you catch yourself wanting two patches to run before each other. Two steps that genuinely need each other's output are one unit of work, and a single patch also gets you one transaction around both, so a failure in the second step rolls the first one back. Splitting is worth it only when the halves are independently useful or belong to different modules.

Example · php
<?php
// Wrong: a cycle. CreateTiers needs the group ids, AssignGroups needs
// the tiers, and each names the other.
public static function getDependencies(): array
{
    return [AssignGroups::class];   // in CreateTiers
}
public static function getDependencies(): array
{
    return [CreateTiers::class];    // in AssignGroups
}
// setup:upgrade stops with:
//   Cyclomatic dependency during patch installation

// Right: one patch does both steps in order, in a single transaction.
public function apply(): self
{
    $this->createTiers();
    $this->assignGroups();

    return $this;
}

Discussion

  • Be the first to comment on this lesson.