PHP-Project-Checker

PHP-Project-Checker-Tutorial von Daniel Schwamm (29.01.2011 - 12.06.2011)

Inhalt

1. Eine Entwicklungsumgebung für PHP?

1.1. Von Delphi zu PHP

Wie aus meinen Tutorien ersichtlich bin ich begeisterter Delphi-Programmierer. Aus beruflichen Gründen arbeite ich inzwischen allerdings mehr mit PHP. Diese Script-Sprache ist auch nicht übel. Ziemlich umfangreich. Und verflixt schnell, insbesondere für eine Interpreter-Sprache.

PHP-Project-Checker - PHP-Logo

1.2. Mein PHP-Project-Checker

Vermisst habe ich bei PHP jedoch stets eine ähnlich leistungsstarke Entwicklungsumgebung, wie sie Delphi bietet. Und um diesem Faktum entgegenzuwirken, zimmerte ich mir meine eigene Programmieroberfläche zusammen - den PHP-Project-Checker (PPC).

PHP-Project-Checker - PHP-Entwicklungsumgebung

"PHP-Project-Checker": Eine kleine Programmierumgebung für PHP-Projekte mit recht umfangreichen Syntax-Check, integriertem Webbrowser, Web-Seiten-Validierung und Such-Optionen über alle Projekt-Dateien hinweg.

1.2.1. Einige Features des PHP-Project-Checkers

  • Einsatz in der populären XAMPP-Umgebung möglich
  • Erlaubt die Verwaltung vollständiger PHP-Projekten
  • Besitzt einen integrierter Editor mit Zeilenanzeige
  • Bietet einen integrierter Webbrowser, zur direkten Anzeige der Ergebnisse
  • Erlaubt einen Syntax-Check über aller Projekt-Dateien ("Lint")
  • Erkennt undefinierte Funktionen
  • Erkennt unbenutzte Funktionen
  • Erkennt undefinierte Variablen
  • Erkennt unbenutzte Variablen
  • Erkennt Funktionsaufrufe mit falscher Parameter-Anzahl
  • Ermöglicht die Suche nach Texten über alle Projekt-Dateien hinweg
  • Bietet einen direkten Aufruf der phpMyAdmin-Oberfläche an
  • Bietet direkte Aufrufe zur W3C Markup Validation, W3C CSS Validation und JavaScript-Validation
  • Bietet verschiedene Fenstergrössen, um Web-Pages bei wechselnden Auflösungen betrachten zu können
  • Der Delphi-Source ist komplett freigegeben und erweiterbar

2. Verwendung des PHP-Project-Checkers

2.1. XAMPP & Co.

PPC benötigt eine PHP-Umgebung wie z.B. XAMPP (Paket mit Apache, MySQL, PHP und Perl), um funktionieren zu können. Wichtig ist, dass sich PHP-Scripts über einen lokalen (Apache-)Webserver aufrufen lassen.

PHP-Project-Checker - XAMPP

2.2. PHP-Project-Checker einrichten

Einige kleine Voreinstellungen sind nötig, um PPC einsetzen zu können.

2.2.1. Apache-Config anpassen

Standardmässig verortet PPC seine PHP-Projekte im Ordner "HOMEDIR/projects", wobei "HOMEDIR" der Arbeitsordner von PPC ist. Um dort PHP-Scripts ausführen zu können, muss die "http.conf" des Apaches entsprechend angepasst werden, z.B. so:

00001
00002
00003
00004
00005
00006
[...]
<IfModule alias_module>
  Alias /projects c:/programme/phpprojectchecker/projects
  [...]
</IfModule>
[...]

2.2.2. Basis-URL für Web-Projekte im PHP-Project-Checker eintragen

Nach dem Start von PPC muss einmalig die Basis-URL der PPC-Projekte eingetragen werden. PPC merkt sich die URL in seiner INI-Datei. Mehr ist zur Einrichtung nicht erforderlich.

PHP-Project-Checker - Basis-URL für PPC-Projekte

Basis-URL für PPC-Projekte: Angabe der URL, unterhalb der die PPC-Projekte abgelegt sind.

2.3. Neues Projekt beginnen

Über den Menü-Punkt "File|Project new" wird ein neues Projekt begonnen. Es öffnet sich ein Datei-Dialog, über den in den gewünschten Ordner navigiert wird. Man gibt einen Projekt-Namen an und drückt speichern. Anschliessend werden dem Projekt eventuell bereits bestehende Dateien hinzugefügt (über den "+"-Button), oder aber neue Dateien generiert.

