← Back to blog

Magento 2 on PHP 8.3 and 8.4: Fixing Compatibility Issues

Rodrigo Biazzi

Magento 2.4.8 runs on PHP 8.3 and 8.4, and older PHP versions are reaching end of life. For most stores the core is not the problem. Adobe tests the core on supported PHP versions. The problems come from custom modules in app/code and from third party extensions that were written for PHP 7.4 and never touched again.

This article covers the language changes that actually break Magento code, how to find them without reading every file, and how to fix them in a way that keeps the code readable.

What changed between PHP 7.4 and 8.4

If a store jumps from PHP 7.4 or 8.1 straight to 8.3 or 8.4, it collects several years of changes at once. These are the ones I see most in Magento code.

Passing null to internal functions (8.1)

Since PHP 8.1, passing null to a non nullable parameter of a built in function raises a deprecation. Code like strlen($value), trim($value) or explode(',', $value) where $value comes from getData() is everywhere in Magento extensions. getData() returns null for missing keys, so the deprecation fires often.

// Before
$sku = trim($product->getData('vendor_sku'));

// After
$sku = trim((string) $product->getData('vendor_sku'));

A cast is fine when an empty string is a valid fallback. When null means something different from empty, handle it explicitly instead of hiding it.

Dynamic properties (8.2)

Creating a property that was not declared in the class is deprecated since 8.2. Old Magento code often does this in blocks and helpers, for example assigning $this->cache inside a method without declaring it. Classes that extend Magento\Framework\DataObject are not affected for setData() style access, because that goes through magic methods and an internal array. Plain classes are affected.

class PriceHelper
{
    private array $cache = [];

    public function get(int $productId): ?float
    {
        return $this->cache[$productId] ?? null;
    }
}

The fix is to declare the property, with a type where you can. Do not add the #[\AllowDynamicProperties] attribute as a general fix. It hides the problem in your own code where declaring the property is a one line change.

Implicitly nullable parameters (8.4)

This is the change that produces the most warnings on PHP 8.4. A parameter with a type and a default of null used to be nullable automatically. That form is now deprecated.

// Deprecated in PHP 8.4
public function execute(OrderInterface $order = null)

// Correct
public function execute(?OrderInterface $order = null)

Magento constructors are full of this pattern, because optional dependencies were commonly declared as Json $serializer = null to keep backward compatibility. The fix is mechanical and safe, which makes it a good job for Rector.

Other changes worth checking

  • ${var} string interpolation is deprecated since 8.2. Use {$var}.
  • utf8_encode() and utf8_decode() are deprecated since 8.2. Use mb_convert_encoding().
  • PHP 8.0 changed string to number comparison. 0 == 'abc' is now false. Code that relied on loose comparisons with attribute values can change behavior without any warning.
  • PHP 8.0 made many internal functions throw TypeError or ValueError instead of returning false with a warning. Code that checked for false after passing a wrong type now gets an exception.
  • Return type declarations on internal interfaces. Classes that implement ArrayAccess, Iterator, Countable or JsonSerializable without matching return types trigger deprecations since 8.1.

Finding the problems

Running the store and waiting for errors finds only the code paths you happen to hit. Static analysis finds the rest. I use three tools together.

PHPCompatibility

PHPCompatibility is a PHP_CodeSniffer standard that checks code against a target PHP version. The Magento coding standard already pulls it in, so if you have magento/magento-coding-standard installed you probably have it. Check which version you have, because older releases do not know about the newest PHP versions.

vendor/bin/phpcs -i
vendor/bin/phpcs --standard=PHPCompatibility \
    --runtime-set testVersion 8.3- \
    --extensions=php,phtml \
    app/code

testVersion 8.3- means PHP 8.3 and above. Run it on app/code and on the vendor folders of the extensions you maintain or plan to patch.

Rector for mechanical fixes

Rector rewrites code automatically. For an upgrade I prefer selected rules over full PHP level sets, because the full sets also rewrite code to use new syntax, which makes the diff huge and harder to review. A minimal config for the 8.4 nullable fix:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app/code'])
    ->withRules([
        ExplicitNullableParamTypeRector::class,
    ]);
vendor/bin/rector process --dry-run
vendor/bin/rector process

Always run the dry run first and read the diff. Rector is reliable for these simple rules, but you are still responsible for what gets committed.

PHPStan

PHPStan catches the errors that are not deprecations: wrong types passed to methods, calls to methods that do not exist, nulls that reach code that cannot handle them. With the bitExpert Magento extension it understands generated factories and proxies. Start at a low level and raise it over time. More on setup in the PHPStan guide.

Seeing deprecations at runtime

Static tools miss code that is built from strings or loaded through configuration. To catch those, log deprecations while you test. In your local or staging PHP configuration:

error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/deprecations.log

Then click through the main flows: category, product, cart, checkout, customer account, admin product and order screens. Group the log by file and fix the most frequent ones first. Most of the noise usually comes from a few helper methods called on every page.

Third party extensions

For extensions installed through Composer, the right fix is a new version from the vendor. Check each vendor for a release that declares support for your target PHP version in composer.json. If the package constraint excludes your PHP version, Composer will refuse to install it, which is the behavior you want.

When the vendor is gone or slow, you have two options:

  • Patch the package with a Composer patch while you wait for an official release. Keep the patch small and limited to compatibility fixes.
  • Replace the extension. Old extensions that nobody maintains are a security risk as well as a compatibility risk. An extension audit helps decide which ones to keep.

Never solve a Composer conflict with --ignore-platform-reqs in production builds. It installs packages that their authors said do not support your PHP version, and the failure shows up later at runtime.

A sensible order of work

  1. Upgrade PHP in your local environment while staying on the current Magento version, if that combination is supported.
  2. Run PHPCompatibility and PHPStan on app/code. Apply Rector for the mechanical fixes.
  3. Update or patch third party extensions.
  4. Test with deprecation logging on, fix what shows up.
  5. Release the PHP upgrade to production on its own.
  6. Upgrade Magento in a separate release.

Splitting the PHP upgrade from the Magento upgrade is the most useful habit here. When something breaks, you know which change caused it.

FAQ

Which PHP version should I use for Magento 2.4.8?

2.4.8 supports PHP 8.3 and 8.4. Check the system requirements page for your exact patch release, and confirm that every extension you use supports the version you pick.

Are PHP deprecations errors?

No. A deprecation is a notice that the code will break in a future PHP version. The store keeps working, but you should fix them before the next upgrade turns them into fatal errors.

Can I use AllowDynamicProperties to silence PHP 8.2 warnings?

It works, but it hides a problem that is usually fixed by declaring the property. Use it only for vendor code you cannot change yet.

Does Rector change the behavior of my code?

Rules like ExplicitNullableParamTypeRector only change syntax. Larger level sets can change more, so run a dry run and review the diff before committing.

I am available for freelance Magento and Adobe Commerce work, including PHP upgrades and extension fixes. If you need help with this, get in touch.

Related articles