Deprecations

How to read a @deprecated tag in Magento, find every usage in your own code, and build an upgrade plan from that list.

Syntax@deprecated <module version> followed by @see <replacement class, ticket id, or Nothing>

A deprecation is a promise with two halves: the code still works now, and it will be removed later. Magento marks it with @deprecated in the doc block, usually next to a @see line pointing at the replacement. Marking public code deprecated means Adobe plans to remove it in a future MINOR release, or after roughly a year if no MINOR release ships in that time.

Reading one

The number after @deprecated is the module's own version, not the 2.4.x release. @deprecated 101.0.0 on Magento\Framework\Url::escape() refers to version 101.0.0 of Magento_Framework. The @see line takes three forms, and they mean different amounts of work:

  • A class or method is the replacement. Magento\Framework\Url::escape() points at Magento\Framework\Escaper::escapeUrl. Swap the call.
  • An internal ticket id, such as @see MAGETWO-54971 on Escaper::escapeJsQuote(), is a reference you cannot read. Find the replacement yourself.
  • @see Nothing, which Magento\Framework\Registry carries, means no replacement exists. That class is @deprecated 102.0.0 and still @api, and the doc block says what to do instead in prose rather than in a tag.

Finding your usages

Nothing reports a deprecated Magento call at runtime. Add phpstan/phpstan-deprecation-rules to turn every one into an error you can count, and run vendor/bin/phpcs --standard=Magento2, whose Legacy sniffs flag obsolete APIs that predate the annotations.

Why the upgrade plan starts here

The deprecations your modules touch are exactly the work the next MINOR release will force. Doing it while the old code still runs is cheaper than doing it when the class is gone.

Example

Example · php
<?php
// app/code/Vendor/Loyalty/Model/PointsCalculator.php
declare(strict_types=1);

namespace Vendor\Loyalty\Model;

/**
 * @api
 * @since 1.0.0
 */
class PointsCalculator
{
    public const DEFAULT_POINTS_PER_UNIT = 10;

    /**
     * Points earned for an order total at the default rate.
     *
     * @deprecated 1.2.0 The rate is now configurable per customer group.
     * @see PointsCalculator::calculateAtRate()
     */
    public function calculate(float $grandTotal): int
    {
        // Delegate. The policy requires the deprecated method to proxy the
        // new one rather than keep its own copy of the logic, so a fix lands
        // in one place and both callers get it.
        return $this->calculateAtRate($grandTotal, self::DEFAULT_POINTS_PER_UNIT);
    }

    public function calculateAtRate(float $grandTotal, int $pointsPerUnit): int
    {
        if ($grandTotal <= 0.0 || $pointsPerUnit <= 0) {
            return 0;
        }

        return (int) floor($grandTotal * $pointsPerUnit);
    }
}

// Output:
// (new PointsCalculator())->calculate(19.99)          => 199
// (new PointsCalculator())->calculateAtRate(19.99, 5) => 99
//
// Adding calculateAtRate() to an @api class is a MINOR change, so this
// module ships as 1.2.0. Run bin/magento setup:di:compile after editing
// the constructor of any injected class.

When to use it

  • Build the work list for an upgrade from the deprecations your own modules touch.
  • Decide whether a @see Nothing deprecation means you must design the replacement yourself.
  • Deprecate a public method in your own module without breaking the integrators using it.
  • Explain to a reviewer why a deprecated call is a scheduled task rather than an immediate failure.

More examples

Three real deprecations, read from the source

These are the three shapes you will meet. The first is a direct swap. The second gives you a ticket number and nothing else, so you read the method body to work out what it did and replace it with an escaper call that matches the context. The third keeps @api alongside @deprecated, which is deliberate: the promise not to break it still holds while it exists, and the prose above the tags is the only guidance you get. Reach for this reading habit before estimating any upgrade.

Example · php
<?php
// vendor/magento/framework/Url.php (fragment)
    /**
     * @deprecated 101.0.0
     * @see \Magento\Framework\Escaper::escapeUrl
     */
    public function escape($value)

// vendor/magento/framework/Escaper.php (fragment)
    /**
     * @deprecated 101.0.0
     * @see MAGETWO-54971
     */
    public function escapeJsQuote($data, $quote = '\'')

// vendor/magento/framework/Registry.php (fragment)
    /**
     * Registry usage as a shared service introduces temporal, hard to detect
     * coupling into system. Its usage should be avoided. Use service classes
     * or data providers instead.
     *
     * @api
     * @deprecated 102.0.0
     * @see Nothing
     * @since 100.0.2
     */
    class Registry implements ResetAfterRequestInterface

Counting your own usages

Grep finds the declarations, PHPStan finds the calls, and the XML pass finds the wiring that neither of the first two would see. Run all three before an upgrade and the result is a finite list you can estimate. The deprecation rules package adds only rules, so it needs no configuration beyond phpstan/extension-installer being present.

Example · bash
# 1. The blunt pass. Catches imports and fully qualified references.
$ grep -rn --include='*.php' --include='*.phtml' \
    'Magento\\Framework\\Registry' app/code/
app/code/Vendor/Loyalty/Block/Balance.php:9:use Magento\Framework\Registry;
app/code/Vendor/Loyalty/Block/Balance.php:24:        Registry $registry,

# 2. XML counts too: a deprecated class can be wired in di.xml or layout.
$ grep -rn 'Magento\\Framework\\Registry' app/code/*/*/etc/ app/design/

# 3. The thorough pass: every call into deprecated code, anywhere.
$ composer require --dev phpstan/phpstan-deprecation-rules
$ vendor/bin/phpstan analyse --memory-limit=4G
 ------ ----------------------------------------------------------------
  Line   app/code/Vendor/Loyalty/Block/Balance.php
 ------ ----------------------------------------------------------------
  31     Call to deprecated method registry() of class
         Magento\Framework\Registry.
 ------ ----------------------------------------------------------------

Replacing a deprecated call

The replacement is a different object, not a renamed method, which is the usual case and the reason a deprecation is rarely a one-line change: the new dependency has to be injected. Keeping the old call in a comment with its annotation gives the next reader the evidence for why the code looks like this. Reach for this shape when working through the list the previous example produced.

Example · php
<?php
// app/code/Vendor/Loyalty/ViewModel/RewardsLink.php
declare(strict_types=1);

namespace Vendor\Loyalty\ViewModel;

use Magento\Framework\Escaper;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class RewardsLink implements ArgumentInterface
{
    public function __construct(
        private readonly UrlInterface $url,
        private readonly Escaper $escaper
    ) {
    }

    public function getRewardsUrl(int $customerId): string
    {
        $url = $this->url->getUrl('loyalty/rewards/index', ['id' => $customerId]);

        // Was: $this->url->escape($url)
        //   @deprecated 101.0.0, @see \Magento\Framework\Escaper::escapeUrl
        return $this->escaper->escapeUrl($url);
    }
}

// Called from a .phtml through the view model. After editing the class run
// bin/magento cache:flush; after changing its constructor also run
// bin/magento setup:di:compile.

Discussion

  • Be the first to comment on this lesson.