Communitygithub.com

PHP-version-migration

Eine Rezeptsammlung für veraltete und breaking Changes bei PHP-Version-Upgrades, speziell für baserCMS und CakePHP-basierte Projekte.

Was ist PHP-version-migration?

Diese Skill-Sammlung bietet eine umfassende, versionierte Anleitung zur Migration von PHP-Versionen (8.2, 8.4, 8.5) für baserCMS und ähnliche Projekte. Sie klassifiziert typische Fehler (fatal vs. deprecated), zeigt konkrete Code-Beispiele (Before/After) und gibt Strategien zur Massenkorrektur (z. B. mit grep/sed). Behandelt werden unter anderem dynamische Eigenschaften, implizite nullable Parameter, Änderungen an fgetcsv/ReflectionProperty/setAccessible, nicht-normative Casts und die neuen reservierten Klassennamen in PHP 8.5. Die Sammlung enthält außerdem Hinweise zur Kompatibilität mit PHP 8.1, zur Fehleranalyse in Testläufen und zu frameworkspezifischen Fallstricken (CakePHP). Sie ist als lebendes Dokument konzipiert, das bei neuen PHP-Versionen um einen neuen Abschnitt erweitert wird.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add baserproject/baser-skills

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Leitfaden zur PHP-Versionsmigration (baserCMS)

Eine Rezeptsammlung für veraltete und breaking Changes, die bei PHP-Version-Upgrades auftreten können, sortiert nach Version. Probleme, die direkt aus dem CakePHP-Framework stammen, werden in der separaten Skill cakephp-migration behandelt.

Enthält: PHP 8.2 / 8.4 / 8.5. Bei der Unterstützung einer neuen Version wird ein neuer Abschnitt „## PHP X.Y“ hinzugefügt. Hinweis: PHP 8.5 ist die letzte Version vor 9.0. Alle in 9.0 als veraltet markierten Funktionen werden entfernt. Daher sollten die in 8.5 auftretenden Deprecation-Warnungen frühzeitig beseitigt werden.

Grundprinzipien

  • Unterscheiden Sie zwischen „Fatal Error“ und „Deprecation Warning“. Im Debug-Modus werden Deprecation-Warnungen unten auf dem Bildschirm angezeigt und sehen aus wie Fehler, aber Einträge mit debug: in logs/debug.log sind nur Warnungen (der Code läuft weiter). Fatal/Exception sind echte Fehler.
  • Regression von bestehenden Fehlern trennen. Prüfen Sie mit git diff HEAD -- <file>, ob die betreffende Datei zu Ihren Änderungen gehört.
  • Wenn Sie die Signatur einer Elternklasse ändern, passen Sie auch Kindklassen und Interfaces an.
  • Zur vollständigen Erkennung aller Deprecations sind statische Analysetools wie Rector effektiv.

Informationen zur Ausführung von Unit-Tests, Syntaxüberprüfungen und Fehleranalyse finden Sie in der Skill basercms-unittest. Beachten Sie, dass php -l (Syntaxprüfung) auch Warnungen zu impliziten Nullable-Typen usw. anzeigt.


PHP 8.2

Dynamische Eigenschaften (deprecated)

Die Zuweisung an nicht deklarierte Eigenschaften ist veraltet. Es erscheint die Meldung Creation of dynamic property X::$prop is deprecated.

  • Lösung A (empfohlen): Deklarieren Sie die Eigenschaft in der Klasse.
    /** @var ThemeFoldersServiceInterface */
    protected $ThemeFoldersService;
    protected $_tempalteTypes = [];
    
    Typischerweise betrifft dies Zuweisungen wie $this->Xxx = $this->getService(...) im Konstruktor.
  • Lösung B: Für Basisklassen, von denen viele Kindklassen betroffen sind (z. B. die Testbasis BcTestCase mit über 400 Unterklassen), fügen Sie #[\AllowDynamicProperties] hinzu. Das Attribut wird an Kindklassen vererbt, daher reicht eine Stelle.
    #[\AllowDynamicProperties]
    class BcTestCase extends TestCase { ... }
    

PHP 8.4

1. Implizite nullable Parameter (deprecated)

Die Angabe von Typ $x = null ist als impliziter nullable-Typ veraltet. Verwenden Sie explizit ?Typ.

