Debugging in PHP

Debuggen von PHP-Applikationen war für mich in den letzten 14 Jahren stets das Buch mit den sieben Siegeln. Auch kenne ich zahlreiche PHP Entwickler denen es damit genau so geht. Dass PHP nunmal als Interpretersprache nicht von Haus aus Debugging mit der IDE bereitstellt, hat es der Sache daher nie leicht gemacht. Aus diesem Grund habe ich mich in den letzten Jahren mit Debugging nie beschäftigt. Ich tat dies immer wieder als komplexes Hexenwerk ab und knüppelte es als unausgereift nieder. Damit begab ich mich – nur um mich selbst zu beschönigen – stets mit File-Logging und echo bzw. var_dump()-Ausgaben zufrieden.

Jedoch sind die gewonnen Erkenntnisse einer Debugsession unermesslich, da man zur Laufzeit exakt sehen kann welche Werte eine Variable in bestimmten Situationen beinhaltet. Ich habe seit Einrichtung dessen viele Stunden an Mühen mir ersparen können, da die erkannten Abläufe in einer Debugsession deutlich früher zu verwertbaren Resultaten führen.

Das kann nun ein Ende auch für Dich haben, denn die Einrichtung mittels X-Debug und der IDE PhpStorm ist denkbar einfach.

Grundsätzliches

Die hier vorgestellte Konfiguration betrifft erst einmal PhpStorm, jedoch kann ich mir vorstellen, dass diese unter Eclipse sowie den anderen gängigen IDEs mit nur minimal abweichenden Konfigurationseinstellungen vorzunehmen sein sollte. Denn die hier zugrunde liegenden Grundeinstellungen und Konzepte sind für alle die gleichen.

Für die Einrichtung und der Benutzung des Debuggers werde ich zum einen für den Abruf im Firefox-Browser, des weiteren auf der Linux-Konsole sowie abschließend im Rest Client – welcher in PhpStorm als Plugin installiert werden kann – beschrieben. X-Debug wurde als Modul auf einem Linux Server installiert und aktiviert und ich kann die Konfiguration nun in folgende Punkte zusammenfassen:

Servereinrichtung

Um X-Debug auf dem Server einzurichten bedarf es lediglich der Modulaktivierung in der php.ini:

zend_extension = /opt/remi/php73/root/usr/lib64/php/modules/xdebug.so
xdebug.remote_enable=1
xdebug.remote_autostart=0
xdebug.remote_handler=dbgp
;xdebug.remote_host=192.168.1.10 <-IGNORE THIS+USE remote_connect_back
xdebug.remote_connect_back=1
xdebug.remote_port=9000
xdebug.idekey=PHPSTORM
xdebug.remote_log=/tmp/xdebug.log

Durch Aktivierung der Option remote_connect_back wird automatisch IP-Adresse des anfragenden Browsers verbunden. Dies funktioniert in den meisten Fällen wunderbar und kann problemlos in lokalen Netzwerken aktiviert werden.

Einrichtung in PhpStorm

  • Settings öffnen und zu Debugger wechseln
    • XDEBUG PHP-IDE Key setzen: PHPSTORM
    • Port 9000 und 9001 wählen
    • Force break zunächst aktiviert lassen, jedoch im täglich Gebrauch deaktivieren. Man will ja nicht ernsthaft, dass bei jedem Abruf der Debugger in der ersten auszuführenden Zeile anhält
  • Debugger einrichten (Run/Debug Configurations)
    • Neue „PHP Web Application“
      • Neuen Server eintragen: URL und Port 80
      • Start URL: „/“
      • Pathmapping aktivieren
        • Hauptverzeichnis mappen mit dem jeweiligen serverseitigen Pfad
        • Gegebenfalls Unterverzeichnis auf weiteren Pfaden mappen.
      • Es wird eine Einstellung für http und https benötigt, falls https nicht erstellt wurde, macht dies PHP-Storm eigenmächtig, muss jedoch dann auch wie o.g. konfiguriert werden
    • Neue „PHP Remote Debug“
      • Servers hinzufügen („…“ wählen) und neuen Server hinzufügen
        • Servername setzen
        • Hostname setzen
        • Port kann auf 80 gesetzt werden
        • Pathmapping wie o.g. aktivieren
      • Den IDE-Key belegen mit „PHPSTORM“
    • Im Debugger springt PhpStorm in den Standardeinstellungen beim Start einer neuen Debugsitzung immer wieder in die Console. Mit Rechtsklick auf den Tab die Einstellung „Focus on startup“ abwählenPHPStorm