PHP-Project-Checker - Bestehende Dateien zum Projekt hinzufügen

Bestehende Dateien zum Projekt hinzufügen: Die Datei "service.php" wird dem Project "xxx.ppc" hinzugefügt. "global.php", "index.php" und "service.php" sind bereits in die Projekt-Liste aufgenommen worden.

2.4. Ausführung

Die Projekt-URL muss eventuell um den/die Unterordner ergänzt werden, in denen sich die Script-Dateien befinden.

Über Radioboxen kann angegeben werden, ob die gerade geladene Editor-Datei gestartet werden soll, oder eine "zentrale" PHP-Datei wie z.B. "index.php", der man zusätzliche CGI-Parameter zuordnen kann, die bei Aufruf automatisch an den Projekt-URL-Pfad angehängt werden.

Um die Web-Site zu starten, genügt ein Druck auf die F9-Taste bzw. der Aufruf des Menüpunkts "Start|Run". Dadurch wird die Projekt-URL an den integrierten Webbrowser übergeben und die Seite letztlich angezeigt.

PHP-Project-Checker - Zentral-Script mit CGI-Parametern

Zentral-Script mit CGI-Parametern: Bei diesem Projekt wird über F9 das Zentral-Script "index.php" mit CGI-Parameter aufgerufen. Die vollständige URL lautet in diesem Fall: "http://localhost/projects/test/index.php?id=1234"

2.4.1. Laufzeit-Fehler

Fehler in PHP-Scripts lassen sich häufig erst zur Laufzeit erkennen. Der PHP-Parser des Web-Servers wirft in diesem Fall eine Fehlermeldung aus, die - für alle sichtbar - auf der Webseite ausgegeben wird, wie im folgenden Beispiel zu sehen ist:

PHP-Project-Checker - PHP-Parser-Error

Der PHP-Project-Checker erkennt solche Laufzeitfehler. Wird der integrierte Web-Browser verlassen, lassen sich alle PHP-Parser-Fehler in der Debug-Liste finden. Ein Doppel-Klick auf einen solchen Eintrag führt direkt zur ermittelten Fehlerstelle im Quellcode.

PHP-Project-Checker - Sprung zum PHP-Parser-Error

2.5. Syntax-Check

Der PHP-Project-Checker verfügt über recht mächtige Syntax-Check-Funktionen. Diese erlauben es, Fehler in den Scripts schon vor der Ausführung zu finden. Das hat den Vorteil, dass dabei auch Stellen im Projekt geprüft werden, die zur Laufzeit nur selten abgerufen werden oder schwierig abzurufen sind (wodurch sich dort Fehler unter Umständen sehr lange vor ihrer Entdeckung "verbergen" können).

Der Syntax-Check des PPC wird durch STRG+F9 gestartet bzw. durch Aufruf des Menüpunkts "Start|Syntax-Check". Es öffnet sich daraufhin ein Dialog, der die Einstellung diverser Optionen erlaubt, die die Prüfroutine beeinflussen.

PHP-Project-Checker - Syntax-Check-Dialog

Syntax-Check-Dialog: Der Syntax-Check eines Projekts kann über verschiedene Optionen gesteuert werden. Für den PHP-Lint-Check ist es notwendig, dass zuvor der vollständige Pfad auf die "php.exe" der XAMPP-Umgebung angeben wird.

Je nach Grösse des Projekts kann der Syntax-Check durchaus einige Minuten lang dauern. Insbesondere der PHP-Lint-Check benötigt relativ viel Zeit, weshalb es sich empfiehlt, ihn nur in Einzelfällen zu aktivieren. Meistens dürfte der Syntax-Check dadurch innerhalb weniger Sekunden erfolgt sein.

Die gefundenen Warnungen und Fehler werden anschliessend in der Debug-Liste aufgeführt. Wie bei den gefundenen Laufzeitfehlern (siehe weiter vorne) bewirkt ein Doppel-Klick auf einen solchen Eintrag eine direkte Navigation zur Problemstelle im Quellcode.

PHP-Project-Checker - Syntax-Check-Meldungen

Syntax-Check-Meldungen: PPC hat einen falschen Funktionsaufruf gefunden, mehrere unbenutzte Variablen, eine unbekannte Funktion und eine unbenutzte Funktion.

2.6. Suche über alle Projekt-Dateien