// Vorher
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// Nachher
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
  • Erkennung (Regex): Findet Parameter, die kein ? vor dem Typ haben, aber = null enthalten. Beispiel: ([A-Za-z_\\][A-Za-z0-9_\\]*)\s+(\$[A-Za-z0-9_]+)\s*=\s*null (wobei kein ? davor steht).
  • Wenn Sie die Signatur einer Elternklasse ändern, passen Sie auch Kindklassen/Interfaces an.
  • Warnung: Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead.

2. $escape-Parameter von fgetcsv() (deprecated)

Die Angabe des $escape-Parameters ist nun erforderlich, das Weglassen ist veraltet.

// Vorher
$head = fgetcsv($fp, 10240);
// Nachher
$head = fgetcsv($fp, 10240, ',', '"', '\\');
  • Betrifft nur Stellen, die tatsächlich fgetcsv() aufrufen. Eigene Implementierungen (z. B. fgets + Regex zum Parsen von CSV) sind nicht betroffen.

3. Einzelargument-Aufruf von ReflectionProperty::setValue() (deprecated)

Bei nicht-statischen Eigenschaften muss das Zielobjekt übergeben werden.

// Vorher
$property->setValue($value);
// Nachher
$property->setValue($object, $value);
  • Warnung: Calling ReflectionProperty::setValue() with a single argument is deprecated.
  • Tritt häufig in Test-Hilfsfunktionen auf (z. B. zum Setzen privater Eigenschaften).
  • Für statische Eigenschaften übergeben Sie null als erstes Argument: setValue(null, $value). Dies funktioniert in allen Versionen 8.1–8.5 (bricht die Kompatibilität zu 8.1 nicht).

4. Übergabe von null an string-Parameter von Kernfunktionen (deprecated, eigentlich ab 8.1+)

Die Übergabe von null an Parameter, die nicht als ?string deklariert sind (z. B. parse_url(null), version_compare($a, null), str_replace(..., null)), löst die Warnung Passing null to parameter #N ($x) of type string is deprecated aus. Verwenden Sie einen expliziten (string)-Cast.

// Vorher → Nachher
parse_url($url);                       →  parse_url((string)$url);
version_compare($current, $required);  →  version_compare((string)$current, (string)$required);
  • Diese Deprecation wurde in PHP 8.1 eingeführt, tritt aber bei Migrationsarbeiten (insbesondere zu 8.4/8.5) gehäuft auf, weshalb sie hier aufgeführt ist.
  • Da die Warnung nur erscheint, wenn der Wert zur Laufzeit null ist, ist sie durch statisches Greppen nicht vollständig erfassbar. Zählen Sie alle Vorkommen von Passing null to parameter in den Testlogs, um die Quellen zu identifizieren.

PHP 8.5 (Release: 2025-11-20)

Die letzte Minor-Version vor 9.0. Die meisten der folgenden Punkte lösen nur Warnungen aus, der Code läuft weiter. Da sie jedoch in 9.0 entfernt werden, sollten sie behoben werden. Die Liste ist nach absteigender Wahrscheinlichkeit von Auswirkungen auf die Anwendung/das Framework sortiert. ※ Nur Punkt „0.“ ist ein fataler Fehler (breaking Change) und hat höchste Priorität.

⚠️ Hinweis zur Aufrechterhaltung der Kompatibilität mit PHP 8.1 (wenn composer.json "php": ">=8.1" erfordert). Das Beheben einiger Deprecations in PHP 8.5 mit der „empfohlenen“ Alternative kann dazu führen, dass das Gleiche in PHP 8.1 nicht existiert und ein fataler Fehler auftritt. Die unten mit ❌ gekennzeichneten Punkte sollten daher zurückgestellt werden, bis die Unterstützung für 8.1 endet (die Warnung in 8.5 ist akzeptabel). Das Ausführen unter 8.5 selbst ist möglich, ohne 8.1 zu beschädigen (nur Warnungen).

  • ❌ str_increment() (8.3+) → unter 8.1 bleibt es bei $s++
  • ❌ http_get_last_response_headers() (8.4+) → unter 8.1 bleibt es bei $http_response_header
  • ❌ Namensraum-PDO (z. B. Pdo\Mysql::ATTR_*, 8.4+) → unter 8.1 bleibt es bei PDO::MYSQL_ATTR_*

