Home: Roshan Kc

← All posts

PHP · Upgrades 10 min read

Upgrading a Legacy PHP 7 App to PHP 8.5: A Practical Playbook

Direct answer: To upgrade a PHP 7 app to PHP 8.5: get tests around the critical paths, run PHPStan to find type problems, use Rector to apply most syntax changes automatically, upgrade your framework and dependencies, then move PHP one version at a time in CI and staging before production. Budget days for a small app and weeks for a large one. Don't wait: PHP 7.4 has had no security fixes since November 2022, and PHP 8.2's security support ends December 31, 2026.

Unsupported PHP versions aren't a theoretical risk. During PHP2Shell, servers on PHP 8.1 and older got no official fix at all. This playbook is part of my PHP in 2026 series.

Where your version stands#

VersionSecurity fixes untilStatus (November 2026)
7.4 and olderEnded November 2022Unsupported: no fixes, including for PHP2Shell
8.0Ended November 2023Unsupported
8.1Ended December 2025Unsupported
8.2December 31, 2026Security fixes only, and ending in weeks
8.3December 31, 2027Security fixes only
8.4December 31, 2028Active support
8.5December 31, 2029Active support, the recommended target

Why target 8.5 rather than 8.6? 8.6 lands on November 19, 2026, and frameworks and extensions take a few weeks to certify a new release. 8.5 is mature, supported for three more years, and you can move to 8.6 later with a much smaller step. More in PHP 8.6 features.

Step 0: Inventory before you touch code#

  • Every place PHP runs: web servers, queue workers, cron, CI images, developer machines and admin tools. Production is rarely the only PHP install.
  • Extensions: php -m on each environment. Abandoned extensions are the most common hard blocker.
  • Dependencies: run composer outdated, and check which packages support PHP 8.5 (look at the require.php constraint on Packagist).
  • Framework version: often the real upgrade is the framework, with PHP following along.

Step 1: Build a safety net#

You don't need 100% coverage. You need tests on what would hurt most if it broke: authentication, payments, invoicing, tenancy boundaries, and your top five revenue paths. A handful of HTTP-level tests that hit real endpoints catch most upgrade regressions.

If you have no tests at all, start with "does every route return a non-500?" tests. They're crude, and they still catch a surprising amount.

Step 2: Static analysis with PHPStan#

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

Start at a low level and generate a baseline (--generate-baseline) so existing errors don't block you, then fix new ones. PHPStan finds undefined methods, wrong argument types and dead code, and those are exactly the things that become fatal errors when PHP 8 gets stricter. For Laravel, use Larastan. More on this in PHP's type system.

Step 3: Let Rector do the mechanical work#

Rector rewrites code automatically using rule sets per PHP version:

composer require --dev rector/rector

// rector.php
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
    ->withPhpSets(php85: true);

vendor/bin/rector process --dry-run   // review the diff first
vendor/bin/rector process

It handles implicitly nullable parameters, deprecated functions, old cast names, constructor promotion and dozens more. Review every diff, commit in small batches, and run the tests between batches.

Step 4: The breaking changes that bite#

ChangeVersionWhat breaks
String-to-number comparison8.00 == "abc" is now false. Loose comparisons in validation and permission checks change behaviour.
Stricter internal function types8.0Passing null or wrong types to built-ins throws TypeError instead of warning, and null to non-nullable built-in parameters is deprecated in 8.1
Warnings promoted to errors8.0Undefined properties and many former warnings now throw
Removed functions8.0each(), create_function() and other long-deprecated functions are gone
Dynamic properties deprecated8.2Setting undeclared properties on objects emits deprecations. Declare them or use #[\AllowDynamicProperties].
Implicitly nullable parameters8.4Foo $x = null is deprecated. Write ?Foo $x = null.
Backtick operator, old cast names8.5`cmd`, (integer) and (boolean) are deprecated. See PHP 8.5.

The null-to-built-in-function deprecation (strlen(null), trim(null)) is usually the noisiest in older codebases. Expect a lot of it, and fix it at the source rather than casting everywhere.

Step 5: Move one version at a time#

  1. Add the target PHP version to your CI matrix next to the current one, and keep both green.
  2. Turn deprecations into failures in CI (error_reporting(E_ALL), and fail tests on deprecations), so the next upgrade is easier.
  3. Deploy to staging on the new version and run real traffic or a replay through it.
  4. Roll out to production gradually, one service or a canary instance first.
  5. Watch error rates, memory use and response times for a few days before calling it done.

Going from 7.4 straight to 8.5 is possible with Rector, but stepping through 8.0 → 8.2 → 8.4 → 8.5 in CI makes each failure easier to understand.

Step 6: Upgrade the runtime properly#

If you deploy with containers, change the base image and rebuild without cache. If you're on servers, install from your distribution's or a trusted packager's repository, then restart every PHP process: FPM, Apache, queue workers and Octane workers. PHP2Shell taught a lot of teams that an upgraded package doesn't patch processes that are still running.

This is also a good moment to reconsider your runtime. The options are in PHP runtimes explained.

After the upgrade: never fall this far behind again#

  • Keep PHPStan and Rector in CI permanently.
  • Schedule a PHP minor upgrade every year, a couple of months after the November release.
  • Rebuild container images at least monthly so OS and library patches arrive too.
  • Track end-of-life dates on php.net/supported-versions.

Key takeaways#

  • PHP 8.1 and older are unsupported, and 8.2 loses security fixes on December 31, 2026. Target 8.5.
  • Order of work: inventory, tests on critical paths, PHPStan, Rector, dependencies, then PHP version by version.
  • The changes that bite most are string-to-number comparisons, nulls passed to built-ins, dynamic properties and removed functions.
  • Restart every PHP process after upgrading, and make yearly upgrades routine.

Sources#

Stuck on an old PHP version with a codebase nobody wants to touch? Talk to us. We'll help you map out a safe upgrade path.