Die Suche nach Textstellen über alle Projekt-Dateien hinweg wird aktiviert über die Taste F4 bzw. den Menüpunkt "Edit|Search Project". Die letzten 20 Suchbegriffe merkt sich der PPC in seiner INI-Datei.

PHP-Project-Checker - Suche-Dialog

Suche-Dialog: PPC hat den Suchbegriff "function" insgesamt viermal gefunden, nämlich zweimal in der Datei "html.php" und zweimal in der Datei "index.php".

3. Kurzer Anriss der Realisierung des PPC-Syntax-Checks

Wie oben beschrieben, wird der Syntax-Check durch Druck von Taste F9 bzw. Aufruf des Menüpunkts "Start|Syntax Check" und anschliessender Bestätigung des Syntax-Check-Dialogs in Gang gesetzt. Wir sehen uns jetzt einmal an, was danach programmtechnisch exakt passiert.

3.1. Haupt-Schleife

Zu Beginn wird die Prozedur "syntax_check" aus der Unit "syntax_check_u.pas" aufgerufen. Sie initialisiert eine Reihe von Stringlisten und durchläuft danach in einer Schleife alle Dateien eines Projekts, um sie auf ihre Syntax hin zu analysieren. Dabei werden die Stringlisten gefüllt und am Schluss ausgewertet. Die Ergebnisse dieser Auswertung landen zuletzt in der Debug-Liste der Haupt-Form.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
procedure tsyntax_check_f.syntax_check;
var
  funcs_all_sl,funcs_project_sl,
  funcs_unused_sl,funcs_unknown_sl,
  func_calls_sl:tstringlist;
  fn,long_fn,php_src,full_src,err,func_name:string;
  r,x,y:integer;
  prj_c:integer;
begin
  screen.cursor:=crhourglass;
  funcs_all_sl:=tstringlist.Create;
  funcs_project_sl:=tstringlist.Create;
  funcs_unused_sl:=tstringlist.Create;
  funcs_unknown_sl:=tstringlist.Create;
  func_calls_sl:=tstringlist.Create;
  try
    //save project an editor
    main_f.debug_lb.clear;
    main_f.src_save;
    main_f.project_save(main_f.project_fn);

    //scan all functions of all project files
    screen.cursor:=crhourglass;
    prj_c:=main_f.project_lb.items.count;
    for r:=0 to prj_c-1 do
    begin
      fn:=main_f.project_lb.items[r];
      long_fn:=main_f.long_fn(main_f.project_lb.items[r]);
      main_f.status_set(
        'Syntax-Check: ['+inttostr(r+1)+'/'+inttostr(prj_c)+'] '+
        'Scan functions of '+long_fn+' ...'
      );
      application.ProcessMessages;
      //scan and add all functions-headers to funcs_project_sl
      fn_funcs_sl_add(long_fn,funcs_project_sl);
    end;

    //assign result to project-functions-listbox
    main_f.project_functions_lb.items.assign(funcs_project_sl);
    main_f.lb_scrollwidth_set(main_f.project_functions_lb);

    //assign project-functions to unused-function-listbox
    //kill later found used functions from this stringlist
    funcs_unused_sl.assign(funcs_project_sl);

    //all possible functions: add php-functions to project-functions
    funcs_all_sl.assign(funcs_project_sl);
    for r:=0 to main_f.php_functions_lb.items.count-1 do
    begin
      funcs_all_sl.add(main_f.php_functions_lb.items[r]+' - PHP');
    end;

    //check syntax, functions and variables of all project-files
    for r:=0 to prj_c-1 do
    begin
      screen.cursor:=crhourglass;
      fn:=main_f.project_lb.items[r];
      long_fn:=main_f.long_fn(main_f.project_lb.items[r]);
      main_f.status_set(
        'Syntax-Check: ['+inttostr(r+1)+'/'+inttostr(prj_c)+'] '+
        'Check '+long_fn+' ...'
      );
      application.ProcessMessages;

      //php-lint-syntax-check
      if lint_chb.Checked then
        syntax_check_php_lint_u.php_lint(long_fn);

      //no more to do?
      if not(
        functions_unknown_chb.checked or
        functions_unused_chb.checked or
        variables_unknown_chb.checked or
        variables_unused_chb.checked
      ) then continue;

      //isolate php-parts from source
      php_src:=php_isolate_fn(long_fn,full_src,err);

      //check functions
      func_calls_sl.clear;
      syntax_check_funcs_u.check(
        fn,full_src,php_src,
        funcs_all_sl,
        funcs_project_sl,
        funcs_unused_sl,
        funcs_unknown_sl,
        func_calls_sl
      );

      //check variables
      syntax_check_vars_u.check(
        fn,
        full_src,
        php_src,funcs_all_sl,
        func_calls_sl
      );
    end;

    //add unknown functions to debug-listbox
    if functions_unknown_chb.Checked then
    begin
      for r:=0 to funcs_unknown_sl.Count-1 do
      begin
        fn:=trim(main_f.delim_param_get(funcs_unknown_sl[r],0,'|','0'));
        y:=strtoint(trim(main_f.delim_param_get(funcs_unknown_sl[r],1,'|','0')));
        x:=strtoint(trim(main_f.delim_param_get(funcs_unknown_sl[r],2,'|','0')));
        func_name:=trim(main_f.delim_param_get(funcs_unknown_sl[r],3,'|',''));
        main_f.debug_add(_db_warning,fn,y,x,func_name,'','UNKNOWN function?');
      end;
    end;

    //add unused functions to debug-listbox
    if functions_unused_chb.Checked then
    begin
      for r:=0 to funcs_unused_sl.Count-1 do
      begin
        func_name:=trim(main_f.delim_param_get(funcs_unused_sl[r],0,'-',''));
        fn:=trim(main_f.delim_param_get(funcs_unused_sl[r],1,'-',''));
        main_f.debug_add(_db_warning,fn,0,0,func_name,'','UNUSED function?');
      end;
    end;
    main_f.status_set(
      'Syntax-Check: ['+inttostr(prj_c)+'/'+inttostr(prj_c)+'] '+
      'Okay, all files of project checked.'
    );
  finally
    func_calls_sl.free;
    funcs_all_sl.Free;
    funcs_unknown_sl.Free;
    funcs_unused_sl.Free;
    funcs_project_sl.Free;
    main_f.lb_scrollwidth_set(main_f.debug_lb);
    screen.cursor:=crdefault;
  end;
