What Breaks in an Upgrade

The four ways an upgrade breaks custom code: a removed method, a renamed layout element, a moved plugin target, a stale override.

Syntaxbin/magento dev:di:info 'Magento\Catalog\Api\ProductRepositoryInterface'

Core code moves between versions. Four shapes of breakage account for most of the work after composer update, and only the first one is loud.

A removed method

A method deprecated in one minor release is deleted in a later one. Magento Open Source 2.4.9 removed addInvoicedAmount() from Magento\AdminUiSdkCustomFees\Api\CustomFeesRepositoryInterface, and the backward-incompatible changes reference on developer.adobe.com lists every such removal release by release. Calls into a deleted method fatal with "Call to undefined method". The Upgrade Compatibility Tool reports these before you upgrade as issue 1410, "Call non-existent Adobe Commerce method", and reports calls to still-present deprecated methods as 1439 at warning level.

A plugin whose target moved

A plugin declared on a concrete class stops applying when core changes which class implements the interface. Nothing fails. The behaviour your plugin added is simply absent. Declare plugins on the @api interface instead, and check what is attached with bin/magento dev:di:info.

A renamed layout element

A referenceBlock or referenceContainer whose name no longer exists matches nothing, so the arguments you set never arrive at any block.

A theme override that is now wrong

A .phtml copied into your theme keeps winning over the module's version forever. After the upgrade, compare each override with the file it overrides in vendor/.

Every fix needs bin/magento setup:di:compile and bin/magento cache:flush, plus setup:static-content:deploy in production mode.

Example

Example · php
<?php
// app/code/Vendor/Theme/ViewModel/StoreNotice.php

declare(strict_types=1);

namespace Vendor\Theme\ViewModel;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\ScopeInterface;

/**
 * Logic a template needs, in a class this module owns.
 *
 * An upgrade cannot remove getNotice(), because core never declared it.
 * It can remove a block method a template was calling, which is why the
 * same logic inside a copied .phtml breaks and this does not.
 */
class StoreNotice implements ArgumentInterface
{
    private const XML_PATH_NOTICE = 'vendor_theme/notice/text';

    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    public function getNotice(): string
    {
        return (string)$this->scopeConfig->getValue(
            self::XML_PATH_NOTICE,
            ScopeInterface::SCOPE_STORE
        );
    }
}

// The view model is passed to a template as a layout argument, and the
// template escapes the value at the point of output:
//   <p><?= $escaper->escapeHtml($viewModel->getNotice()) ?></p>
//
// After adding the class:
//   bin/magento setup:di:compile && bin/magento cache:flush

When to use it

  • Working through a UCT report after the version bump but before the smoke tests.
  • Finding out why a plugin that worked on 2.4.8 adds nothing on 2.4.9.
  • Auditing a theme's template overrides against the new module versions.
  • Deciding whether to fix a deprecation now or file it for the next upgrade.

More examples

Declare the plugin on the interface, not the implementation

Both forms are valid, and both are common in extensions. The interface form is the one that survives, because an interface marked @api carries a compatibility promise that a concrete class does not. After editing di.xml run bin/magento setup:di:compile followed by bin/magento cache:flush, then confirm the plugin is still attached with bin/magento dev:di:info 'Magento\Catalog\Api\ProductRepositoryInterface', which lists the preference, the constructor arguments and every plugin on the type.

Example · xml
<!-- app/code/Vendor/Catalog/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Survives an upgrade that changes which class implements this. -->
    <type name="Magento\Catalog\Api\ProductRepositoryInterface">
        <plugin name="vendor_catalog_product_badge"
                type="Vendor\Catalog\Plugin\ProductRepositoryPlugin"
                sortOrder="10"/>
    </type>

    <!-- Breaks quietly if core stops routing calls through this class:
    <type name="Magento\Catalog\Model\ProductRepository">
        <plugin name="vendor_catalog_product_badge"
                type="Vendor\Catalog\Plugin\ProductRepositoryPlugin"/>
    </type>
    -->
</config>

A theme override checked against the version it overrides

Do this for every override after the upgrade, before the smoke tests, because the symptom otherwise appears as a design problem days later. New core templates use the $escaper object that the template engine passes in; older copies call escaping methods on $block, which are deprecated. Replace $block->escapeHtml($x) with $escaper->escapeHtml($x) while you are in the file. Both escape; only one is still maintained.

Example · bash
# Every template this theme has taken over
find app/design/frontend/Vendor/custom -name '*.phtml'

# For each one, diff it against the module file it hides.
# Example: the theme overrides Magento_Catalog's product list template.
diff -u \
  vendor/magento/module-catalog/view/frontend/templates/product/list.phtml \
  app/design/frontend/Vendor/custom/Magento_Catalog/templates/product/list.phtml

# Hunt for calls that newer core templates no longer make
grep -rn 'block->escapeHtml\|block->escapeUrl' app/design/frontend/Vendor/custom

# After editing templates, in production mode only:
bin/magento setup:static-content:deploy -f en_US && bin/magento cache:flush

The short and long option names of the compatibility scan

Run this before the upgrade and again after fixing, so the list shrinks on purpose. Work critical first: those are the fatals. The error level is slower-burning risk, because a class without an @api annotation can change in any release without notice. Warnings are the next upgrade's criticals, which makes them the right backlog to file rather than the right thing to block on.

Example · bash
# Same scan, both ways of naming the option
bin/uct upgrade:check /var/www/magento -c 2.4.9
bin/uct upgrade:check /var/www/magento --coming-version=2.4.9

# Only the issues the upgrade introduces, at error level and above
bin/uct upgrade:check /var/www/magento -c 2.4.9 -i --min-issue-level=error
bin/uct upgrade:check /var/www/magento -c 2.4.9 \
    --ignore-current-version-compatibility-issues \
    --min-issue-level=error

# Reading the result:
#   1110-1114 critical  your code names a class the target version does not have
#   1410      critical  your code calls a method the target version removed
#   1121-1124 error     your code extends or uses a class not marked @api
#   1131-1134 warning   the class is still there but @deprecated
#   1439      warning   the method is still there but @deprecated

Discussion

  • Be the first to comment on this lesson.