Upgrading a legacy plugin to PHP 8.3 without a rewrite

17 January 2026

A plugin written against PHP 5.6, still in production, about 14k lines. The goal was to get it running on 8.3 with the smallest possible diff, because a rewrite was not going to be approved and would not have been a good idea anyway.

Order of operations

  1. Get it under static analysis before changing anything
  2. Fix what the analyser flags, in the order it flags it
  3. Only then run it against a real database

Doing it the other way round means debugging runtime errors one page load at a time, which on a plugin this size is weeks.

What actually broke

Passing null to string functions

The single biggest category. Since 8.1 this is a deprecation, and with warnings promoted it is a wall of noise. Most of it was trim($maybe_null) where the value comes out of a meta field that has never been set. A null coalescing default at the call site fixed nearly all of them.

Optional parameters before required ones

function fmt($sep = ',', $items) {}   // deprecated
function fmt(?string $sep, array $items) {}  // fine

Dynamic properties

Assigning to a property that was never declared is deprecated as of 8.2. In a 2014-era codebase this is everywhere. Declaring the properties is the right fix; the attribute that suppresses the deprecation is for buying time, not for keeping.

Curly brace string offsets

$s{0} is gone. Mechanical replacement with $s[0].

The tooling that made it tractable

composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=1

Start at level 0 or 1, not at the maximum. The goal is a clean run at a low level that you then raise one step at a time, not a list of 4,000 problems you will never read. Raising the level after each green run turned a hopeless list into about a week of small commits.

The thing I nearly missed

Sorting behaviour changed. usort is stable as of 8.0, which is an improvement, but code that relied on the old unstable order to produce a particular tie-break now produces a different one. Nothing errors. The output is just quietly different. Worth diffing a report before and after rather than trusting that a clean static analysis means a clean upgrade.