Upgrade von PHP 7.4 auf 8.4: was bricht und in welcher Reihenfolge

Der Weg von PHP 7.4 auf 8.4 Schritt für Schritt: die Breaking Changes aus 8.0 bis 8.4, die echte Websites treffen, mit den Befehlen zum Scannen und Beheben.

Vorab

PHP 7.4 hat am 28. November 2022 das End of Life erreicht. Die PHP2Shell-Fixes gibt es dafür nur als Backport-Branch im Quellcode, ein 7.4-Release mit diesen Fixes existiert nicht. Der Ausweg ist der Wechsel auf einen unterstützten Branch. Ziel ist 8.4 (behoben in 8.4.27). Siehe welche Version Sie wählen sollten und PHP 7.4.

Die Arbeit zwischen 7.4 und 8.4 lässt sich nicht überspringen, Sie können sie aber in einem Durchgang erledigen. Diese Seite listet auf, was sich in jeder Version geändert hat. Jeder Punkt stammt aus den Migrationsleitfäden im PHP-Handbuch, verlinkt unter den Quellen.

Die Reihenfolge, die funktioniert

  1. Sichern Sie Dateien und Datenbank. Testen Sie die Wiederherstellung.
  2. Kopieren Sie die Website auf eine Staging-Umgebung mit PHP 8.4 und denselben Extensions.
  3. Aktualisieren Sie Framework, CMS, Plugins und Composer-Pakete auf Versionen, die 8.4 offiziell unterstützen. composer outdated und composer why-not php 8.4 zeigen, was Sie blockiert.
  4. Scannen Sie Ihren eigenen Code mit PHPCompatibility (Befehle weiter unten).
  5. Beheben Sie die Funde, zuerst die Punkte aus 8.0, denn die brechen Code.
  6. Betreiben Sie Staging mit error_reporting = E_ALL und log_errors = On. Gehen Sie jeden Ablauf durch und lesen Sie danach das Log.
  7. Beheben Sie die Deprecations aus 8.1 bis 8.4. Sie brechen heute keinen Code, füllen aber die Logs, und die betroffenen Features können in einer späteren Version wegfallen.
  8. Stellen Sie die Produktion um, starten Sie jeden PHP-Worker neu und prüfen Sie die Version.

PHP 8.0: die Änderungen, die Code brechen

8.0 ist ein Major Release. Hier liegt der Großteil der echten Probleme.

  • Vergleich von Strings und Zahlen. 0 == "foo" war true und ist jetzt false. Dasselbe gilt für 0 == "" und 42 == "42foo". Suchen Sie nach losen ==-Vergleichen mit Benutzereingaben oder Datenbankwerten und nehmen Sie ===.
  • Aus Warnings wurden Exceptions. Arithmetik mit einem nicht numerischen String wirft TypeError. count() auf einem nicht zählbaren Wert wirft TypeError. Eine undefinierte Konstante wirft einen Error (7.4 las sie als String). Eine falsche Anzahl Argumente bei einer eingebauten Funktion wirft ArgumentCountError.
  • Aus Notices wurden Warnings. Das Lesen einer undefinierten Variable, eines undefinierten Array-Keys oder einer undefinierten Property erzeugt jetzt eine Warning. Alte Themes und Plugins werden das Log damit füllen.
  • Entfernte Funktionen. each(), create_function(), __autoload(), money_format(), get_magic_quotes_gpc() und fgetss() gibt es nicht mehr. Nutzen Sie foreach, anonyme Funktionen und spl_autoload_register().
  • Entfernte Syntax. $str{0} (nutzen Sie $str[0]), die Casts (real) und (unset) sowie Methoden mit dem Namen der Klasse als Konstruktor (nutzen Sie __construct()).
  • Vorrang bei der Verkettung. Bei "Sum: " . $a + $b wird jetzt zuerst addiert. Setzen Sie Klammern, wo Sie die alte Reihenfolge gemeint haben.
  • Named Arguments. call_user_func_array() behandelt String-Keys jetzt als Parameternamen, statt sie zu ignorieren. Prüfen Sie Aufrufe, die assoziative Arrays übergeben.
  • Resources sind Objekte. curl_init() liefert ein CurlHandle, auch die GD- und OpenSSL-Funktionen liefern Objekte. Code, der is_resource($ch) prüft, schlägt jetzt fehl. Prüfen Sie stattdessen auf false.
  • @ unterdrückt keine Fatal Errors mehr. Error-Handler, die error_reporting() == 0 prüfen, sollten error_reporting() & $errno prüfen.
  • Standardwerte. error_reporting steht standardmäßig auf E_ALL. Der Standard-Fehlermodus von PDO sind Exceptions.
  • Reservierte Namen. match ist ein Schlüsselwort, und mixed darf kein Klassenname sein. #[ leitet jetzt ein Attribut ein statt eines Kommentars.
  • Extensions und Module. mbstring.func_overload ist weg, XML-RPC ist nach PECL gewandert. Der Name des Apache-Moduls hat sich von php7_module zu php_module geändert, alte LoadModule- und IfModule-Zeilen greifen deshalb nicht mehr.
  • Deprecated in 8.0. Ein Pflichtparameter nach einem optionalen, libxml_disable_entity_loader() und Sortier-Callbacks, die einen Boolean statt eines Integers zurückgeben. Nutzen Sie <=> in usort().

