— Artikel — № 131

131 —PHP

PHP 7.4 naar 8.2 upgrade: pre-flight audit-checklist

Je hebt ja gezegd tegen de PHP 8.2-sprong op een tien jaar oude WordPress. Voordat het onderhoudsvenster opengaat, dit is de audit die het saai houdt.

Foto van boven: ruitjespapier PHP-upgrade checklist, functievellen, manilla AUDIT-map, messing PRE-FLIGHT plaat, vulpen.
Hero · gestileerd stilleven№ 131

Op vrijdag om 17:40 stuurde een partner van een Nederlands bureau waar we mee werken ons één regel: "Hosting zegt dat ze maandag PHP 8.2 doordrukken, wat checken we eerst?" De site was een WordPress 6.4-installatie met twaalf jaar aangekoekte themacode, twee custom plugins die voor het laatst in 2019 zijn aangeraakt, en een Magento 1-sidecar waar niemand het graag over heeft. Het onderhoudsvenster was 90 minuten op zondagavond.

Deze post is de audit die we toen draaiden. Het is dezelfde checklist die we nu meegeven aan iedereen die een verouderde site in één stap van PHP 7.4 naar 8.2 wil tillen, want de meeste hosters laten je niet meer apart stagen van 7.4 naar 8.0 naar 8.1 naar 8.2. Je springt of je blijft.

De vier uur die je echt nodig hebt

De volledige PHP 7.4 naar 8.2 audit kost ongeveer vier uur op een single-site WordPress- of Drupal-installatie, plus nog eens twee uur als er een custom plugin of module is die naar de database schrijft. Wie zegt dat het in vijftien minuten kan met een compatibility scanner, heeft de output van die scanner niet gelezen.

Het meeste werk zit niet in de PHP-code. Het zit in de omringende stack: de MySQL-collation, de opcache-config, de .htaccess-directives die stilzwijgend uitgingen van mod_php, de cron jobs die php aanroepen zonder versie-pin. Hieronder de volgorde waarin we ze afwerken.

Deprecations die echt breken

Vergeet de volledige lijst op php.net. Op echte legacy WordPress- en Drupal-codebases zijn er vier deprecation-klassen tussen 7.4 en 8.2 die zo goed als elke fatal verklaren die we ooit hebben gezien bij een PHP 7.4 naar 8.2 cutover.

Impliciet nullable parameters

Deze is nu een deprecation en binnenkort een fatal. Elke functie zoals deze:

function fetch_user(string $email, array $opts = null) {
    // ...
}

Geeft een Deprecated-notice in 8.1 en 8.2 omdat $opts impliciet nullable is. De fix is één karakter:

function fetch_user(string $email, ?array $opts = null) {
    // ...
}

Oudere WordPress-plugins zitten er vol mee. Draai een grep over de plugin-directory vóór de cutover:

grep -rnE 'function[^(]+\([^)]*=\s*null' wp-content/plugins/

Dynamic properties

PHP 8.2 deprecate het zetten van een property op een class die hem niet heeft gedeclareerd. Code die vijftien jaar werkte, waarschuwt nu:

class Cart {}
$c = new Cart();
$c->total = 99.50; // Deprecated in 8.2

Magento 1, de helft van de oudere payment integrations van WooCommerce, en zo goed als elk "MVC-framework dat één persoon in 2014 heeft geschreven" loopt hier tegenaan. De pragmatische tijdelijke fix is de #[AllowDynamicProperties]-attribute. De juiste fix is de properties declareren.

Null coalescing op offsets van non-arrays

Als je legacy code dit deed:

$name = $maybe_user['name'] ?? 'guest';

En $maybe_user was soms false in plaats van null of een array, dan gaf 7.4 stilletjes 'guest' terug en gooien 8.0 en later Cannot access offset of type bool. Drupal-modules die FALSE teruggaven bij een cache miss zijn de grootste boosdoeners.