end;

3.2. Einsatz von Stringlisten

Zuerst werden mit dem PHP-Project-Checker folgende String-Listen erzeugt:

  • funcs_all_sl : Enthält alle PHP- and Projekt-Funktionen.
    Format je Eintrag: function - filename
  • funcs_project_sl: Enthält alle Projekt-Funktionen.
    Format je Eintrag: function - filename
  • funcs_unused_sl : Enthält alle unbenutzten Projekt-Funktionen.
    Format je Eintrag: function - filename
  • funcs_unknown_sl: Enthält alle unbekannten Projekt-Funktionen.
    Format je Eintrag: filename|y|x|function
  • func_calls_sl : Enthält alle Funktionsaufrufe einer PHP-Datei.
    Format je Eintrag: y|x|function

3.3. Funktionssammlung

In der ListBox "main_f.project_lb" der Haupt-Form sind alle Dateinamen der Projekt-Dateien aufgeführt. Diese werden in einer Schleife einzeln abgerufen und an die Funktion "fn_funcs_sl_add" übergeben. Dabei wird die Stringliste "funcs_project_sl" gefüllt, die danach alle Funktionen des Projekts enthält, jeweils mit ihren Parametern und ihrer Quelldatei. Das sieht dann etwa so aus:

00001
00002
00003
00004
html_content_get() - html.php
html_get() - html.php
tst_xxx($a) - index.php
tst($a) - index.php

Die Stringliste "funcs_unused_sl" übernimmt die Werte von "funcs_project_sl". Wird bei der weiteren Analyse der Aufruf einer Funktion gefunden, so wird diese aus "funcs_unused_sl" entfernt. Die Funktionen, die bis zuletzt übrig bleiben, werden im Projekt offenbar nicht benutzt.

3.4. Berücksichtigung der PHP-eigenen Funktionen

Auch die Stringliste "funcs_all_sl" übernimmt die Werte von "funcs_project_sl". In einer Schleife werden darüber hinaus sämtliche PHP-Funktionen ergänzt, die in der ListBox "main_f.php_functions_lb" fix vorgegeben sind. Da diese "eingebauten" PHP-Funktionen keiner bestimmten PHP-Datei zugeordnet werden können, bekommen sie als Dateiangabe einfach nur die Kennung "PHP" zugewiesen.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
html_content_get() - html.php
html_get() - html.php
tst_xxx($a) - index.php
tst($a) - index.php
abs - PHP
acos - PHP
acosh - PHP
addcslashes - PHP
addslashes - PHP
and - PHP
array - PHP
array_change_key_case - PHP
array_chunk - PHP
[...]
zend_thread_id - PHP
zend_version - PHP

