eXtreme Legacy 6: From PHP 5 to PHP 8 19/11/2025
π€ The Migration Warlord breaks the rusted shackles of PHP 5.6, leading the great exodus through the valleys of breaking changes, into the promised land of PHP 8.4 where type hints and constructor property promotion await in glory! π₯
In this series, we're dealing with BisouLand, an eXtreme Legacy application (2005 LAMP spaghetti code base). So far, we have:
- π got it to run in a local container
- π¨ written Smoke Tests
- π― written End to End Tests
- π§Ή created and applied Coding Standards
- β migrated to PDO
This means we can run it locally (http://localhost:43000/), and have some level of automated tests.
But it's still stuck in the past with PHP 5.6, so let's upgrade it to PHP 8.4!
Docker
With the migration from the deprecated MySQL extension to PDO, BisouLand's codebase is technically compatible with the latest PHP version.
At least that's what Claude tells me.
If that's true, then upgrading it is as simple as modifying the monolith's Dockerfile:
- # Uses PHP 5.6 with PDO MySQL driver
+ # Uses PHP 8.4 with PDO MySQL driver
- FROM php:5.6-apache
-
- # Update sources.list to use archive repositories for Debian Stretch
- RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list \
- && sed -i 's/security.debian.org/archive.debian.org/g' /etc/apt/sources.list \
- && sed -i '/stretch-updates/d' /etc/apt/sources.list
+ FROM php:8.4-apache
We can now build the new image:
cd apps/monolith
make app-init # docker down, docker build, docker up
And that's it, welcome to the future!
Rector
Your eXtreme Legacy application might not be as lucky as BisouLand, and might require more work for an upgrade.
One way to do so is to use Rector, an automated refactoring tool that uses the power of AST to make sure the changes it makes are safe (i.e. non breaking) to do. Let's install it:
cd apps/qa
make composer arg='require --dev rector/rector'
We then need to configure it by creating the rector.php file:
<?php
declare(strict_types=1);
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withCache(
// CI compatible temporary paths for cach
cacheDirectory: '/tmp/rector',
cacheClass: FileCacheStorage::class,
)
->withPaths([
__DIR__,
__DIR__.'/../monolith',
])
->withSkip([
// ββ Excluded paths βββββββββββββββββββββββββββββββββββββββββββββββββββ
// [qa]
__DIR__.'/vendor',
// [monolith]
__DIR__.'/../monolith/vendor',
])
->withSets([
// ββ PHP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::PHP_56,
]);
We can then run it as follow:
make rector # ./vendor/bin/rector
This initial run will ensure both the Monolith and QA apps are PHP 5.6 compliant, which they are so no changes done.
We can be bold and brave and change Rector's config straight from SetList::PHP_56
to SetList::PHP_84, which will then make all the necessary upgrades from PHP 5.6 to 8.4,
but incremental steps are safer so we'll start with PHP 7.0:
sed -i -e 's/PHP_56/PHP_70/g' ./rector.php
# π On Mac: sed -i '' -e 's/PHP_56/PHP_70/g' ./rector.php
Running it (make rector) will spot one change that needs to be done:
replace rand() with random_int():
// Si les bisous du dΓ©fenseurs sont prΓ©sent, donc qu'il n'attaque pas.
if (0 == $DefBloque) {
- $DefSmack = floor($DefSmack * (1 - 1 / rand(2, 10)));
- $DefBaiser = floor($DefBaiser * (1 - 1 / rand(2, 10)));
- $DefPelle = floor($DefPelle * (1 - 1 / rand(2, 10)));
+ $DefSmack = floor($DefSmack * (1 - 1 / random_int(2, 10)));
+ $DefBaiser = floor($DefBaiser * (1 - 1 / random_int(2, 10)));
+ $DefPelle = floor($DefPelle * (1 - 1 / random_int(2, 10)));
}
Technically rand is still supported in PHP 8,
but it does not generate cryptographically secure digits,
so random_int is recommended instead.
Next step is to upgrade to PHP 7.1, which detects no changes, so we're safe to upgrade to PHP 7.2, which detects no changes, so we're safe to upgrade to PHP 7.3, which detects some changes!
The function setcookie can take an array of options as a third parameter
(and yes, we store the password in the cookie. This will need to be fixed later):
if (isset($_POST['auto'])) {
$timestamp_expire = time() + 30 * 24 * 3600;
- setcookie('pseudo', $pseudo, $timestamp_expire);
- setcookie('mdp', $mdp, $timestamp_expire);
+ setcookie('pseudo', $pseudo, ['expires' => $timestamp_expire]);
+ setcookie('mdp', $mdp, ['expires' => $timestamp_expire]);
}
Next step is to upgrade to PHP 7.4, which detects no changes, so we're safe to upgrade to PHP 8.0, which detects no changes, so we're safe to upgrade to PHP 8.1, which detects some changes!
This is an interesting one, as it enforces the type of variables for function
calls that expect for example a string but might receive null:
$pdo = bd_connect();
if (isset($_POST['action'])) {
$cout = 0;
- $nuageCible = htmlentities($_POST['nuage']);
- $positionCible = htmlentities($_POST['position']);
+ $nuageCible = htmlentities((string) $_POST['nuage']);
+ $positionCible = htmlentities((string) $_POST['position']);
Next step is to upgrade to PHP 8.1, which detects no changes, so we're safe to upgrade to PHP 8.2, which detects no changes, so we're safe to upgrade to PHP 8.3, which detects no changes, so we're safe to upgrade to PHP 8.4, which detects some changes!
This one is for the QA app, it removes the now unnecessary parenthesis around object instantiation:
- (new Dotenv())->load(__DIR__.'/../../../monolith/.env');
+ new Dotenv()->load(__DIR__.'/../../../monolith/.env');
And that's it.
PHP CS Fixer
With the latest version of PHP,
we can change or Coding Style in .php-cc-fixer.dist.php
to take into account new changes:
- // ββ Disabed rules due to PHP version compatibility βββββββββββββββββββ
-
- // [PER-CS2.0] Partially disabled due to PHP version constraints.
- 'trailing_comma_in_multiline' => [
- 'after_heredoc' => true,
- 'elements' => [
- // 'arguments', For PHP 7.3+
- // 'array_destructuring', For PHP 7.1+
- 'arrays',
- // 'match', For PHP 8.0+
- // 'parameters', For PHP 8.0+
- ],
- ],
+ // ββ Overriden rules ββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ // [Symfony] Adding `['elements']['parameters']` (Symfony doesn't have it)
+ 'trailing_comma_in_multiline' => [
+ 'after_heredoc' => true,
+ 'elements' => [
+ 'arguments',
+ 'array_destructuring',
+ 'arrays',
+ 'match',
+ 'parameters',
+ ],
+ ] ,
But that's not all. PHP CS Fixer also has rule set to help us migrate the style from old PHP versions to new ones, which we'll do incrementally.
First we enable the rule sets:
// ββ CS Rule Sets βββββββββββββββββββββββββββββββββββββββββββββββββββββ
'@Symfony' => true,
'@Symfony:risky' => true,
+ '@PHP7x0Migration' => true,
+ '@PHP7x0Migration:risky' => true,
And we run it with make cs-fix. It's detected some changes:
replacing mt_rand which is also not cryptographically secure with random_int:
// On choisi une valeur au hasard.
- $FinalPos = $FreePos[mt_rand(0, $nbLibre - 1)];
+ $FinalPos = $FreePos[random_int(0, $nbLibre - 1)];
Next step we upgrade the rule set to PHP 7.1
sed -i -e 's/PHP7x0/PHP7x1/g' ./.php-cs-fixer.dist.php
# π On Mac: sed -i '' -e 's/PHP7x0/PHP7x1/g' ./php-cs-fixer.dist.php
Running it, we get void return type hints:
- function GiveNewPosition($idJoueur)
+ function GiveNewPosition($idJoueur): void
{
$pdo = bd_connect();
$sql_info = $pdo->query('SELECT nombre FROM nuage WHERE id=1');
Next step is to upgrade to PHP 7.2, which detects no changes, so we're safe to upgrade to PHP 7.3, which detects some changes!
This one is related to HEREDOC indentation.
I'm gonna be brief now, as after that, upgrading all the way to PHP 8.4, there were no changes.
Conclusion
With a modern version of PHP, not only do we get security patches, exciting new feature (typed code, constructor property promotion, readonly final classes, etc), as well as access to many modern tools and libraries (composer, Symfony components, etc), but we also get a boost in performance.
Let's back up that last claim by running some vanity benchmarks as follow:
- start a fresh Monolith container
- sign up and log in a temporary account
- test load the homepage, as a visitor (not logged in)
- test load the Brain page (logged in)
# Start fresh
cd apps/monolith
make app-init
BENCH_USER="BisouTest_bench"
BENCH_PASS="SuperSecret123"
# Sign up
curl -X POST 'http://localhost:43000/inscription.html' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "Ipseudo=${BENCH_USER}&Imdp=${BENCH_PASS}&Imdp2=${BENCH_PASS}&inscription=S%27inscrire"
# Log in
BENCH_COOKIE=$(curl -X POST 'http://localhost:43000/redirect.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "pseudo=${BENCH_USER}&mdp=${BENCH_PASS}&connexion=Se+connecter" \
-i -s | grep -i 'set-cookie: PHPSESSID' | sed 's/.*PHPSESSID=\([^;]*\).*/\1/' | tr -d '\r')
# Test load homepage (not signed in)
ab -l -q -k -c 50 -n 10000 http://localhost:43000/ \
| grep -E "Complete requests|Failed requests|Exception|Requests per second|Time per request.*across"
# Test load Brain page (signed in)
ab -l -q -k -c 50 -n 10000 -C "PHPSESSID=$BENCH_COOKIE" http://localhost:43000/cerveau.html \
| grep -E "Complete requests|Failed requests|Exception|Requests per second|Time per request.*across"
On my MacBook M4 (with Docker), the results are as follow:
- Homepage:
+8.9%improvement- Requests per second (mean):
from
545.67to594.49 - Time per request (ms, mean, across all concurrent requests):
from
1.833to1.682
- Requests per second (mean):
from
- Brain Page:
+30.2%improvement- Requests per second (mean):
from
313.88to408.78 - Time per request (ms, mean, across all concurrent requests):
from
3.186to2.446
- Requests per second (mean):
from
Surely with such almighty gains we won't need to rewrite BisouLand in go / rust.
βοΈ What do you mean, "the code is still unrefactorable"?