String-naar-nummer juggling

De klassieker. 0 == "abc" was vroeger true. In 8.0 is het false. Elke custom auth-check die een gehasht token los vergeleek met een gestringificeerde input laat nu stilletjes verkeerde dingen door, of weigert geldige. Grep op losse == in elk codepad dat authenticatie of signed URLs aanraakt en zet om naar ===.

Inventaris van extensions en SAPI

De op één na grootste oorzaak van een mislukte 8.2-sprong is niet de code. Het is een extensie die niet is meegekomen. Draai dit vóór het onderhoudsvenster op de live server met de 7.4 PHP-binary:

php -m > /tmp/ext-74.txt
php --ri opcache > /tmp/opcache-74.txt
php -i | grep -E '^(PHP Version|Loaded Configuration|Scan this dir|Server API)'

Draai dan hetzelfde op de 8.2-build (de meeste hosters laten je via SSH expliciet php8.2 aanroepen). Diff ze. Let specifiek op:

  • mysqli met mysqlnd. WordPress draait op beide drivers, maar als de 7.4-build alleen de libmysqlclient-variant had, doet de 8.2-build dat waarschijnlijk niet. Plugins die op runtime extension_loaded('mysqlnd') aanroepen, schakelen zichzelf hard uit.
  • imagick versus gd. WooCommerce thumbnail-regeneration gaat ervan uit dat aan beide kanten van de verhuizing dezelfde aanwezig is.
  • opcache. Als 7.4 was getuned met opcache.jit=1255, dan veranderde precies die flag van semantiek in 8.0. De directive in php.ini laten staan geeft een startup warning, geen fatal, maar het logt bij elke request.
  • intl, bcmath, gmp. Magento 2 en elke Drupal Commerce-installatie hebben alle drie nodig. Sommige hosters leveren ze op 8.2 als optioneel, terwijl ze op 7.4 standaard aan stonden.

Nu je toch in de SAPI-sectie zit, check of de hoster in dezelfde wijziging van mod_php naar PHP-FPM is overgestapt. Veel doen dat, in stilte. Je .htaccess heeft misschien een blok als dit:

<IfModule mod_php7.c>
    php_value upload_max_filesize 64M
    php_value memory_limit 256M
</IfModule>

Onder PHP-FPM doen php_value-directives in .htaccess helemaal niets. Je hebt in plaats daarvan een .user.ini-bestand in de docroot nodig. Dit is veruit de meest voorkomende oorzaak van "het uploadformulier accepteert na de upgrade stilletjes geen grote bestanden meer".

De databasekant van de sprong

PHP 8.0 veranderde hoe mysqli standaard fouten meldt: exceptions aan, warnings uit. Elk codepad dat dit doet:

$result = mysqli_query($link, $sql);
if (!$result) {
    log_error(mysqli_error($link));
    return [];
}

Komt nu nooit meer in het if-blok. Het throwt. Pak het in een try/catch of zet mysqli_report(MYSQLI_REPORT_OFF) expliciet tijdens de transitie, en plan daarna een fatsoenlijk exception-pad.

Nu je toch de database doorloopt, check de collation. WordPress 4.2 en later gebruiken standaard utf8mb4_unicode_520_ci. Een site die in 2015 op MySQL 5.5 draaide, staat waarschijnlijk nog op utf8_general_ci met drie-byte UTF-8. De 8.2-upgrade dwingt zelden een MySQL-bump af, maar als je hoster je in hetzelfde venster ook van MySQL 5.7 naar 8.0 verhuist, gaan vier-byte emoji in productomschrijvingen van per-ongeluk-werken naar throwen. Draai dit tegen elke tekstkolom die je belangrijk vindt:

SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND DATA_TYPE IN ('varchar','text','longtext','char')
ORDER BY TABLE_NAME;

Je wilt één collation over het hele schema. Gemengde collations veroorzaken stille WHERE-mismatches die eruitzien als ontbrekende data.