Alle anderen Punkte (nicht-normative Casts, null-Offset → ''-, Entfernung von Auto-Free-Funktionen, Backticks → shell_exec, case ;, __serialize) können sicher auch unter 8.1 angewendet werden.

0. [Breaking Change, Fatal] Verwendung reservierter Klassennamen (z. B. Array) mit use führt zu Fatal

Ab PHP 8.5 sind Namen wie Array als spezielle Klassennamen reserviert. Die Verwendung von use ...\Array; oder class Array führt zu einem fatalen Fehler (keine Warnung). Wenn dies bereits in der Autoloading-Phase der Tests passiert, kann die gesamte Testsuite nicht gestartet werden. Daher hat dies höchste Priorität.

Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
  • Beispiel aus baserCMS: In MailMessagesTableTest.php fanden sich am Anfang use ...\Array; und use ...\ClassRegistry; (beides tote Importe von nicht existierenden Klassen aus der CakePHP2-Ära). Unter PHP 8.4 waren sie harmlos, unter 8.5 verursachen sie einen Fatal. Löschen Sie die betreffenden use-Zeilen (die tatsächlichen Referenzen waren bereits durch markTestIncomplete deaktiviert).
  • Erkennung: grep -rnE "use [A-Za-z0-9_\\\\]+\\\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\s*;" --include="*.php".
  • Hinweis: Da es sich um einen fatalen Fehler handelt, kann er mit php -l für einzelne Dateien gefunden werden. Ein vorheriges Durchsuchen mit grep verhindert jedoch, dass die Testsuite bereits in der Ladephase abstürzt.

1. Nicht-normative Cast-Namen (deprecated, hohe Auswirkung)

Die folgenden Casts sind veraltet und sollten durch die regulären Namen ersetzt werden: (boolean), (integer), (double), (binary).

// Vorher → Nachher
(boolean) → (bool)
(integer) → (int)
(double)  → (float)
(binary)  → (string)
  • Erkennung: grep -rnE "\\((boolean|integer|double|binary)\\)" --include="*.php".
  • Findet sich oft in älterem Code. Kann mechanisch ersetzt werden.

2. Verwendung von null als Array-Offset (deprecated)

Die Verwendung von null als Schlüssel in Array-Zugriffen (z. B. $arr[null], array_key_exists(null, $arr)) ist veraltet. Es wurde implizit in "" umgewandelt. Verwenden Sie stattdessen explizit einen leeren String "".

// Vorher
$arr[null] = $v;  array_key_exists(null, $arr);
// Nachher
$arr[''] = $v;    array_key_exists('', $arr);
  • Achten Sie auf Stellen, an denen eine Variable als Schlüssel verwendet wird ($arr[$key]) und $key null sein kann.
  • Schwer mit statischem Greppen zu erkennen (tritt nur auf, wenn der Wert zur Laufzeit null ist). Führen Sie alle Tests unter PHP 8.5 aus und sammeln Sie die Quellen mit grep "null as an array offset" phpunit.log (die Ausgabe enthält on line N of <file>).
  • Korrekturstrategie: ① Ersetzen Sie den Schlüssel durch $key ?? '' (entspricht der alten impliziten Konvertierung, achten Sie darauf, dass Set und Get denselben Schlüssel verwenden). ② Fügen Sie vor isset()/Zugriff eine Prüfung $key !== null && ein. Alternativ kann der Rückgabewert von getData('batch') usw. mit ?? '' abgefangen werden.
  • Beispiele aus baserCMS (insgesamt 308 Fälle): BcFileUploader (Set/Get von uploadingFiles[$bcUploadId] mit ?? '' harmonisiert), BcUploadBehavior (oldEntity[$alias][$entity->_bc_upload_id] mit ?? ''), MailMessagesTable ($dists[$mailField->group_valid]), SearchIndexes/Contents/PermissionsController (getData('batch') ?? ''), BcTextHelper::arrayValue / BcUtil::decodeContent ($key !== null && isset(...)).

3. Ressourcen-Freigabefunktionen für Objekte (deprecated)