Zuletzt stehen in der Stringliste "funcs_all_sl" alle verwendbaren, weil definierten Funktionen eines Projekts. Wird daher bei der weiteren Analyse des PHP-Quellcodes ein Funktionsaufruf gefunden, der in dieser Liste nicht aufgeführt ist, dann handelt es sich offenbar um eine undefinierte, sprich unbekannte Funktion.

3.5. Parsing alle Projektdateien mit "Lint"

Nun werden in einer weiteren Schleife erneut alle Projekt-Dateien durchlaufen. Ist die PHP-Lint-Option gesetzt, werden die Dateinamen an die Funktion "syntax_check_php_lint_u.php_lint" übergeben. Hier erfolgt der Syntax-Check der Datei mittels der "php.exe" der XAMPP-Umgebung. Gefundene Fehler werden in der Debug-Liste der Haupt-Form eingetragen.

3.6. PHP-Code ausfiltern

Anschliessend wird der Quellcode der Datei über die Funktion "php_isolate_fn" derart verarbeitet, dass dort alle Bestandteile herausgefiltert werden, die sich nicht innerhalb eines PHP-Start-Tags ("<?") und PHP-End-Tags ("?>") befinden. Und auch der übrig gebliebene PHP-Teil wird durch "Ausmarkierung" von z.B. String-Inhalten noch weiter dahin gehend modifiziert, dass er einer leichteren (Syntax-)Analyse zugänglich wird. Ein Beispiel mag dies verdeutlichen:

Aus der PHP-Datei ...

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
HTML-Text
<?php
include '/lib/globals.inc';
include '/lib/string.inc';

function tst($a,$b){echo 'tst:test';};

echo 'Test';
?>
und noch mehr HTML-Text

... wird:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
.........
<?php
include .................';
include ................';

function tst($a,$b){echo .........';};

echo .....';
..
... .... .... .........

Wie man sieht, wird der Original-Code quasi auf die PHP-Funktionsdefinitionen, die PHP-Funktionsaufrufe und die PHP-Variablen reduziert, alles andere wird durch Punkte überschrieben. Das Parsen dieses Codes mittels der Funktionen "syntax_check_funcs_u.check" und "syntax_check_vars_u.check" nach Syntax-Fehlern ist natürlich ungleich einfacher, als das Parsen des kompletten Original-Codes.

3.7. Abgleich mit Funktionslisten

Nachdem alle Projekt-Dateien analysiert wurden, wird die Stringliste "funcs_unknown_sl", also die Liste alle unbekannten Funktionen, durchlaufen und alle Einträge darin an die "main_f.debug_add"-ListBox übergeben.

Das gleiche Verfahren wird anschliessend auch auf die Stringliste "funcs_unused_sl" angewendet, also auf die Liste aller unbenutzten Funktionen.

Jetzt müssen nur noch die verwendeten Stringlisten aus dem Speicher geräumt werden und der Syntax-Check des Projekts ist abgeschlossen.

4. Fazit

"Pre-Debugger" für PHP-Projekte findet man im Web nicht. Nun ja, zumindest ich nicht. Vermutlich geht es also anderen ähnlich. Wenn diese Suchenden es aber bis hierher geschafft haben und damit wohl zu jenen gehören, die in PHP programmieren und sich besser fühlen würden, wenn sie ihre Projekte hin und wieder "General-Überprüfen" lassen können, dann mögen sie sich gerne meinen PHP-Project-Checker downloaden.

Klar, das Programm ist recht spartanisch. So wäre es z.B. sinnvoll, bei der Prüfung auf "unnötige Funktionen" bestimmte Ordner ausschliessen zu können. Bindet man nämlich sonst umfangreichen PHP-Bibliotheken in ein Projekt ein, verwendet von dort aber wie üblich nur einen Bruchteil des Funktionsumfangs, dann werden bei jedem Syntax-Check eventuell Hunderte von "Warnings" wegen unbenutzter Funktionen generiert.

So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts Hundertprozentiges. But who cares?

5. Download

"PHP-Project-Checker" wurde mit Delphi 7 programmiert. Der komplette Source und die ausführbare EXE sind alle in diesem ZIP-Archiv verpackt:

PHP-Project-Checker.zip (ca. 120 kB)