Für den ersten Debug muss entweder ein beliebiger Breakpoint gesetzt werden bei dem der Debugger anhält. Alternativ kann im Menü -> Run die Option “Break at first line in PHP scripts” gesetzt werden. Dies jedoch ist für den alltäglichen Gebrauch nicht geeignet, außer man weiß zu Beginn rein gar nicht wohin der Abruf geroutet wird oder man wissen möchte welche Stellen noch relevant sind für die Ausführung sind.

REST Client (PhpStorm)

  • Header setzen
    • Accept: */*
    • Cache-Control: no-cache
  • Request Parameters XDEBUG_SESSION_START hinzufügen
    • Der Inhalt dieser Variable kann beliebig gesetzt werden und sogar leer bleiben
    • Dieser Wert wird serverseitig zu einem Cookie
  • Request zusammensetzen
    • Methode setzen (GET, PUT, POST, HEAD, DELETE)
    • Host setzen
    • Path mit dem Pfad und dem Zielskript befüllen
  • Debugger starten (Icon mit Telefonhörer)
  • Request absetzen

Firefox Browser

  • Plugin “The easiest Xdebug” installieren
  • IDE-Key eintragen: netbeans-xdebug
  • Debugging durch Seitenabruf initiieren

Einrichtung im Chrome

  • Die App “Header Hacker” über den Chrome Webstore suchen und installieren
  • Unter “Custom Request Headers” einen Eintrag einstellen:
    • Title: X-Debug
    • Header: Cookie
    • Append/&Replace: Append
    • String: XDEBUG_SESSION=PHPSTORM
    • Match String: #LEER LASSEN#
  • Unter „Permanent Header Switches“ ebenfalls einen neuen Eintrag einstellen:
    • Domain: Domain eintragen unter welcher der Header mitgesendet werden soll – z.B. http://debug.moderlak.de
    • Header Switch: Auswählen des oben eingetragenen Wert
  • Nachteil: Der Header muss immer manuell pro Domain konfiguriert werden und er wird immer mitgesendet. Daher schaltet man am besten den Debugger in der IDE ein und aus.

CLI Debugging

Die .bashrc wie folgt konfigurieren

Einrichtung der .bash.rc

Festlegen der CLIENTIP um die Verbindung zur lokalen IDE zu definieren und einen Alias um PHP-Skripte mit den umfassenden Parametern zu starten:

CLIENTIP=$(w|egrep -o "$USER\W*pts/([0-9]{1,3})\W*(([0-9]{1,3}).){3}.([0-9]{1,3}).*" | head -n1 | awk "{print \$3}")
# Alternative wenn anstelle der IP-Adresse der Computername im Netzwerk angezeigt wird - dies funktioniert zuverlässig wenn nur ein Benutzer mit dem Server per SSH verbunden ist:
# CLIENTIP=$(netstat -tn 2>/dev/null | grep :22 | awk '{print $5}' | grep -v :22 | cut -d: -f1 | sort | uniq | sort -nr | head)

alias xphp="XDEBUG_CONFIG=\"idekey=PHPSTORM\" PHP_IDE_CONFIG=serverName=\"$HOSTNAME(CLI)\" php -d \"xdebug.remote_host=$CLIENTIP\" -d \"xdebug.remote_connect_back=0\" -d \"xdebug.remote_port=9000\" -d \"xdebug e_enable=1\""

CLI Debugging lokal unter Windows

Inzwischen wurde ich auch dazu genötigt unter Windows zu debuggen, welches jedoch nicht im Browser, sondern nur auf der Konsole ausgeführt wird. Dies einzurichten und zu nutzen ist allerdings recht einfach und bedarf nur weniger Schritte der Konfiguration.

Hierfür muss zunächst die Extension heruntergeladen und in das Unterverzeichnis von php unter “ext” abgelegt werden. Die DLL erhält man von https://xdebug.org/ aus der Sektion Download.

In der php.ini wird die Extension als “zend_extension” aktiviert mit dem Namen der geladenen DLL-Datei.

Dann legt man lokal für den eigenen Benutzer eine neue Umgebungsvariable wie folgt an:

Name: XDEBUG_CONFIG
Wert: remote_enable=1 remote_mode=req idekey=PHPSTORM

Abschließend wird nun in dem zu debuggenden Skript in der IDE ein Breakpoint gesetzt und das Skript einfach via php aufgerufen. Es sind keine weiteren Parameter notwendig.

Troubleshooting

Jedes beliebige PHP-Skript kann nun mit xphp myscript.php anstelle von php myscript.php gestartet werden. Dabei werden die notwendigen Header dem Skriptabruf angehangen, die das Debugging initiieren und man somit durch den Code navigieren kann.

Wenn es mal nicht so läuft wie gewünscht habe ich mal die typischen Fehlermeldungen zusammengetragen:

  • Nach der Formularabsendung blockiert der Browser und zeigt nur noch den Hinweis: “Fehler: Gesicherte Verbindung fehlgeschlagen“
    • Problem: Debugging im PhpStorm aktiviert und Cookie XDEBUG_SESSION wird vom Browser gesendet. Dies passiert mir besonders häufig mit dem Plugin “The easiest Xdebug”. Mit einem Klick auf das Icon um den Cookie ein/auszuschalten ist nicht ausreichend, da der Cookie nicht mehr entfernt wird.
    • Lösung: Den Listener für Debugging im PhpStorm abschalten und den Cookie XDEBUG_SESSION im Browser explizit entfernen und Seite erneut abrufen.
  • CLI-Modus: PHPStorm antwortet nicht auf Debugrequest obwohl alle Daten korrekt eingegeben
    • Problem: Die lokale IP-Adresse des Clients im xphp-Aufruf unter xdebug.remote_host ist veraltet
    • Lösung: bashrc neuladen mittels: source ~/.bash.rc 

Diese Liste der Fehler wird im Laufe der Zeit gegebenenfalls erweitert.

Fazit

Mein Entwicklerdasein hat sich seit dieser Einführung um 180 Grad gewandelt, denn Probleme kann ich seither viel früher erkennen. Beim Durchlauf des Programms werden Erkenntnisse zu Tage gefördert, welche beim sonst üblichen Komplettdurchlauf nur selten sofort ersichtlich sind.
Auch lassen sich mögliche Fehler hiermit sehr schnell aufspüren und exakt analysieren.

Releasewechsel in PHP

Offensichtlich stellt die Auslieferung ein neuen Releases in einigen PHP-Projekten einen hohen manuellen Aufwand dar, da Dateien über die bestehende Installation mehr oder weniger manuell kopiert werden. Dabei müssen selbstverständlich zu löschende Dateien und Ordner berücksichtigt werden, sowie ist darüber hinaus dieser Vorgang nur vorwärtsgerichtet und kann im Falle eines massiven Fehlers nur zurückgerollt werden sofern eine Komplettsicherung vorliegt.
Zu guter Schluss kann dieser manuelle Vorgang mehrere Minuten bis sogar Stunden in Anspruch nehmen und damit eine längere Downtime der Webseite bzw. des Services einfordern. Je nach Projekt kann dies unerwünscht und in Folge dessen auch noch mit hohen Kosten verbunden sein – zum einen durch Personalkosten sowie zum anderen der Nichtverfügbarkeit und dem damit verbundenen Nutzungsausfall. Wer zur Krönung dann noch eine ganze Farm von installierten Applikationen zu aktualisieren hat wird letztlich seinen Aufwand nochmal um die Anzahl der Hosts multiplizieren dürfen.

In diesem Sinne: Prost Mahlzeit!

Releasewechsel in PHP weiterlesen

isset() Prüfung schlägt fehl

Die Prüfung auf definierte Schlüssel in einem PHP-Array kann wie folgt vorgenommen werden:

$data = array(
    'id' => 4711,
    'value' => NULL
);

if( isset( $data['id'] ) ) {
 echo "id existiert!\n";
}
if( isset( $data['value'] ) ) {
 echo "value existiert!\n";
}

Es kann jedoch vorkommen, dass hinter einem Eintrag der Wert NULL gespeichert ist. Die Funktion isset() wird dann das Vorhandensein des Eintrags dennoch als FALSE werten und damit wird dem Entwickler bzw. dem ursprünglichen Programmfluss weisgemacht, dass der Eintrag nicht vorhanden ist und mit dieser falschen Annahme die Programmausführung fortsetzen und zu unbegreiflichen Ergebnissen führen. isset() Prüfung schlägt fehl weiterlesen

Referenzen in PHP

Wer braucht schon Referenzen in PHP? Wenn man nun alles in PHP elegant via objektorientierter Programmierung erledigt ist diese Frage eigentlich gar kein Thema für den Alltag.

Was aber tun wenn man nun doch damit konfrontiert wird, so stelle ich fest, dass man die Referenzierung sowohl bei der Definition als auch beim Abruf explizit aufzeigen muss. Referenzen in PHP weiterlesen

Back in time

Hin uns wieder steht man als Entwickler vor der Aufgabe einen Prozess zu verschiedenen Zeitpunkten zu testen. Das löst man meist recht pragmatisch indem man die Systemzeit ändert und sodann seine Tests durchführt. Dies hat jedoch oftmals einen unangenehmen Beigeschmack, der sich unter anderem im folgenden bemerkbar macht:

  1. Andere Programme legen ein ungewolltes Verhalten an den Tag
  2. Andere Benutzer auf dem gleichen System sind ebenfalls von  der Zeitumstellung betroffen
  3. Der Taskplaner startet Jobs die während der gegenwärtigen Uhrzeit das System negativ beeinflussen
  4. Zertifikate werden plötzlich ungültig
  5. Logeinträge des Systems sowie jene diverser Applikationen sind möglicherweise nicht mehr zuordnbar bzw. nur schwer nachvollziehbar

Welche Gründe man nun auch immer für die Zeitumstellung hat und sich mit den damit verbundenen Problemen herumschlagen muss – sofern die Entwicklung auf einem Windowssystem stattfindet ist man mit dem Tool RunAsDate in der Lage einen beliebigen Prozess mit einer eigenen Zeiteinstellung gezielt zu starten. Dafür klinkt dieses Tool sich in die Kernel-API ein und manipuliert die Zeit für den Prozess mit der übergebenen Zeiteinstellung.

Einen PHP-Webserver in anderen Zeiten starten:

RunAsDate.exe /movetime 01\01\2001 18:15:00 C:\php5\php.exe -S localhost:8000 -t D:\var\www\mydomain\

PHP im CLI-Modus starten:

RunAsDate.exe /movetime 01\01\2001 18:15:00 C:\php5\php.exe -r “date(‘Y-m-d H:i:s’);fgets( STDIN );”

Beachte:

  1. Manche Zeiteinstellungen z.B. vor 2000 oder ein Datum vor bestimmten Zeitpunkten zu den das Programm einfach seine Kalkulationen unterlässt lassen das gewünschte Programm nicht starten oder zu ungewünschten Ergebnissen führen
  2. Es ist nicht empfehlenswert Programme zu starten, die einer zeitlichen Beschränkung unterliegen, da diese möglicherweise das intern gegenprüfen und so ihren Dienst künftig gänzlich verweigern

Download:

http://www.heise.de/download/runa04sdate-1169076.html

Umstellung mysql auf mysqli

Mit PHP 5.5.0 wurden die alten mysql-Funktionen als deprecated markiert und seit PHP 7.0.0 gänzlich entfernt. Da inzwischen immer mehr Provider ihr Hostingangebote auf PHP 7 aktualisieren stehen diese Funktionen ab dieser Version dann gar nicht mehr zur Verfügung. Daher muss eine Umstellung dieser in den eigenen Projekten zwingend eingeplant werden. Dafür habe ich an dieser Stelle eine umfängliche Schritt-für-Schritt Anleitung zusammengestellt, welche bei sorgfältiger Abarbeitung einen innerhalb kurzer Zeit ziemlich sicher zum Ziel führt.

MySQL: Das umfassende Handbuch

Die Umstellung der mysql_*-Funktionen auf mysqli_*-Funktionen geht relativ reibungslos vonstatten, sofern man eine zentrale Verarbeitung der Datenbankabfragen implementiert hat. Ist dies nicht der Fall so wird sich der hier aufgeführte Aufwand um die Anzahl der zu behandelnden Stellen zuzüglich der notwendigen Tests in ungefähr multiplizieren.

Umsetzung

Für die Umstellung des eigenen Adapters muss man sich den nachfolgenden Punkten in jedem Einzelfall gesondert widmen und kann dazu wie folgt vorgehen:

  1. Im ersten Schritt ersetzt man alle mysql_ Funktionsaufrufe mit einem mysqli_. Das nachfolgende Suchpattern eignet sich hierfür sehr gut:
    /mysql_([a-z_]+)\(/
  2. In der Funktion mysqli_connect() werden ab dem vierten Parameter Port und Socket erwartet anstelle von link und clients. Sofern diese angegeben wurden müssen diese zunächst entfernt werden. Haben diese Angaben bisher darüber hinaus noch Relevanz gehabt so müssen jene an dieser Stelle gesondert behandelt werden.
  3. Die Funktionen mysqli_query()mysqli_error(), mysqli_escape_string() und mysqli_real_escape_string() benötigen nun zusätzlich die Angabe der Ressource. Dies kann für die beiden zuletzt genannten Funktionen an den Stellen problematisch werden, an denen eine Verbindung möglicherweise noch nicht hergestellt wurde oder die Ressource gar nicht erst verfügbar ist. Hier muss sichergestellt sein, dass diese vor dem ersten Aufruf aufgebaut wurde.
  4. In der Funktion mysqli_select_db() sind von nun an die beiden Parameter für Ressource und DB-Name vertauscht.
  5. Die vier Funktionen mysqli_fetch_assoc()mysqli_fetch_row()mysqli_fetch_object() und mysqli_fetch_array() liefern im Fall eines leeren Ergebnisses nun NULL wo ihre mysql-Pendants  hingegen bisher den Wert FALSE zurückgegeben haben.
  6. Andersherum jedoch liefert die Methode mysqli_stat() fortan FALSE anstelle von NULL sofern die Datenermittlung erfolglos stattgefunden hat.
  7. Prüfungen auf is_resource() sind nicht mehr möglich da der mysqli-Adapter keine PHP Ressourcen mehr darstellen sondern interne Klassenrepräsentationen.
    Konstrukte dieser Art:
    if( is_resource( $this->_link ) {     // ... }
    müssen überarbeitet und ersetzt werden gegen folgende Logik:
    if( $this->_link instanceof mysqli ) {     // ... }
  8. Die Funktion mysql_field_name() kann nicht ersetzt werden, da das entsprechende Pendant nicht existiert. Man muss es durch den nachfolgenden Schnipsel, Umsetzung in einer Klasse vorausgesetzt, ergänzen:
    private function field_name( $offset ) {
       $properties = mysqli_fetch_field_direct( $this->result, $offset );
       return is_object( $properties ) ? $properties->name : null; 
    }
    Anstelle von mysql_field_name( $resource, $offset ) erfolgt der Aufruf nun mit $this->field_name( $offset ).
  9. Der Abruf der Methoden mysqli_stat() und mysql_error() ohne Parameter ist nun nicht mehr zulässig und es muss die Ressource künftig mit angegeben werden.

Fazit

Ich stelle fest, dass eine Umstellung auf mysqli keine zeitaufwändige Angelegenheit ist und mit den wenigen notwendigen Tests innerhalb eines halben Arbeitstages nahezu mühelos erledigt werden kann.

Weiterführende Literatur