Die entsprechenden Ressourcen sind nun Objekte, die durch den Garbage Collector automatisch freigegeben werden. Daher ist der explizite Aufruf von close/free-Funktionen veraltet. Entfernen Sie den Aufruf.

  • curl_close() / curl_share_close() (CurlHandle / CurlShareHandle)
  • finfo_close() (finfo)
  • imagedestroy() (GdImage) ← Häufig in Bildverarbeitungs-Hilfsfunktionen
  • xml_parser_free() (XMLParser)
  • Erkennung: grep -rnE "\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\\(" --include="*.php".
  • Beispiel aus baserCMS: Imageresizer.php und BcFileUploader.php enthielten 4 imagedestroy()-Aufrufe. Sie können entweder einfach gelöscht oder durch unset($img) ersetzt werden, um die sofortige Speicherfreigabe innerhalb von Schleifen zu erhalten (GdImage ist ein Objekt, das der GC beim Lösen der Referenz freigibt. Sicher ab PHP 8.1).

3-2. ReflectionProperty/Method::setAccessible() (deprecated, häufigster Fall)

setAccessible() ist seit PHP 8.1 ein No-Op (private/protected sind mit getValue/setValue/invoke direkt zugänglich). In PHP 8.5 wird es als deprecated markiert. Entfernen Sie die Aufrufzeile (sicher ab >=8.1).

// Vorher
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true);   // ← Diese Zeile entfernen
$property->setValue($obj, $v);
  • Beispiel aus baserCMS: 33 Vorkommen in src/ und Tests (u. a. BcUtil, BcTestCase, BaserCorePlugin, PreviewController, BcDatabaseService). Da sie sich an Hot-Paths (z. B. Test-Basisklassen) befinden, ist die Anzahl der Laufzeit-Vorkommen um Größenordnungen höher (ca. 6.300 in der gesamten Testsuite ≈ der Großteil der 8.5-Deprecations). Wenn Sie diese zuerst beheben, reduziert sich das Log drastisch.
  • Massenlöschung (nur für einzelne Anweisungen): sed -i -E "/^[[:space:]]*\$[A-Za-z0-9_]+->setAccessible\(true\);[[:space:]]*$/d" <file>.
  • Hinweis zur Erkennung: Wenn das grep-Muster mit ->setAccessible beginnt, könnte - als Option interpretiert werden. Verwenden Sie grep -rn "setAccessible" (vermeiden Sie den führenden Bindestrich) oder grep -e.

4. Inkrementieren von nicht-numerischen Strings (deprecated)

$str++ (für nicht-numerische Strings) ist veraltet. Verwenden Sie str_increment(). ※ str_increment() ist erst ab PHP 8.3 verfügbar. Solange die Kompatibilität mit 8.1 gewahrt werden muss, bleibt es bei $str++.

// Vorher
$code++;            // Angenommen $code ist z. B. 'AZ'
// Nachher
$code = str_increment($code);

5. __sleep() / __wakeup() (soft deprecated)

Verwenden Sie __serialize() / __unserialize() (wenn keine Kompatibilität mit PHP 7 mehr benötigt wird).

6. $http_response_header (deprecated)

Die Superglobale $http_response_header ist veraltet. Verwenden Sie http_get_last_response_headers(). ※ http_get_last_response_headers() ist erst ab PHP 8.4 verfügbar. Solange die Kompatibilität mit 8.1 gewahrt werden muss, bleibt es bei $http_response_header.

7. Backtick-Operator (deprecated)

Der Backtick-Operator `command` (ein Alias für shell_exec()) ist veraltet. Verwenden Sie shell_exec() direkt.

8. Beenden eines case-Statements mit ; (deprecated)

case 1; ist veraltet. Verwenden Sie case 1:.

  • Erkennung: grep -rnE "case\s+.+;\s*$" --include="*.php" (Achtung: Kann Fehlalarme liefern, daher visuelle Überprüfung empfohlen).

8-2. array_key_exists(null, ...) (deprecated, oft durch ungültige Serialisierung ausgelöst)