Een rollback die je in 90 seconden draait

Elk audit-document dat we ooit hebben gezien eindigt met "maak een back-up". Dat is geen rollback-plan. Een rollback-plan is één commando, getest vóór de cutover, dat de site in minder dan twee minuten weer werkend terugbrengt. Dit is degene die wij gebruiken:

#!/usr/bin/env bash
set -euo pipefail
SITE=/var/www/example.com
STAMP=$(date +%Y%m%d-%H%M)

# 1. snapshot the current state to a sibling dir
cp -al "$SITE" "${SITE}.pre82-${STAMP}"

# 2. dump the database in a single file, no locks on InnoDB
mysqldump --single-transaction --quick --routines \
  --databases example_db > "/var/backups/example-${STAMP}.sql"

# 3. write a one-liner that reverses both
cat > "/var/backups/rollback-${STAMP}.sh" <<EOF
#!/usr/bin/env bash
set -euo pipefail
rm -rf "$SITE"
mv "${SITE}.pre82-${STAMP}" "$SITE"
mysql example_db < "/var/backups/example-${STAMP}.sql"
echo "rolled back to ${STAMP}"
EOF
chmod +x "/var/backups/rollback-${STAMP}.sh"
echo "rollback ready: /var/backups/rollback-${STAMP}.sh"

De cp -al gebruikt hardlinks, dus de snapshot kost vrijwel geen schijfruimte en duurt seconden, zelfs op een 40GB-site. Test het rollback-script op een staging-kopie vóór het onderhoudsvenster, niet erin.

Het eerste uur na de cutover

Zodra 8.2 live staat, is het eerste uur niet "surf naar de homepage". Het is samen tailen van de PHP error log en de slow query log. De meeste deprecations en stille fouten duiken op in de eerste 200 page-views, niet in je smoke test.

tail -F /var/log/php-fpm/error.log /var/log/mysql/slow.log \
  | grep --line-buffered -E 'Deprecated|Fatal|Notice: Trying|Slow_query'

Als één Deprecated-notice meer dan tien keer in vijf minuten afgaat, zit hij vrijwel zeker in een codepad dat bij elke request draait. Vind hem voordat de deprecation-lijst uitgroeit tot een log-flood die een echte fatal verbergt.

Toen we Pier bouwden liepen we hier keer op keer tegenaan met de bureaus waar we mee werken. We losten het uiteindelijk op met een chat-first editor die elke save koppelt aan een versiegeschiedenis-entry, plus een ingebouwde MySQL-editor voor de collation- en error-mode checks hierboven, zodat de rollback-target voor één bestand één klik is in plaats van het hele script opnieuw draaien.

Als je vandaag niets anders doet, schrijf dan het rollback-${STAMP}.sh-script voor de volgende site in je queue en draai het één keer tegen staging. De audit kan tot morgen wachten. De geteste rollback niet.

— Vragen —

Kan ik 8.0 en 8.1 overslaan en direct van 7.4 naar 8.2 gaan?

Ja, de meeste hosters forceren dit. De audit blijft hetzelfde; je krijgt alleen de deprecations van alle drie de releases in één venster in plaats van drie. Het risico zit in het volume, niet in iets nieuws.

Hoe lang duurt een fatsoenlijke PHP 7.4 naar 8.2 audit echt?

Vier uur op een single-site WordPress- of Drupal-installatie, plus ongeveer twee uur per custom plugin of module die naar de database schrijft. Beloofde audits van vijftien minuten zijn scanner-output, geen audit.

Wat wordt het vaakst over het hoofd gezien bij de upgrade?

PHP-FPM die de php_value-directives in .htaccess negeert. Hosters stappen in hetzelfde venster vaak ook over van mod_php naar PHP-FPM, en upload-limieten vallen stilletjes terug naar de defaults tot je een .user.ini-bestand toevoegt.