skip to content

When upgrading a ten-year-old PHP 7.4 invoicing application to PHP 8.5, which changes are most likely to break it, and how do you sequence the work?

level: seniorimportance: must knowfreq 55%

answer

  1. string-number comparison changed in 8.0
  2. warnings became TypeError and ValueError
  3. PDO now throws by default
  4. null to built-in parameters deprecated 8.1
  5. fix on 7.4 first, then step up

basics

~20 s

Most breakage comes from 8.0: 0 == "" is false, many warnings became TypeError, count(null) throws, PDO throws by default. Then come later deprecations. Add tests, fix on 7.4, run 8.x in CI, walk the migration guides, then switch.

solid answer

~50 s

The big risk is **8.0**: comparing a number with a non-numeric string now compares strings, so `0 == ""` and `0 == "N/A"` became false; many warnings became `TypeError`/`ValueError` (for example `count(null)`); arithmetic on a non-numeric string throws; and **PDO's default error mode became exceptions**, so `if ($stmt->execute() === false)` never runs. Later minors add traps an invoicing app hits: 8.1 makes mysqli throw, returns native int/float from PDO MySQL with emulated prepares and deprecates `null` to built-in string parameters and `strftime()`; 8.2 deprecates dynamic properties; 8.4 deprecates `Foo $x = null` and rewrote `round()`, whose results can differ. Sequence: pin behaviour with characterisation tests on 7.4, run static analysis and the 8.5 interpreter in CI, fix what works on both, walk each migration guide from 8.0 to 8.5, then switch with a rollback plan.

code

php · 20 lines
php
<?php
// Legacy 7.4 idioms whose behaviour changes on PHP 8.5.
function normaliseDiscount(string $discount): string
{
    return $discount == 0 ? '0' : $discount; // '' == 0: true on 7.4, false on 8.0+
}

function lineCount(?array $items): int
{
    return count($items); // null: 7.4 warns and returns 0; 8.0+ throws TypeError
}

function saveInvoice(PDO $pdo, string $number): bool
{
    $stmt = $pdo->prepare('INSERT INTO invoice (number) VALUES (?)');
    if ($stmt->execute([$number]) === false) {
        return false; // reached on 7.4 (silent default); 8.0+ throws PDOException first
    }
    return true;
}

go deeper

for a junior

Recall that 8.0 changed number-to-string comparison and turned many warnings into exceptions, and that the manual has a migration guide per version.

for a middle

Name concrete breaks with examples: 0 == "", count(null), PDO exceptions, dynamic properties, implicitly nullable parameters, and explain the replacement for each.

for a senior

Lay out the sequence: characterisation tests, dependency upgrades, static analysis, dual-version CI, per-version migration guides, staged switch with rollback.

for a principal

Weigh a direct jump against intermediate stops, decide how much legacy behaviour to fix versus wrap, and set the risk bar for money calculations such as rounding.