PHP 8.1: meist Deprecations, zwei geänderte Verhaltensweisen

  • null an eingebaute Funktionen. Wer null an einen nicht nullable Parameter übergibt, etwa str_contains($x, null) oder trim($x), bekommt eine Deprecation ins Log. Bei altem WordPress-Code ist das die größte Quelle für Log-Rauschen. Casten Sie mit (string) $x oder nutzen Sie $x ?? ''.
  • mysqli wirft Exceptions. Der Standard-Fehlermodus wechselte von stumm zu Exceptions. Kapseln Sie Aufrufe, die sich auf stilles Scheitern verlassen haben, oder stellen Sie den alten Modus mit mysqli_report(MYSQLI_REPORT_OFF); wieder her.
  • htmlspecialchars() nutzt standardmäßig ENT_QUOTES | ENT_SUBSTITUTE, aus ' wird also &#039;.
  • Rückgabetypen interner Methoden. Klassen, die ArrayAccess, Iterator, Countable oder JsonSerializable implementieren, brauchen einen kompatiblen Rückgabetyp oder das Attribut #[\ReturnTypeWillChange], sonst gibt es eine Deprecation.
  • Serializable allein ist deprecated. Ergänzen Sie __serialize() und __unserialize().
  • Weitere Deprecations. strftime(), FILTER_SANITIZE_STRING, das Anhängen an false, als wäre es ein Array, und implizite Umwandlungen von Float zu Int mit Genauigkeitsverlust, etwa $a[15.5].
  • PDO liefert mit den Treibern für MySQL und SQLite Integer und Floats jetzt als native Typen.

PHP 8.2: Dynamic Properties

  • Dynamic Properties sind deprecated. Wer $obj->newField = 1 auf einer Klasse setzt, die newField nie deklariert hat, bekommt eine Deprecation. Deklarieren Sie die Property. Als Notlösung dient #[\AllowDynamicProperties]. stdClass ist ausgenommen.
  • String-Interpolation mit "${var}" ist deprecated. Schreiben Sie "{$var}" oder "$var".
  • utf8_encode() und utf8_decode() sind deprecated. Nutzen Sie mb_convert_encoding().
  • Relative Callables wie "self::method" sind deprecated. Nutzen Sie [self::class, 'method'].
  • Funktionen für Groß- und Kleinschreibung wie strtolower() und ucfirst() ignorieren jetzt die Locale. Für lokalisierten Text nehmen Sie die mb_*-Funktionen.
  • str_split('') liefert ein leeres Array statt [""].