Die Übergabe von null als erstes Argument an array_key_exists() ist veraltet (verwenden Sie einen leeren String). Wenn dies im Framework-Core (CakePHP JsonView::_dataToSerialize) auftritt, liegt die Ursache in einer ungültigen Form der serialize-Option in Ihrem eigenen Code. Wenn Sie setOption('serialize', ['foo', 'message' => $message]) verwenden, also 'key' => Wert in die Liste der Variablennamen mischen, wird $message möglicherweise null auftreten, was zu array_key_exists(null, ...) führt.

  • Korrektur: Die serialize-Option sollte eine Liste von View-Variablennamen sein. ['foo', 'message' => $message] → ['foo', 'message'] (die Werte werden mit $this->set() gesetzt). Beispiel aus baserCMS: MailMessagesController (index/view-Aktionen).
  • Identifikation der Quelle, wenn sie im Framework liegt: Patchen Sie das Vendor-Verzeichnis temporär (z. B. if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString())). Führen Sie dann die Tests des betreffenden Plugins aus, identifizieren Sie den eigenen Controller im Stacktrace und stellen Sie das Vendor-Verzeichnis wieder her.

8-3. Übergabe eines Objekts als backing-Array an ArrayObject (deprecated)

Die Übergabe eines Objekts (z. B. stdClass von json_decode()) an new ArrayObject($obj) ist veraltet. Casten Sie es in ein Array.

  • Korrektur: new ArrayObject(json_decode($body)->devices) → new ArrayObject((array)json_decode($body)->devices). Beispiel aus baserCMS: SitesControllerTest.

9. Sonstiges (falls zutreffend)

  • PDO-treiberspezifische Konstanten/Methoden werden in den Namensraum verschoben (z. B. PDO::MYSQL_ATTR_* → Pdo\Mysql::ATTR_*, PDO::sqliteCreateFunction() → Pdo\Sqlite::createFunction()). Betrifft meist den Framework-Core, es sei denn, Sie verwenden solche Konstanten selbst. Das "uri:"-DSN-Schema von PDO ist ebenfalls veraltet. ※ Namensraum-PDO-Klassen sind erst ab PHP 8.4 verfügbar. Solange die Kompatibilität mit 8.1 gewahrt werden muss, bleibt es bei PDO::MYSQL_ATTR_*.
  • Der $key_length-Parameter von openssl_pkey_derive() und der $context-Parameter von finfo_buffer() sind veraltet (werden ignoriert).
  • Die INI-Einstellung register_argc_argv ist in Nicht-CLI-SAPIs veraltet (verwenden Sie $_GET / $_SERVER['QUERY_STRING']).
  • Einige Closure-Bindings (z. B. an statische Closures) sind veraltet.
  • Geänderte Fehlermeldung für clone null (Testauswirkungen): Bis PHP 8.4 hieß es __clone method called on non-object, ab PHP 8.5 clone(): Argument #1 ($object) must be of type object, null given. Tests, die String-Vergleiche für solche Fehlerfälle durchführen, müssen die erwartete Meldung aktualisieren (Beispiel baserCMS: UploaderCategoriesControllerTest in Admin/Api test_copy).

⚠️ Fehlerketten in vollständigen Testläufen: In dieser Codebasis kann es selten vorkommen, dass ein vorheriger Test einen globalen Zustand (z. B. Plugin-Konfigurationen in Configure) verschmutzt. Dies führt dazu, dass fast alle nachfolgenden Tests im Application-Bootstrap mit Fehlern wie PluginCollection::create(): $config ... null given fehlschlagen. Wenn eine einzelne Plugin-Suite (z. B. plugins/baser-core/tests/TestCase) erfolgreich läuft und ein erneuter Durchlauf sauber ist, liegt es nicht am Code, sondern an Flakyness. Überprüfen Sie dies immer durch Isolation und Wiederholung.


Vorgehen bei der Untersuchung (Checkliste)

  1. Verwenden Sie php -l (Syntaxprüfung) oder logs/debug.log, um die Datei und Zeile der Deprecation-Warnung zu identifizieren.
  2. Klassifizieren Sie das Muster im entsprechenden Versions-Abschnitt und korrigieren Sie es. Für mechanisch ersetzbare Muster (z. B. nicht-normative Casts) können Sie grep zur Massenidentifikation verwenden.
  3. Führen Sie php -l erneut aus, um zu prüfen, ob die Warnung verschwunden ist.
  4. Führen Sie den entsprechenden Test isoliert mit --filter aus. Führen Sie abschließend alle Tests erneut aus.

Verwandt: Probleme, die im Framework (Rückgabewerte von Events, doppelte Associations, ResultSet, FormProtection usw.) liegen, werden in der Skill cakephp-migration behandelt.


Quellen

Verwandte Skills