## Why 7.4 to 8.5 is a big jump The move crosses one **major** release (8.0) and five minors. 8.0 allowed large behaviour changes, and every later minor added its own incompatibilities and deprecations. A ten-year-old invoicing application usually has thin tests, loose comparisons on form and database values, silent database error handling and money arithmetic on strings: exactly the areas that changed. ## What is most likely to break ### Comparisons and arithmetic (8.0) - **String-number comparison.** A number compared with a **non-numeric** string is now compared as strings: `0 == ""`, `0 == "N/A"` and `42 == "42foo"` are `false` on 8.0+, `true` on 7.4. Code such as `if ($discount == 0)` where `$discount` is `""` changes branch. - **Non-numeric strings in arithmetic** now throw `TypeError` instead of warning, so `$total + $row['fee']` with `fee = "N/A"` stops the request. - **Float to string is locale-independent**: after `setlocale(LC_ALL, 'de_DE')`, `(string) 3.14` used to be `"3,14"` and is now `"3.14"`. Invoices formatted by casting floats change; use `number_format()` or `NumberFormatter`. ### Warnings that became exceptions (8.0) - `count()` on a non-countable value such as `null` throws `TypeError`. - Internal functions throw `TypeError` or `ValueError` on invalid arguments instead of returning `null` or `false` with a warning. - Undefined variables, properties and array keys are warnings now, not notices, so logs fill up. ### Database error handling (8.0 and 8.1) - **PDO's default error mode changed from silent to exceptions.** Code that checked `false` returns now gets an uncaught `PDOException` instead. - **mysqli throws by default since 8.1** (`MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT`). - **PDO MySQL with emulated prepares returns native `int` and `float`** since 8.1, so `$row['qty'] === '1'` stops matching. `PDO::ATTR_STRINGIFY_FETCHES` restores strings. ### Deprecations on the way (8.1 to 8.5) | Release | Typical hit in an old invoicing app | |---|---| | 8.1 | `trim(null)` or `strlen(null)` on nullable columns; `strftime()` for invoice dates | | 8.2 | dynamic properties on model objects; `"${var}"` in templates; `utf8_encode()` | | 8.4 | `Customer $c = null` parameters; `round()` results that differ from 7.4 on some inputs | | 8.5 | backtick shell calls; `(integer)` and `(double)` casts; `case 1;` with a semicolon | The 8.4 `round()` rewrite deserves a specific test on an invoicing system: it removed "pre-rounding" and fixed cases such as `0.49999999999999994`, so a few totals can round differently. ## Fixes that run on both versions Most fixes are valid on 7.4 and on 8.5, which is what lets you make them before the switch: | 7.4 idiom | Replacement that runs on 7.4 and 8.5 | |---|---| | `$discount == 0` on form input | validate, cast, then compare strictly: `(int) $discount === 0` | | `count($maybeNull)` | `count($items ?? [])` | | `trim($nullableColumn)` | `trim($value ?? '')` | | `Customer $c = null` | `?Customer $c = null` (nullable types exist since 7.1) | | a dynamic property on a model | a declared property | | `"${var}"` in a template | `"{$var}"` | | `strftime()` for invoice dates | `date()` or `IntlDateFormatter` | | `(string) $float` for display | `number_format()` | ## How to sequence the work 1. **Pin current behaviour.** Write characterisation tests around totals, tax, rounding, date formatting and PDF output on 7.4. You need them to tell a bug fix from a regression. 2. **Upgrade the dependencies that block you.** Libraries and frameworks must support 8.x; this is often the largest single task. 3. **Run static analysis** at a level that catches type and null issues; it finds code paths the tests never reach. 4. **Add PHP 8.5 to CI next to 7.4** and fix what fails, preferring fixes that run on both: `===` with explicit casts, `?Customer $c = null`, declared properties, `number_format()`. 5. **Walk each migration guide** from 8.0 to 8.5, because some changes emit no notice, such as the locale-independent float cast. 6. **Stage and switch** with production-like data, error logging at `E_ALL`, and a tested rollback to the old runtime. Whether you stop at an intermediate version depends on the team; the test-first, fix-on-both approach is what keeps each step small.

  • Why fix deprecations on PHP 7.4 or the current version instead of after switching?
    Because most fixes are valid on both versions: `?Foo $x = null`, declared properties, `===` with explicit casts. Making them first keeps each change small and testable on the runtime you trust, and it shrinks the switch to a configuration change you can roll back. Fixing after switching mixes two sources of failure in one release.
  • The app relies on string values from PDO MySQL; what changes in 8.1 and how do you handle it?
    Since 8.1, PDO MySQL with emulated prepares returns native `int` and `float` values, matching native prepares. Strict comparisons against strings such as `$row['qty'] === '1'` stop matching. Either fix the comparisons to the real types, which is better, or set `PDO::ATTR_STRINGIFY_FETCHES` to restore strings while you migrate.
  • Why test round() specifically in an invoicing system on PHP 8.4 or later?
    PHP 8.4 rewrote integer rounding in `round()` and removed pre-rounding. The manual notes that some inputs, such as `0.49999999999999994`, now round differently. For invoices, compare totals from 7.4 and 8.5 over real data, and consider exact decimal arithmetic such as `BcMath\Number` or integer minor units instead of floats.

saying these in an interview costs you the question

  • 0 == "" is still true in PHP 8, so loose checks on empty fields behave the same.
  • PDO still returns false silently on failure unless you enable exceptions.
  • A clean run on 7.4 with no warnings means the code is ready for 8.5.
  • Changing the interpreter version in production first and fixing errors as they appear is a fine plan.
  • Minor releases after 8.0 contain no changes that affect an upgrade.
  • round() gives identical results on every PHP version, so money code needs no retesting.