PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Applikationen migrieren von PHP 5.6 auf PHP 7.3

without comments

PHP 7.0 ist nun schon über drei Jahre verfügbar, 7.3 ist auch schon wieder 9 Monate alt. PHP 7.4 wird Ende diesen Jahres erscheinen.

In den letzten 2 Jahren habe ich mehrere Projekte von PHP 5.6 auf eine 7er Version bringen dürfen. Zwei davon waren etwas größer, von denen möchte ich hier berichten.

Ich hatte in einem Artikel „PHP 7: Migration eines Projekts„, der kurz vor dem Release von PHP 7.0 erschien, schon einige kleine Tipps gegeben wie man einen Überblick bekommen kann über die zu bearbeitenden Baustellen. Heute soll es etwas mehr ins Eingemachte gehen.

Die 2 Projekte haben jeweils über 100.000 Zeilen „eigenen Code“, also ohne externe Libraries, so dass ich sie als „groß“ bezeichnen möchte. Das eine Projekt wurde zu Zeiten von PHP 5.3 gestartet und entwickelt, hat in den letzten 9 Jahren auch einiges an Pflege und Aktualisierungen erfahren. Externe Bibliotheken wurden ab und zu erneuert, es wurde teils auf Composer umgestellt, und hat auch in den letzten Jahren das ein oder andere PHP 5.4, 5.5 und 5.6 Feature erhalten.
Das andere Projekt ist etwas älter und stammt aus dem Jahr 2009, d.h. PHP 5.2 war damals aktuell, und es wurde noch kompatibel zu PHP 4 erstellt, da noch nicht alle Welt PHP 5 nutzte. Außerdem hat das Projekt seitdem kaum Aktualisierungen bekommen, quasi alles stammt noch aus der damaligen Zeit, ihr werdet später hier im Artikel sehen was ich damit meinen könnte 🙂

PHPStorm „Deprecated“-Inspection

Also gut, zuerst wollte ich mir jeweils einen Überblick verschaffen, und haben diverse Analyse-Tools über die Projekte laufen lassen. Als erstes ließ ich die „Deprecated“-Inspection von PHPStorm laufen. Dazu habe ich die genutzte PHP-Version auf 7.3 gestellt, und danach mittels Code->“Run inspection by name“->“Deprecated“ laufen lassen. Im ersten Projekt waren es nur 14 Einträge, recht übersichtlich und schnell zu beheben.
Im zweiten Projekt jedoch wurden 79 Probleme erkannt. Die häufigsten waren:

  • Function „ereg“ is deprecated
  • Function „eregi“ is deprecated
  • Function „ereg_replace“ is deprecated
  • Function „eregi_replace“ is deprecated
  • Function „split“ is deprecated
  • Function „set_socket_blocking“ is deprecated
  • Function „set_magic_quotes_runtime“ is deprecated
  • Function „mysql_*“ is deprecated

ereg(), eregi(), ereg_replace(), eregi_replace()

