Jay GajjarSenior Software Engineer

PHP 7.4 to 8 upgrade checklist for legacy applications (2026)

A PHP 7.4 to 8 upgrade checklist: choosing 8.3, 8.4 or 8.5, scanning with PHPCompatibility and Rector, the breakages by version, and a safe rollout.

By Jay Gajjar10 min read

A PHP 7.4 to 8 upgrade is overdue for any app still on 7.4: that branch stopped getting security fixes on November 28, 2022. The safe path is to pick a target (PHP 8.3 or 8.4 for most apps in late 2026), inventory your framework and Composer packages, scan the code with PHPCompatibility and PHPStan, apply mechanical fixes with Rector, and then test on a side-by-side PHP install before switching production. The checklist below walks through each step, with the code changes that break most often.

I work in PHP 8 daily and have shipped CodeIgniter 3 and Laravel 8 to 12 code, so this PHP 8 upgrade guide focuses on the breakages legacy apps actually hit, not on the full changelog. If the app is plain core PHP with no framework, my core PHP vs framework comparison helps decide whether to upgrade it as it is or move it onto Laravel.

Quick answer: the PHP 7.4 to 8 upgrade checklist

  1. Pick the target version: 8.3 or 8.4 (8.5 if your packages already support it).
  2. Inventory the framework version, Composer packages and PHP extensions.
  3. Get tests or at least smoke tests in place on 7.4.
  4. Scan with PHPCompatibility and PHPStan against the target version.
  5. Apply automated fixes with Rector, review them, then fix the rest by hand.
  6. Upgrade the framework where the target PHP requires it.
  7. Install the new PHP next to the old one and test on staging.
  8. Switch production, watch the logs, keep the old version ready for rollback.

Why PHP 7.4 has to go, and which PHP 8 version to target

PHP 7.4, 8.0 and 8.1 are all end of life. Per php.net (opens in a new tab), these are the supported branches as of October 2026:

Branch Released Active support until Security fixes until
PHP 8.2 December 8, 2022 December 31, 2024 December 31, 2026
PHP 8.3 November 23, 2023 December 31, 2025 December 31, 2027
PHP 8.4 November 21, 2024 December 31, 2026 December 31, 2028
PHP 8.5 November 20, 2025 December 31, 2027 December 31, 2029

Do not target 8.2: it loses security support in a couple of months. My default advice:

  • PHP 8.3 if your framework or a key package does not yet support 8.4. It is the minimum for Laravel 13. A PHP 7.4 to 8.3 migration gets you security fixes until the end of 2027.
  • PHP 8.4 for most apps. A PHP 7.4 to 8.4 upgrade buys security support until the end of 2028, and the extra deprecations (mainly implicitly nullable parameters) are easy to fix with Rector.
  • PHP 8.5 if every dependency already declares support. It is the newest branch, so check your packages before you commit to it.

If you still need a PHP 7.4 to 8.2 migration because a hosting plan offers nothing newer, treat it as a stepping stone and plan the next jump now.

Step 1: inventory before you change anything

Run these on the production server (or a copy of it) and save the output:

php -v
php -m                      # loaded extensions
php --ini                   # which ini files are in use
composer show --direct      # installed direct dependencies
composer why-not php 8.4    # packages that reject PHP 8.4

composer why-not php 8.4 is the quickest way to see which packages block the target. Then answer three questions:

  1. Framework and version. For a Laravel or CodeIgniter app, the framework version usually decides how far you can go (more on this below).
  2. Extensions. Compare php -m against what the new version will have. PHP 8.4 moved IMAP, OCI8, PDO_OCI and PSpell out of core to PECL, so an app that reads mailboxes with imap_open() needs the PECL extension or a pure-PHP library.
  3. Entry points other than the web. Cron jobs, queue workers, CLI scripts and deploy hooks all run under the CLI binary, which can be a different version from PHP-FPM.

Step 2: tests first, even if you have none

Automated tests turn a guessing game into a checklist. If the app has a suite, get it green on 7.4 before anything else. If it has none, do not try to write unit tests for everything. Write a small set of smoke tests that hit the pages and actions that matter: login, the main list and detail pages, a form submission, an export, a payment callback. A Laravel feature test or a short PHPUnit test that makes HTTP requests against a staging copy is enough.

Also switch on full error reporting in a test environment so deprecations show up:

error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/app-errors.log

Deprecations are warnings about the next version. On 8.x you want to see them in the log, not on the screen.

Step 3: static scans with PHPCompatibility and PHPStan