PHP 8.3: wenig Bruch

  • range() ist strenger. Eine Schrittweite von 0, eine negative Schrittweite bei aufsteigendem Bereich sowie INF oder NAN werfen ValueError.
  • number_format() rundet bei negativem $decimals jetzt vor dem Dezimalkomma.
  • Ein negativer erster Array-Index ändert den nächsten impliziten Key: Nach $a[-5] ergibt $a[] den Key -4 statt 0.
  • Datumsfehler nutzen jetzt die Klassen DateException und DateError statt Warnings.
  • Deprecated: get_class() ohne Argumente, assert_options() und die INI-Einstellungen assert.* sowie ReflectionProperty::setValue() mit nur einem Argument.

PHP 8.4: die Deprecation für nullable Parameter

  • Implizit nullable Parameter. function f(Foo $a = null) ist deprecated. Schreiben Sie ?Foo $a = null. Alte Bibliotheken lösen das bei jedem Laden aus, aktualisieren Sie also erst die Pakete, bevor Sie Ihren eigenen Code bewerten.
  • E_STRICT ist weg, und trigger_error() mit E_USER_ERROR ist deprecated. Werfen Sie stattdessen eine Exception.
  • CSV-Escape. Sich auf das Standardargument escape von fgetcsv(), fputcsv() und str_getcsv() zu verlassen, ist deprecated. Übergeben Sie es explizit.
  • Nach PECL verschoben: IMAP, OCI8, PDO_OCI und PSpell. Wenn Sie imap_*-Funktionen nutzen, installieren Sie das PECL-Paket oder wechseln Sie die Bibliothek.
  • Session-Einstellungen. Das Ändern von session.sid_length, session.sid_bits_per_character, session.use_only_cookies, session.use_trans_sid und verwandten Einstellungen ist deprecated, ebenso die Konstante SID.
  • round() wirft ValueError bei einem ungültigen Modus, und das alte Vorrunden ist entfallen. Einige Ergebnisse ändern sich dadurch.
  • Der bcrypt-Cost steht standardmäßig auf 12 statt 10. Logins dauern länger. Bestehende Hashes lassen sich weiter prüfen.
  • Die Standardwerte für den OPcache-JIT haben sich geändert. Wenn Sie JIT nur über opcache.jit_buffer_size aktiviert haben, setzen Sie zusätzlich opcache.jit.
  • Neue Namen können kollidieren. Jede in 8.4 neue Funktion oder Klasse bricht Code, der denselben Namen definiert. Kapseln Sie eigene Helfer in function_exists().
  • Höhere Mindestversionen für Bibliotheken. Das Handbuch nennt libcurl 7.61.0, libxml2 2.9.4 und OpenSSL 1.1.1. Sehr alte Server erfüllen das unter Umständen nicht.

Befehle zum Scannen und Beheben

Führen Sie diese Befehle auf einem Arbeitsrechner oder in der CI aus. Auf dem Webserver haben sie nichts zu suchen.

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -i

# während des Umzugs: Code muss auf beiden Versionen laufen
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 7.4-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

# nach der Umstellung: nur noch 8.4 als Ziel
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.4 \
  --ignore='*/vendor/*,*/node_modules/*' .

Die Versionsangabe oben ist die, die das README von PHPCompatibility am 22. Oktober 2026 genannt hat. Die aktuelle finden Sie im README. PHPCompatibility 10 braucht PHP_CodeSniffer 4.0.2 oder neuer.

Rector kann einen Teil der Fixes automatisch anwenden. Sehen Sie sich den Diff an, bevor Sie ihn committen.

composer require rector/rector --dev
vendor/bin/rector                      # bietet an, rector.php anzulegen
vendor/bin/rector process --dry-run    # Vorschau
vendor/bin/rector process              # anwenden
<?php
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withPhpSets();   // liest die Ziel-PHP-Version aus composer.json

Keines der beiden Tools ersetzt Tests auf Staging. Sie lesen Code und führen ihn nie aus. Lassen Sie Plugin- und Theme-Code, der nicht Ihnen gehört, in Ruhe und aktualisieren Sie ihn stattdessen. Für WordPress siehe PHP-Version ändern.

Zuletzt geprüft am 22. Oktober 2026.