ereg() ist leicht zu ersetzen durch preg_match(), der Name der Funktion muss geändert werden, und dem Pattern fügt man Begrenzungszeichen (Delimiter) hinzu (meistens /, | oder #).
Aus

ereg('[a-z]', $string)

wird also

preg_match('/[a-z]/', $string)

eregi() wird ebenfalls durch preg_match() ersetzt, Begrenzungszeichen hinzugefügt, und hinter dem hinteren Begrenzungszeichen der Modifikator i eingesetzt.

Aus

eregi('[a-z]', $string)

wird also

preg_match('/[a-z]/i', $string)

Selbiges gilt für ereg_replace() und eregi_replace().

split(), set_socket_blocking(), set_magic_quotes_runtime()

split() kann durch explode() ersetzt werden wenn kein Regex im Suchbegriff enthalten ist, sondern nur „normale Strings“ (Literals). Ansonsten muss preg_split() genutzt werden.

set_socket_blocking() kann in den meisten (oder gar allen?) Fällen durch stream_set_blocking() ersetzt werden.

set_magic_quotes_runtime() war im Code dankenswerterweise eh an allen Stellen bereits in einem if-Konstrukt verschachtelt, so dass set_magic_quotes_runtime() seit PHP 5.4 nie aufgerufen wurde:

if (get_magic_quotes_runtime()) {
   @set_magic_quotes_runtime(0);
}

Seit PHP 5.3 ist set_magic_quotes_runtime() deprecated, in PHP 7.0 wurde es entfernt.
Seit PHP 5.4 liefert get_magic_quotes_runtime() immer FALSE, und ist bis 7.3 immer noch verfügbar.

mysql_* Funktionen

Hier gilt es, eine Langzeitbaustelle endlich zu beackern. Im „moderneren“ Projekt haben wir das die letzten Jahre kontinuierlich erledigt, und alles auf PDO umgestellt, bzw. neue Scripte immer direkt mit PDO geschrieben. Hier gab es nichts zu tun.

Im zweiten, älteren Projekt war dankenswerterweise die Wahl zwischen mysql_* und mysqli_* bereits vorhanden, man konnte es einfach per Konfiguration umschalten (es wurde dann die Datenbank-Abstraktionsklasse „project_mysqli extends project_database“ statt „project_mysql extends project_database“ genutzt). ABER: Beim Durchgehen fiel auf, dass der ursprüngliche Entwickler nicht an allen Stellen mysql_* und mysqli_* variabel programmiert hatte, denn an einigen wenigen Stellen wurde noch

return mysql_result($res, 0, "col");

im Code genutzt. Wenn mysqli genutzt wird, nutze ich nun:

return mysqli_fetch_array($res, MYSQLI_ASSOC)['col'];

php7mar

Nachdem diese ersten Schritte gemacht sind, und alle Deprecated-Meldungen aus PHPStorm berichtigt wurden, ließ ich php7mar nochmal laufen. Dort wurden mir dann „nur noch“ folgende Probleme angezeigt:

  • oldClassConstructors
  • funcGetArg
  • variableInterpolation

PHP4-Style Konstruktoren

Ersteres sind Überbleibsel aus der PHP4 Zeit, da das alte Projekt vermutlich zu PHP4-Zeiten begonnen wurde, oder zumindest noch kompatibel und mit alten PHP4-Versionen lauffähig sein sollte.

Diese alten Konstruktoren sind schnell ersetzt durch die modernen __construct() Konstruktoren, jedoch gibt es dabei ein Problem, bzw. eine Sache, auf die geachtet werden sollte:

Die alten Konstruktor-Funktionen könnten explizit aufgerufen werden in Kindklassen.  Beispielsweise gibt es eine Vaterklasse „database“ und eine Kindklasse „mysql“, die folgendermaßen aussehen:

class database
{
  function database() {

  }
}

class mysql extends database
{
  ....
  function do() {
    $this->database();
  }
}

Nun gibt es 2 Möglichkeiten:
1. Einfach den Namen aller Konstruktoren ändern auf __construct(), und danach im ganzen Projekt suchen nach expliziten Aufrufen der alten Methode „database()“. Diese werden dann auch durch parent::__construct() ersetzt.
2. Man lässt die alte Methode der Vaterklasse bestehen, und fügt zusätzlich noch einen neuen Konstruktor __construct() hinzu. In diesem Fall müsste man nur die Vaterklasse „database“ ändern, und die Kindklasse „mysql“ könnte so belassen werden wie sie ist, denn die alte Methode „database()“ gibt es nach wie vor, und sie funktioniert.

class database
{
  function __construct() {  // neu hinzugefügt, VOR dem alten PHP4-Style Konstruktor

  }

  function database() {
    $this->__construct();
  }
}

Für welche der beiden Möglichkeiten man sich entscheidet, hängt vielleicht auch von der Anzahl an Kindklassen ab. Oder falls es eine öffentlich verfügbare und von fremden Leuten genutzte Bibliothek ist, sollte man vielleicht auch die zweite Methode wählen, so dass bestehender Fremdcode (Kindklassen) weiterhin ohne Änderungen funktionieren.

func_get_arg() + variableInterpolation

Zu func_get_arg() hatte ich bereits etwas geschrieben. Man kann schnell prüfen ob die Verhaltensänderung eintritt oder nicht, in den aller meisten Fällen kann man es so lassen wie es ist.

Eine der gefährlichsten Änderungen an PHP 7.0 ist die Änderung der Uniform Variablen Syntax. Darüber hatte ich im letzten Artikel bereits ausführlich geschrieben. Da es keine Warnungen, Deprecation-Meldungen, oder „undefined function“-Errors gibt, sondern der Bug evtl. unsichtbar ausgeführt wird, finde ich ihn gefährlich.
Dankenwerterweise gibt php7mar einige mögliche Stellen aus, die man sich angucken sollte. Es sind evtl. auch einige Falschmeldungen dabei, aber auch welche, bei denen man aktiv werden muss.

Zwischenfazit

Nachdem nun all diese Fehler berichtigt wurden, sind die meisten Fehler verschwunden, das Error-Log bleibt leer, und die Webseiten und Scripte funktionieren. Aber fertig sind wir noch nicht, denn die bisherigen Tools haben noch nicht alles gefunden. Ich habe ein drittes Tool genutzt, um die beiden Projekte zu analysieren und mir weitere PHP 7.x Probleme anzeigen zu lassen.

Exakat

Ein wirklich großartiges Tool, ich bin erstaunt dass ich es erst jetzt entdeckt habe, und in den letzten Jahren noch nie davon gehört habe. Exakat ist Open Source auf Github zu finden, es ist ein PHP Analyzer, und es ist großartig. Es wird seit über 4 Jahren fast täglich daran gearbeitet. Einzig die Installation ist recht umfangreich (Neo4J mit Gremlin zu installieren ist schonmal eine Sache für sich), aber Docker sei Dank nur eine Sache von Minuten, das einzig schwierige ist das ungeduldige Warten beim Download 🙂

Die Konfiguration von Exakat hat mich einige Zeit gekostet, bis ich verstanden habe wie es funktioniert. Wo muss das Projekt liegen, wo die Konfiguration? Wie erhöhe ich die Maximal Anzahl an Tokens, denn es kam die Fehlermeldung dass ich das Limit von 1 Million Tokens überschritten hätte und die Analyse abgebrochen wurde. Das Projekt hat ziemlich genau 150.000 Lines of Code, was in ca. 1,5 Million PHP-Tokens resultierte… Das neue Limit von 2 Millionen war dann ausreichend.

Nachdem der Docker-Container einsatzbereit ist, muss das Projekt eingerichtet werden („exakat init“), und dann der eigentlich Analyse-Lauf gestartet werden. Ich habe es verbose mit „-v“ gestartet, um etwas sehen zu können, denn es dauerte beim einen Projekt über 2 Stunden, bis das Ergebnis in Form einer HTML-Datei im Ordner „reports“ vorlag. Hier ein Screenshot:

Wenn man sich diese Übersicht in Ruhe anschaut, wird einem Angst und Bange. 150.000 Zeilen in 196 Dateien, im Durchschnitt also 765 Zeilen pro Datei. Die größte PHP-Datei hat mehr als 7500 LoC… Das ist ein Monster… 7500 Zeilen mit 13455 Issues darin… 2 Issues pro Zeile, das muss man erstmal schaffen 🙂

Die anderen Dateien sind zwar etwas kleiner, aber mindestens genauso voll mit Issues. Das dürfte Jahre dauern, wenn man die alle berichtigen wollen würde… Insgesamt über 90.000 Issues.

In den „Favorites“ findet sich eine Statistik, die besagt, dass an 967 Stellen exit aufgerufen wird, und an 65 Stellen die… Dabei lernt man doch dass man das nicht nutzen sollte, da Destruktoren nicht mehr schön aufräumen können, oder andere „nach-Request-Aufräum-Plugins“ nicht mehr so laufen wie gewünscht etc… Naja, nicht zu ändern, es läuft ja 🙂 Außerdem enden 193 der 196 Dateien mit einem schliessenden PHP-Tag ?>, auch ein Code-Smell heutzutage.

Mich interessieren aber nicht die ganzen kleinen Unschönheiten, sondern erstmal nur die PHP 7.x Inkompatibilitäten. Links im Menu klicken auf „Compatibility -> 7.1“, und schon bekommt man eine Liste der Probleme angezeigt. Ich habe die Analyse einmal auf dem ursprünglichen Code laufen lassen, so dass ich auch z.B. die Vorkommnisse von ereg() in der Liste sehe, Exakat hat sie alle gefunden:

Die vorbildliche Dokumentation erklärt beim Klick auf das Buch, was es mit dem jeweiligen Check auf sich hat. Einige sind wichtig, andere wiederum eher weniger. „PHP Keywords as Names“ beispielsweise bemängelt die Namen der Variablen $class oder $return. Unschön, aber kein Problem.

Die usort() Problematik habe ich mir schnell angeschaut. Seit PHP 7.0 werden bei usort() gleiche Werte eventuell in einer anderen Reihenfolge als in PHP < 7.0 sortiert. In den meisten Fällen kein Problem.

„Use random_int()“ und „Use password_hash()“ sind Empfehlungen, um die neuen, kryptografisch besseren Funktionen zu nutzen. Ja, kann man machen, ist aber nicht zwingend notwendig, wenn man vor allem den Code unter PHP 7.x ans Laufen bekommen möchte.

preg_replace /e Modifier

Sowohl in Exakat, als auch im Error-Log (bei display_errors=1 findet man es auch direkt auf der Webseite) fand ich dann noch folgenden Fehler:

Warning: preg_replace(): The /e modifier is no longer supported, use preg_replace_callback instead in xxxxxxx.php on line 765

Der Modifikator „e“ steht in diesem Fall für „eval“, und er wurde aus gutem Grund entfernt. Wenn man bei einem Replace-Match eine Funktion ausführen möchte, muss nun preg_replace_callback() genutzt werden. /e ist seit PHP 5.5 deprecated, seit PHP 7.0 gibt es eine Warnung und es wird es nicht mehr ausgeführt.

Beispiel: Aus

$return = preg_replace('#%u([0-9A-F]{1,4})#ie', "convert_u2cs(hexdec('\\1'), \$conf['template_cs'])", $text);

wird

$return = preg_replace_callback('#%u([0-9A-F]{1,4})#i', function ($m) use ($conf) { return convert_u2cs(hexdec($m[1]), $conf['template_cs']);}, $text);

Declaration of xxx should be compatible with yyy

PHP Warning: Declaration of Example::do($a, $b, $c) should be compatible with ParentOfExample::do($c = null) in Example.php on line 2548

Diese Meldung gibt es seit PHP 5.2, früher war es eine E_STRICT Meldung. Da es seit PHP 7 kein E_STRICT mehr gibt (siehe Migration-Guide und RFC), wird nun diese Warnung generiert. Ich habe insgesamt 8 dieser Fehler in beiden Projekten behoben, ich wollte sie nicht einfach im Error-Log stehen lassen, denn wenn bei jedem Aufruf der Seite 3-5 von diesen Warnungen weggeloggt werden, dann wird das Error-Log ziemlich schnell ziemlich groß, und auch Errors wegloggen kostet Performance. Außerdem ist diese Warnung vielleicht auch ein Hinweis auf zukünftige PHP-Änderungen, die dann zu Bugs führen könnten, oder die Applikation z.B. mit PHP 8 gar nicht mehr läuft. Also lieber beheben. In fast allen Fällen waren es fehlende Default-Werte, oder ein zusätzlicher Parameter mit Default-Wert, der nur an einer Stelle (Kind- oder Elternklasse) hinzugefügt wurde. An einer Stelle war auch die Sichtbarkeit eine unterschiedliche (public vs. protected). Es hat länger gedauert, diese Sätze hier zu schreiben, als die 8 Fehler zu beheben 🙂

Fazit

Exakat bietet noch eine Menge mehr, und es ist wirklich ein super Tool, um ein Projekt zu analysieren, einen Überblick zu bekommen über die Code-Qualität, (mögliche) Sicherheitsprobleme und Kompatibilitätsprobleme mit aktuellen und zukünftigen PHP-Versionen aufgelistet zu bekommen, und mehr oder weniger interessante Statistiken (Lines of Code, wird mehr echo oder print genutzt, array() oder [], usw.).

Sich durch die Liste der Analyzers durchzuklicken macht auch Spass, und mit hoher Wahrscheinlich lernt man neues kennen. Code-Smells, Do’s and Don’ts, geplante Deprecations in PHP 7.4, und wahrscheinlich diverse Changes zwischen PHP-Versionen, die man gar nicht mitbekommen hat, oder schon wieder vergessen hat 🙂

Ein größeres Projekt von PHP 5 auf PHP 7 zu bringen kann einige Dutzend Stunden dauern, je nachdem ob es ein älteres, eher ungepflegtes Projekt ist, oder ein Projekt, an dem fast täglich gearbeitet wird, und wo die Entwickler bereits in den letzten Jahren auf die Vermeidung von Deprecated Funktions geachtet haben. Wenn man Tests hat, die einem Rückendeckung bei Codeänderungen geben, kann eigentlich nicht viel schief gehen, aber nicht jedes Projekt ist mit diesem Luxus gesegnet.

So oder so: Es lohnt sich, der Performance-Boost von PHP 7 macht die Anstrengungen wieder wett, außerdem wurde PHP 5.6 nur noch bis Ende 2018 mit Sicherheitsupdates versorgt, es ist also höchste Zeit! Holt euch die Migrationsaufträge eurer Chefs/Kunden, holt euch die Tools, nehmt euch genug Zeit, und dann ran ans Werk!

(An diesem Artikel habe ich über 1 Jahr geschrieben. Da gerade das Thema „PHP 5.6 Abschaltung“ wieder aufpoppte, weil DomainFactory alte PHP-Versionen richtigerweise endlich abschalten will, fiel mir ein, dass ich noch diesen Artikel vervollständigen und veröffentlichen wollte. Er ist also nicht unbedingt brandaktuell, aber alle Hinweise und Tools lassen sich auch auf andere und zukünftige Versionssprünge anwenden)

Written by Michael Kliewe

September 19th, 2019 at 11:56 pm

Leave a Reply

You can add images to your comment by clicking here.