PHPCompatibility (opens in a new tab) is a PHP_CodeSniffer standard that flags removed functions, changed signatures and new reserved words for a given range of versions. Note the version: the last stable release, 9.3.5, dates from 2019 and predates PHP 8. The 10.x line covers PHP 8 and, as of October 2026, is still a pre-release. Install it the way its README shows:

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -ps . --standard=PHPCompatibility --runtime-set testVersion 7.4-8.4 --extensions=php --ignore=*/vendor/*

A testVersion range of 7.4-8.4 reports code that breaks on any version in the range, which is what you want while the app must still run on 7.4.

PHPStan (opens in a new tab) catches a different class of bug: calls with the wrong types, undefined methods and dead code paths, which is exactly what PHP 8's stricter internal functions turn into fatal errors. Set the target version in phpstan.neon and start at a low level:

parameters:
    level: 3
    phpVersion: 80400
    paths:
        - app
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse

Raise the level one step at a time once the earlier findings are fixed.

Step 4: automated fixes with Rector

Rector (opens in a new tab) rewrites code with rules. For a version upgrade, its PHP sets apply every rule up to the version you name:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withPhpSets(php84: true);
composer require --dev rector/rector
vendor/bin/rector process --dry-run
vendor/bin/rector process

Two cautions. First, the PHP sets also modernize syntax (constructor promotion, match, str_contains() and so on), so the output may no longer run on 7.4. Apply it on the branch where you have already dropped 7.4, or use withPhpLevel() to raise the rule set gradually, as Rector's docs recommend. Second, review the diff like a pull request from a colleague. Commit Rector's changes separately from your manual fixes so each is easy to review and revert.

What Rector won't catch

Rector changes syntax. It cannot know what values flow through your code at runtime, so these need tests and logs:

  • loose comparisons whose result changed in PHP 8.0
  • null arriving from the database into string functions
  • integers that used to be strings in PDO results
  • behavior of third-party packages and extensions

Common PHP 8 breakages, with code

These are the changes that cause most of the errors I see in legacy apps, grouped by the version that introduced them.

PHP 8.0: comparisons, TypeErrors and removed functions

String-to-number comparison changed. A non-numeric string is no longer converted to 0 when compared with a number:

var_dump(0 == 'abc');                        // 7.4: true   8.0+: false
var_dump(in_array(0, ['pending', 'paid']));  // 7.4: true   8.0+: false
var_dump('1' == '01');                       // true in both

Code that relied on the old result (often by accident, with a status column defaulting to 0) silently takes a different branch. Search for loose == and in_array() without the strict flag on mixed data.

Internal functions now throw instead of warning:

count(null);   // 7.4: warning, returns 0     8.0+: TypeError
strlen([]);    // 7.4: warning, returns null  8.0+: TypeError

// Fix: make the type explicit
$total = is_countable($items) ? count($items) : 0;

Removed features that fail immediately: each(), create_function(), money_format(), curly-brace string offsets ($str{0}, use $str[0]) and unparenthesized nested ternaries. PHP 4 style constructors (a method named like the class) fail quietly instead: they are simply no longer called, so objects are left uninitialized. Rename them to __construct(). The @ operator no longer hides fatal errors, so problems that were silent may suddenly show.

Resources became objects: curl_init() returns a CurlHandle and GD functions return GdImage, so is_resource($ch) checks now return false. Compare against false instead.

PHP 8.1: null arguments, return types, PDO and MySQLi

Passing null to a non-nullable parameter of an internal function is deprecated. This is the most common deprecation in legacy apps, because nullable database columns feed string functions everywhere:

$name = trim($row['middle_name']);         // Deprecated when the column is NULL
$name = trim($row['middle_name'] ?? '');   // Fixed, and works on 7.4

Classes implementing Countable, ArrayAccess, Iterator or JsonSerializable without return types trigger deprecations. Add the return types, or use the attribute, which PHP 7.4 reads as a comment:

final class LineItems implements \Countable
{
    private array $items = [];

    #[\ReturnTypeWillChange]
    public function count()
    {
        return \count($this->items);
    }
}

MySQLi now throws mysqli_sql_exception by default instead of returning false, so if (!$result) { ... } error handling never runs. Wrap queries in try/catch, or call mysqli_report(MYSQLI_REPORT_OFF) as a temporary measure. With PDO and emulated prepares on MySQL, integers and floats now come back as native PHP types, so $row['is_active'] === '1' becomes false. Also deprecated in 8.1: FILTER_SANITIZE_STRING, strftime(), and implicit lossy float-to-int conversion such as $arr[1.5].

PHP 8.2: dynamic properties and string helpers

Creating a property that the class does not declare is deprecated:

class Invoice
{
    public $total;
}

$invoice = new Invoice();
$invoice->discount = 10; // Deprecated in 8.2

// Fix: declare it (public $discount;) or, for legacy base classes only:
#[\AllowDynamicProperties]
class LegacyModel {}

Also deprecated: utf8_encode() and utf8_decode() (use mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1')), "${var}" string interpolation (use "{$var}"), and callables like "self::method" or "parent::method" passed as strings.

PHP 8.3, 8.4 and 8.5: smaller but real

PHP 8.3 deprecates get_class() and get_parent_class() without arguments, incrementing empty or non-alphanumeric strings, and decrementing empty or non-numeric strings. PHP 8.4 deprecates implicitly nullable parameter types, which appear in almost every older codebase:

function notify(Mailer $mailer = null) {}   // Deprecated in 8.4
function notify(?Mailer $mailer = null) {}  // Fixed, valid since PHP 7.1

PHP 8.4 also deprecates trigger_error() with E_USER_ERROR, the E_STRICT constant, mysqli_ping() and lcg_value(). PHP 8.5 deprecates the backtick operator (use shell_exec()), the cast names (integer), (boolean), (double) and (binary), null as an array offset, and case labels ending in a semicolon. The official migration guides on php.net list everything, version by version: 8.0 (opens in a new tab), 8.1 (opens in a new tab), 8.2 (opens in a new tab), 8.3 (opens in a new tab), 8.4 (opens in a new tab) and 8.5 (opens in a new tab).

Framework coupling: Laravel, CodeIgniter 3 and WordPress

For a framework app, the PHP 7 to 8 upgrade is usually two projects, and the framework is the bigger one.

  • Laravel. Laravel 6 and 7 top out at PHP 8.0 and Laravel 8 at 8.1, all end of life. Laravel 10 runs on PHP 8.3, but the framework itself no longer gets security fixes, so the realistic target is Laravel 12 (PHP 8.2 to 8.5) or Laravel 13 (PHP 8.3 to 8.5). Upgrade one major at a time. My Laravel 13 upgrade guide covers the last step, and I take on Laravel upgrades as fixed-scope projects.
  • CodeIgniter 3. The core still uses dynamic properties heavily, so PHP 8.2 and later fill the logs with deprecations and some apps break outright. I cover the fixes in CodeIgniter 3 on PHP 8 errors. For a long-lived app, a CodeIgniter 4 migration is the durable answer.
  • WordPress. Core supports PHP 8, but old themes and plugins often do not. Test every active plugin on staging with the target PHP and full error logging before you switch.

Server side: run both PHP versions side by side

Never upgrade PHP in place on a production server. Install the new version alongside the old one and point one site at a time to it. On Ubuntu with the widely used ondrej/php packages:

sudo apt install php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-gd php8.4-intl php8.4-bcmath

Then point a staging virtual host at the new FPM socket:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

Copy your custom ini values (memory_limit, upload_max_filesize, max_execution_time, OPcache settings) into the new version's conf.d folder, because they do not carry over. On cPanel, the MultiPHP Manager switches the version per domain. In both cases, update cron jobs and queue workers to call the new binary explicitly, for example /usr/bin/php8.4 artisan schedule:run. A forgotten cron still running 7.4 is the most common half-finished upgrade I come across.

Rollout and monitoring

  1. Run the full test suite and a manual click-through on staging with the new PHP.
  2. Switch production during a quiet window. With FPM, that is a socket change and a reload.
  3. Watch the error log and your error tracker for the first hours. Fix deprecations in the following days, but fix TypeErrors and fatals immediately.
  4. Keep the old PHP version installed for a week or two so rollback is one config change.
  5. Once stable, remove the old version and update "php" in composer.json so nobody installs packages that need the old runtime.

Frequently asked questions

How long does a PHP 7.4 to 8 upgrade take?

It depends mostly on the framework and test coverage, not the PHP version. A small, well-tested app can move in a day or two. A legacy app with no tests, an old framework version and abandoned packages can take weeks, because the framework upgrade comes along with it.

Can I upgrade straight from PHP 7.4 to 8.4?

Yes. You do not need to install each version in between. Scan with a testVersion range that covers 7.4 to 8.4 and run Rector with the PHP 8.4 set, so the code is checked against every change along the way.

Will my PHP 7.4 code run on PHP 8 without changes?

Simple code often runs, but rarely without some fixes. The usual failures are TypeErrors from internal functions that used to warn, changed loose comparisons, removed functions like each(), and an outdated framework or package that does not support PHP 8.

Is PHP 8 faster than PHP 7.4?

PHP 8 releases include performance work, but for a typical web app the database and network dominate response time, so the gain varies. Treat speed as a side benefit. The real reason to upgrade is security support.

Do I need to upgrade Laravel to use PHP 8?

Usually, yes. Laravel 8 supports PHP up to 8.1 and older versions stop at 8.0, all of which are end of life. Laravel 10 can run on PHP 8.3, but Laravel 10 itself is out of support, so aim for Laravel 12 or 13.

If you want someone to run this checklist on your app, I handle PHP and Laravel maintenance including version upgrades, working directly with you from audit to rollout.