Skip to content

4. Beispiele

4.1. Steuer-App: Einführung

Wir stellen hier die Anwendung IMPOTS vor, die im weiteren Verlauf noch häufig zum Einsatz kommen wird. Es handelt sich um eine Anwendung zur Berechnung der Steuer eines Steuerpflichtigen. Wir betrachten den vereinfachten Fall eines Steuerpflichtigen, der lediglich sein Gehalt anzugeben hat:

  • Wir berechnen die Anzahl der Anteile des Arbeitnehmers: nbParts = nbEnfants/2 + 1, wenn er unverheiratet ist, nbEnfants/2 + 2, wenn er verheiratet ist, wobei nbEnfants die Anzahl seiner Kinder ist.
  • Hat er mindestens drei Kinder, erhält er einen halben Anteil zusätzlich
  • sein steuerpflichtiges Einkommen R wird berechnet als R = 0,72 * S, wobei S sein Jahresgehalt ist
  • man berechnet seinen Familienkoeffizienten QF = R / nbParts
  • Man berechnet seine Steuer I. Betrachten wir die folgende Tabelle:
12620,0
0
0
13.190
0,05
631
15640
0,1
1290,5
24.740
0,15
2072,5
31.810
0,2
3309,5
39.970
0,25
4900
48360
0,3
6898,5
55.790
0,35
9316,5
92.970
0,4
12106
127.860
0,45
16754,5
151250
0,50
23.147,5
172.040
0,55
30.710
195.000
0,60
39312
0
0,65
49062

Jede Zeile enthält 3 Felder. Um die Steuer I zu berechnen, wird die erste Zeile gesucht, in der QF <= Feld 1 gilt. Wenn beispielsweise QF = 23000 ist, wird die Zeile

    24740        0.15        2072.5

Die Steuer I beträgt dann 0,15*R – 2072,5*nbParts. Wenn QF so ist, dass die Beziehung QF <= Feld1 niemals erfüllt ist, werden die Koeffizienten der letzten Zeile verwendet. Hier:

    0                0.65        49062

was die Steuer I = 0,65 * R – 49062 * nbParts ergibt.

Die Daten, die die verschiedenen Steuerklassen definieren, befinden sich in einer ODBC-MySQL-Datenbank. MySQL ist eine gemeinfreie Version von SGBD, die auf verschiedenen Plattformen, darunter Windows und Linux, verwendet werden kann. Mit diesem SGBD wurde eine Datenbank namens „dbimpots“ angelegt, die eine einzige Tabelle namens „impots“ enthält. Der Zugriff auf die Datenbank wird über einen Benutzernamen und ein Passwort gesteuert, in diesem Fall „admimpots“ und „mdpimpots“. Der folgende Screenshot zeigt, wie die Datenbank „dbimpots“ mit MySQL verwendet wird:


C:\Program Files\EasyPHP\mysql\bin>mysql -u admimpots -p
Enter password: *********
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 18 to server version: 3.23.49-max-nt

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> use dbimpots;
Database changed

mysql> show tables;
+--------------------+
| Tables_in_dbimpots |
+--------------------+
| impots             |
+--------------------+
1 row in set (0.00 sec)

mysql> describe impots;
+---------+--------+------+-----+---------+-------+
| Field   | Type   | Null | Key | Default | Extra |
+---------+--------+------+-----+---------+-------+
| limites | double | YES  |     | NULL    |       |
| coeffR  | double | YES  |     | NULL    |       |
| coeffN  | double | YES  |     | NULL    |       |
+---------+--------+------+-----+---------+-------+
3 rows in set (0.02 sec)

mysql> select * from impots;
+---------+--------+---------+
| limites | coeffR | coeffN  |
+---------+--------+---------+
|   12620 |      0 |       0 |
|   13190 |   0.05 |     631 |
|   15640 |    0.1 |  1290.5 |
|   24740 |   0.15 |  2072.5 |
|   31810 |    0.2 |  3309.5 |
|   39970 |   0.25 |    4900 |
|   48360 |    0.3 |    6898 |
|   55790 |   0.35 |  9316.5 |
|   92970 |    0.4 |   12106 |
|  127860 |   0.45 |   16754 |
|  151250 |    0.5 | 23147.5 |
|  172040 |   0.55 |   30710 |
|  195000 |    0.6 |   39312 |
|       0 |   0.65 |   49062 |
+---------+--------+---------+
14 rows in set (0.00 sec)

mysql>quit

Die Datenbank dbimpots wird wie folgt in die Datenquelle ODBC umgewandelt:

  • Der 32-Bit-Datenquellenmanager ODBC wird gestartet

Image

  • über die Schaltfläche [Add] wird eine neue Datenquelle ODBC hinzugefügt

Image

  • Man wählt den Treiber MySQL aus und führt [Terminer] aus
  • Der Treiber MySQL fordert eine Reihe von Angaben an:
1
den Namen DSN, der der Datenquelle ODBC zugewiesen werden soll – dieser kann beliebig sein
2
den Rechner, auf dem der SGBD MySQL ausgeführt wird – hier localhost. Interessanterweise könnte es sich bei der Datenbank um eine entfernte Datenbank handeln. Lokale Anwendungen, die die Datenquelle ODBC verwenden, würden dies nicht bemerken. Dies wäre insbesondere bei unserer Anwendung PHP der Fall.
3
die zu verwendende Datenbank MySQL. MySQL ist ein SGBD, das relationale Datenbanken verwaltet, bei denen es sich um Gruppen von Tabellen handelt, die durch Beziehungen miteinander verbunden sind. Hier wird der Name der verwalteten Datenbank angegeben.
4
den Namen eines Benutzers, der Zugriffsrechte auf diese Datenbank hat
5
sein Passwort

4.2. Anwendung „Steuern“: die Klasse ImpotsDSN

Unsere Anwendung basiert auf der Klasse PHP ImpotsDSN, die die folgenden Attribute, Konstruktoren und Methoden aufweist:

<?php
...
     // Attribute
    var $limites;        // Grenzwerttabelle
    var $coeffR;        // Tabelle der coeffR
    var $coeffN;        // Tabelle der coeffN
    var $erreurs;        // Fehlertabelle
     // Hersteller
    function ImpotsDSN($impots){...}
     // Methode
    function calculer($personne){...}
  • Wir haben in der Problemstellung gesehen, dass wir drei Datensätze benötigen
12620,0
0
0
13190
0,05
631
15640
0,1
1290,5
24.740
0,15
2072,5
31.810
0,2
3309,5
39.970
0,25
4900
48360
0,3
6898,5
55.790
0,35
9316,5
92.970
0,4
12106
127.860
0,45
16.754,5
151250
0,50
23.147,5
172.040
0,55
30.710
195.000
0,60
39312
0
0,65
49062

Die erste Spalte wird im Attribut $limites der Klasse ImotsDSN abgelegt, die zweite im Attribut $coeffR und die dritte im Attribut $coeffN. Diese drei Attribute werden vom Konstruktor der Klasse initialisiert. Dieser bezieht die Daten aus einer Datenquelle namens ODBC. Beim Erstellen des Objekts können verschiedene Fehler auftreten. Diese Fehler werden im Attribut $erreurs in Form eines Arrays mit Fehlermeldungen gemeldet.

  • Der Konstruktor erhält als Parameter ein Dictionary $impots mit den folgenden Feldern:
// DSN: Name der Datenquelle, die die Werte der Grenzwerttabellen enthält: ODBC, coeffR, coeffN
// user: Name eines Benutzers mit Leserechten für die Datenquelle ODBC
// pwd: sein Passwort
// table: Name der Tabelle, die die Werte enthält (Grenzwerte, coeffR, coeffN)
// Grenzwerte: Name der Spalte, die die Grenzwerte enthält
// coeffR: Name der Spalte, die die Werte coeffR enthält       
// coeffN: Name der Spalte, die die Werte enthält coeffN

Der Konstruktorparameter enthält alle notwendigen Informationen, um die Daten aus der Datenquelle ODBC auszulesen

  • Sobald das Objekt ImpotsDSN erstellt ist, können Benutzer der Klasse die Methode calculerImpots des Objekts aufrufen. Diese Methode erhält als Parameter ein Dictionary $personne:
       // $personne["marié"]: ja, nein
       // $personne["enfants"]: Anzahl der Kinder
       // $personne["salaire"]: Jahresgehalt

Die vollständige Klasse lautet wie folgt:

<?php
   // Definition einer Klasse objImpots
  class ImpotsDSN{

     // Attribute: die 3 Datentabellen
    var $limites;        // Grenzwerttabelle
    var $coeffR;        // Tabelle der coeffR
    var $coeffN;        // Tabelle der coeffN
    var $erreurs;        // Fehlerübersicht

     // Hersteller
    function ImpotsDSN($impots){
       // $impots: Wörterbuch mit den folgenden Feldern
       // dsn: Name DSN der Datenquelle ODBC, die die Werte der Grenztabellen coeffR und coeffN enthält
       // user: Name eines Benutzers mit Leserechten für die Datenquelle ODBC
       // pwd: sein Passwort
       // table: Name der Tabelle, die die Werte enthält (Grenzwerte, coeffR, coeffN)
       // Grenzwerte: Name der Spalte, die die Grenzwerte enthält
       // coeffR: Name der Spalte, die die Werte coeffR enthält       
       // coeffN: Name der Spalte, die die Werte coeffN enthält

       // zunächst keine Fehler
      $this->erreurs=array();

             // Überprüfung des Aufrufs
      if (! isset($impots[dsn]) || ! isset($impots[user]) ||! isset($impots[pwd]) ||! isset($impots[table]) ||
          ! isset($impots[limites]) ||! isset($impots[coeffR]) ||! isset($impots[coeffN])){
         // Fehler
        $this->erreurs[]="Appel incorrect";
         // Ende
        return;
      }//if

       // Datenbank öffnen DSN
      $connexion=odbc_connect($impots[dsn],$impots[user],$impots[pwd]);
       // Fehler?
      if(! $connexion){
         // Fehler
        $this->erreurs[]="Impossible d'ouvrir la base DSN [$impots[dsn]] (".odbc_error().")";
         // Ende
        return;
      }//if

             // Abfrage an die Datenbank gesendet
      $requête=odbc_prepare($connexion,"select $impots[limites],$impots[coeffR],$impots[coeffN] from $impots[table]");
            if(! odbc_execute($requête)){
         // Fehler
        $this->erreurs[]="Impossible d'expoiter la base DSN [$impots[dsn]] (".odbc_error().")";
         // Ende
        odbc_close($connexion);
        return;
      }//if

       // Auswertung der Abfrageergebnisse
      $this->limites=array();
      $this->coeffR=array();
      $this->coeffN=array();
      while(odbc_fetch_row($requête)){     
          // eine weitere Zeile
        $this->limites[]=odbc_result($requête,$impots[limites]);
        $this->coeffR[]=odbc_result($requête,$impots[coeffR]);
        $this->coeffN[]=odbc_result($requête,$impots[coeffN]);
      }//while

       // Datenbank schließen
      odbc_close($connexion);
    }//Konstruktor                

    // --------------------------------------------------------------------------
    function calculer($personne){
       // $personne["marié"]: ja, nein
       // $personne["enfants"]: Anzahl der Kinder
       // $personne["salaire"]: Jahresgehalt

       // Befindet sich das Objekt in einem ordnungsgemäßen Zustand?
      if (! is_array($this->erreurs) || count($this->erreurs)!=0) return -1;

            // Aufrufprüfung
      if (! isset($personne[marié]) || ! isset($personne[enfants]) ||! isset($personne[salaire])){
         // Fehler
        $this->erreurs[]="Appel incorrect";
         // Ende
        return -1;
      }//if

       // Sind die Parameter korrekt?
      $personne[marié]=strtolower($personne[marié]);
      if($personne[marié]!=oui && $personne[marié]!=non){
           // Fehler
        $this->erreurs[]="Statut marital [$personne[marié]] incorrect";
      }//if
      if(! preg_match("/^\s*\d{1,3}\s*$/",$personne[enfants])){
          // Fehler
        $this->erreurs[]="Nombre d'enfants [$personne[enfants]] incorrect";
      }//if
      if(! preg_match("/^\s*\d+\s*$/",$personne[salaire])){
          // Fehler
        $this->erreurs[]="salaire [$personne[salaire]] incorrect";
      }//if
       // Fehler?
      if(count($this->erreurs)!=0) return -1;

      // Anzahl der Anteile
      if($personne[marié]==oui) $nbParts=$personne[enfants]/2+2;
        else $nbParts=$personne[enfants]/2+1;
       // ein halber Anteil mehr, wenn mindestens 3 Kinder vorhanden sind
      if($personne[enfants]>=3) $nbParts+=0.5;
       // steuerpflichtiges Einkommen
      $revenuImposable=0.72*$personne[salaire];
       // Familienquotient
      $quotient=$revenuImposable/$nbParts;
       // wird am Ende der Grenzwertetabelle eingefügt, um die folgende Schleife zu beenden
      $this->limites[$this->nbLimites]=$quotient;
       // Steuerberechnung
      $i=0;
      while($quotient>$this->limites[$i]) $i++;
      // da $quotient am Ende des Arrays $limites platziert wurde, wird die vorherige Schleife
       // darf nicht über das Array $limites hinausgehen
       // Nun kann die Steuer berechnet werden
      return floor($revenuImposable*$this->coeffR[$i]-$nbParts*$this->coeffN[$i]);
    }//calculImpots
  }//Steuerklasse
?>

Ein Testprogramm könnte wie folgt aussehen:

<?php
     // Bibliotheken
  include "ImpotsDSN.php";

     // Anlegen eines Steuerobjekts
  $conf=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

  $objImpots=new ImpotsDSN($conf);

  // Fehler?
  if(count($objImpots->erreurs)!=0){
       // Meldung
    echo "Les erreurs suivantes se sont produites :\n";
    $erreurs=$objImpots->erreurs;
    for($i=0;$i<count($erreurs);$i++){
        echo "$erreurs[$i]\n";
    }//FOR
    // Ende
    exit(1);
  }//if

   // Tests
  calculerImpots($objImpots,oui,2,200000);
  calculerImpots($objImpots,non,2,200000);
  calculerImpots($objImpots,oui,3,200000);
  calculerImpots($objImpots,non,3,200000);
  calculerImpots($objImpots,array(),array(),array());

  // Ende
  exit(0);

  // -----------------------------------------------------------
  function calculerImpots($objImpots,$marié,$enfants,$salaire){
       // Ausgabe
        echo "impots($marié,$enfants,$salaire)\n";    

       // Steuerberechnung
    $personne=array(marié=>$marié,enfants=>$enfants,salaire=>$salaire);        
      $montant=$objImpots->calculer($personne);
     // Fehler?
    if(count($objImpots->erreurs)!=0){
         // Meldung
      echo "Les erreurs suivantes se sont produites :\n";
      $erreurs=$objImpots->erreurs;
      for($i=0;$i<count($erreurs);$i++){
          echo "$erreurs[$i]\n";
      }//for
    }else echo "montant=$montant\n";
  }//calculerImp
  • Das Testprogramm sorgt dafür, dass die Datei, die die Klasse ImpotsDSN enthält, „eingebunden“ wird
  • und erstellt anschließend ein Objekt objImpots der Klasse ImpotsDSN, wobei er dem Konstruktor des Objekts die erforderlichen Informationen übergibt
  • Sobald dieses Objekt erstellt ist, wird dessen Methode calculer fünfmal aufgerufen

Die erhaltenen Ergebnisse lauten wie folgt:


dos>e:\php43\php.exe test.php
impots(oui,2,200000)
montant=22504
impots(non,2,200000)
montant=33388
impots(oui,3,200000)
montant=16400
impots(non,3,200000)
montant=22504
impots(Array,Array,Array)
Les erreurs suivantes se sont produites :
Statut marital [array] incorrect
Nombre d'enfants [Array] incorrect
salaire [Array] incorrect

Wir werden fortan die Klasse ImpotsDSN verwenden, ohne deren Definition erneut anzugeben.

4.3. Anwendung Steuern: Version 1

Wir stellen nun die Version 1 der Anwendung IMPOTS vor. Wir befinden uns im Kontext einer Webanwendung, die einem Benutzer eine Schnittstelle HTML anzeigt, um die drei für die Steuerberechnung erforderlichen Parameter zu erfassen:

  • den Familienstand (verheiratet oder unverheiratet)
  • die Anzahl der Kinder
  • das Jahreseinkommen

Image

Die Anzeige des Formulars erfolgt über die folgende Seite PHP:

<?php //Steuerformular ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
           // Formular zurücksetzen
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//mit
      }//Löschen      
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui" <?php echo $requête->chkoui ?>>oui
              <input type="radio" name="optMarie" value="non" <?php echo $requête->chknon ?>>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête->enfants ?>"></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête->salaire ?>"></td>
          </tr>
          <tr>
              <td><font color="green">Impôt</font></td>
            <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête->impots ?>" readonly></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="submit" value="Calculer"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
    <?php
         // Gibt es Fehler?
      if(count($requête->erreurs)!=0){
           // Fehler anzeigen
        echo "<hr>\n<font color=\"red\">\n";
        echo "Les erreurs suivantes se sont produites<br>";
        echo "<ul>";
        for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i]."</li>\n";
        }
        echo "</ul>\n</font>\n";
      }//if 
   ?>
 </body>
</html>

Die Seite PHP zeigt lediglich Informationen an, die ihr vom Hauptprogramm der Anwendung in der Variablen $requête übergeben werden. Dabei handelt es sich um ein Objekt mit den folgenden Feldern:

chkoui, chknon
Attribute der Optionsfelder „Ja“ und „Nein“ – mögliche Werte sind "checked" oder "", um das entsprechende Optionsfeld zu aktivieren bzw. nicht zu aktivieren
enfants
Anzahl der Kinder des Steuerpflichtigen
salaire
sein Jahresgehalt
impots
die Höhe der zu zahlenden Steuer
erreurs
eine eventuelle Fehlerliste – kann leer sein.

Die an den Kunden gesendete Seite enthält ein JavaScript-Skript mit einer Funktion namens „effacer“, die der Schaltfläche „Effacer“ zugeordnet ist und dazu dient, das Formular in seinen Ausgangszustand zurückzusetzen: Schaltfläche nicht markiert, Eingabefelder leer. Es ist zu beachten, dass dieses Ergebnis mit einer Schaltfläche vom Typ „HTML“ nicht erzielt werden konnte. Denn wenn dieser Schaltflächentyp verwendet wird, versetzt der Browser das Formular in den Zustand zurück, in dem er es empfangen hat. In unserer Anwendung erhält der Browser jedoch Formulare, die möglicherweise nicht leer sind.

Die Anwendung, die das vorstehende Formular verarbeitet, lautet wie folgt:

<?php
     // bearbeitet das Steuerformular

     // Bibliotheken
  include "ImpotsDSN.php";

  // Konfiguration der Anwendung
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="impots_form.php";
  $erreursImpots="impots_erreurs.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

   // Parameter werden abgerufen
  $requête->marié=$_POST["optMarie"];
  $requête->enfants=$_POST["txtEnfants"];
  $requête->salaire=$_POST["txtSalaire"];

   // Sind alle Parameter vorhanden?
  if(! isset($requête->marié) || ! isset($requête->enfants) || ! isset($requête->salaire)){
       // Vorbereitung des leeren Formulars
    $requête->chkoui="";
    $requête->chknon="checked";
    $requête->enfants="";
    $requête->salaire="";
    $requête->impots="";
    $requête->erreurs=array();
     // Formular anzeigen
    include $formulaireImpots;
     // Ende
    exit(0);
  }//if

   // Parameterprüfung
  $requête=vérifier($requête);

   // Fehler?
  if(count($requête->erreurs)!=0){
       // Formular anzeigen
    include "$formulaireImpots";
     // Ende
    exit(0);
  }//if

   // Berechnung der Antwort
  $requête=calculerImpots($bdImpots,$requête);

   // Fehler?
  if(count($requête->erreurs)!=0){
       // Anzeige des Fehlerformulars
    include "$erreursImpots";
     // Ende
    exit(0);
  }//if

   // Formular anzeigen
  include "$formulaireImpots";
   // Ende
  exit(0);

  // --------------------------------------------------------
  function vérifier($requête){
       // Überprüfung der Gültigkeit der Anfrageparameter

     // zunächst keine Fehler
    $requête->erreurs=array();

     // Status gültig?
    $requête->marié=strtolower($requête->marié);
    if($requête->marié!="oui" && $requête->marié!="non"){
         // ein Fehler
      $requête->erreurs[]="Statut marital [$requête->marié] incorrect";
    }

     // Gültige Anzahl von Kindern?
    if(! preg_match("/^\s*\d+\s*$/",$requête->enfants)){
        // ein Fehler
      $requête->erreurs[]="Nombre d'enfants [$requête->enfants] incorrect";
    }

     // Gültiges Gehalt?
    if(! preg_match("/^\s*\d+\s*$/",$requête->salaire)){
        // ein Fehler
      $requête->erreurs[]="Salaire [$requête->salaire] incorrect";
    }

     // Status der Optionsfelder
    if($requête->marié=="oui"){
        $requête->chkoui="checked";
      $requête->chknon="";
    }else{
        $requête->chknon="checked";
      $requête->chkoui="";
    }

     // Die Anfrage wird gesendet
    return $requête;
  }//prüfen

  // --------------------------------------------------------
  function calculerImpots($bdImpots,$requête){
       // Steuerbetrag berechnen

     // $bdImpots: Wörterbuch mit den erforderlichen Informationen zum Einlesen der Datenquelle ODBC
     // $requête: die Abfrage mit den Informationen zum Steuerpflichtigen

     // Es wird ein Objekt erstellt: ImpotsDSN 
    $objImpots=new ImpotsDSN($bdImpots);

    // Gibt es Fehler?
    if(count($objImpots->erreurs)!=0){
         // Die Fehler werden in die Abfrage aufgenommen
      $requête->erreurs=$objImpots->erreurs;
      // Fertig
      return $requête;
    }//if

    // Steuerberechnung
    $personne=array(marié=>"$requête->marié",enfants=>"$requête->enfants",salaire=>"$requête->salaire");
        $requête->impots=$objImpots->calculer($personne);

     // Das Ergebnis wird ausgegeben
    return $requête;
  }//calculerImpots

Anmerkungen:

  • Zunächst wird die Klasse ImpotsDSN eingebunden. Sie bildet die Grundlage für die Steuerberechnung und verbirgt den Zugriff auf die Datenbank
  • Es werden einige Initialisierungen vorgenommen, um die Wartung der Anwendung zu erleichtern. Wenn sich Parameter ändern, werden ihre Werte in diesem Abschnitt angepasst. In der Regel werden diese Konfigurationswerte eher in einer Datei oder einer Datenbank gespeichert.
  • Es werden die drei Parameter des Formulars abgerufen, die den Feldern HTML, optMarie, txtEnfants und txtSalaire entsprechen.
  • Diese Parameter können ganz oder teilweise fehlen. Dies ist insbesondere bei der ersten Anforderung des Formulars der Fall. In diesem Fall wird lediglich ein leeres Formular gesendet.
  • Anschließend wird die Gültigkeit der drei abgerufenen Parameter überprüft. Sollten diese fehlerhaft sein, wird das Formular so zurückgesendet, wie es eingegeben wurde, jedoch zusätzlich mit einer Fehlerliste.
  • Sobald die Gültigkeit der drei Parameter überprüft wurde, kann die Steuerberechnung durchgeführt werden. Diese wird von der Funktion calculerImpots berechnet. Diese Funktion erstellt ein Objekt vom Typ ImpotsDSN und nutzt die Methode calculer dieses Objekts.
  • Die Erstellung des Objekts ImpotsDSN kann fehlschlagen, wenn die Datenbank nicht verfügbar ist. In diesem Fall zeigt die Anwendung eine spezielle Fehlerseite an, auf der die aufgetretenen Fehler aufgeführt sind.
  • Wenn alles erfolgreich verlaufen ist, wird das Formular so zurückgegeben, wie es eingegeben wurde, jedoch zusätzlich mit dem zu zahlenden Steuerbetrag.

Die Fehlerseite sieht wie folgt aus:

<?php    // Fehlerseite ?>
<html>
    <head>
      <title>Application impôts indisponible</title>
  </head>
  <body background="/poly/impots/images/standard.jpg">
      <h3>Calcul d'impôts</h3>
    <hr>
    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <?php
           // Anzeige der Fehler
        for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

Hier sind einige Fehlerbeispiele. Zunächst wird ein falsches Passwort eingegeben:

Image

Es werden falsche Daten in das Formular eingegeben:

Image

Schließlich werden korrekte Werte eingegeben:

Image

4.4. Steuer-App: Version 2

Im vorangegangenen Beispiel werden die Parameter txtEnfants und txtSalaire des Formulars vom Server auf ihre Gültigkeit überprüft. Hier soll die Überprüfung durch ein in die Formularseite eingebundenes JavaScript-Skript erfolgen. Die Überprüfung der Parameter übernimmt dann der Browser. Der Server wird nur dann angesprochen, wenn diese gültig sind. Auf diese Weise wird „Bandbreite“ eingespart. Die Anzeigeseite impots-form.php sieht dann wie folgt aus:

<?php //Steuerformular ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
........
      }//Löschen

      //------------------------------
      function calculer(){
           // Überprüfung der Parameter vor dem Absenden an den Server
        with(document.frmImpots){
          //Anzahl der Kinder
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // Das Formular wurde nicht überprüft
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
           //Gehalt
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // Das Modell wurde nicht überprüft
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//wenn
          // Alles in Ordnung – das Formular wird an den Server gesendet
          submit();
        }//mit
      }//berechnen            
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
............
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
........
 </body>
</html>

Es sind folgende Änderungen zu beachten:

  • Die Schaltfläche Calculer hat nicht mehr den Typ submit, sondern den Typ button, der einer Funktion namens calculer zugeordnet ist. Diese Funktion führt die Analyse der Felder txtEnfants und txTsalaire durch. Sind diese korrekt, werden die Werte des Formulars an den Server gesendet (Submit), andernfalls wird eine Fehlermeldung angezeigt.

Hier ein Beispiel für die Anzeige im Fehlerfall:

Image

4.5. Steueranwendung: Version 3

Wir nehmen eine geringfügige Änderung an der Anwendung vor, um das Konzept der Sitzung einzuführen. Wir betrachten die Anwendung nun als eine Simulation zur Steuerberechnung. Ein Benutzer kann somit verschiedene „Konfigurationen“ von Steuerpflichtigen simulieren und sehen, wie hoch die jeweils zu zahlende Steuer wäre. Die folgende Webseite zeigt ein Beispiel dafür, was dabei herauskommen könnte:

Image

Das Programm zur Darstellung der oben gezeigten Seite lautet wie folgt:

<?php //Steuerformular ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
...
      }//löschen

      //------------------------------
      function calculer(){
...
      }//berechnen            
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui" <?php echo $requête[chkoui] ?>>oui
              <input type="radio" name="optMarie" value="non" <?php echo $requête[chknon] ?>>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête[enfants] ?>"></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête[salaire] ?>"></td>
          </tr>
          <tr>
              <td><font color="green">Impôt</font></td>
            <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête[impots] ?>" readonly></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
    <?php
         // Gibt es Fehler?
      if(count($requête[erreurs])!=0){
           // Fehler anzeigen
        echo "<hr>\n<font color=\"red\">\n";
        echo "Les erreurs suivantes se sont produites<br>";
        echo "<ul>";
        for($i=0;$i<count($requête[erreurs]);$i++){
            echo "<li>".$requête[erreurs][$i]."</li>\n";
        }
        echo "</ul>\n</font>\n";
      }//if 
         // Gibt es Simulationen?
      else if(count($requête[simulations])!=0){
          // Anzeige der Simulationen
        echo "<hr>\n<h2>Résultats des simulations</h2>\n";
        echo "<table border=\"1\">\n";
        echo "<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>\n";
        for($i=0;$i<count($requête[simulations]);$i++){
            echo "<tr>".
              "<td>".$requête[simulations][$i][0]."</td>".
              "<td>".$requête[simulations][$i][1]."</td>".
              "<td>".$requête[simulations][$i][2]."</td>".
              "<td>".$requête[simulations][$i][3]."</td>".
            "</tr>\n";                                    
        }//for
        echo "</table>\n";
      }//if 
   ?>
 </body>
</html>

Das Anzeigeprogramm weist geringfügige Unterschiede zur vorherigen Version auf:

  • Das Wörterbuch erhält ein Wörterbuch mit der Kennung $requête anstelle eines Objekts mit der Kennung $requête. Man schreibt also $requête[enfants] und nicht $requête->Kinder.
  • Dieses Wörterbuch enthält ein Feld simulations, bei dem es sich um ein zweidimensionales Array handelt. $requête[simulations][i] ist die Simulation Nr. i. Diese ist ihrerseits ein Array aus vier Zeichenketten: Familienstand, Anzahl der Kinder, Jahresgehalt, zu zahlende Steuer.

Das Hauptprogramm hat sich hingegen stärker weiterentwickelt:

<?php
     // bearbeitet das Steuerformular

     // Bibliotheken
  include "ImpotsDSN.php";

   // Sitzung starten
  session_start();

   // Anwendungskonfiguration
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="impots_form.php";
  $erreursImpots="impots_erreurs.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

     // die Sitzungsparameter werden abgerufen
  $session=$_SESSION["session"];
   // Ist die Sitzung gültig?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // Eine neue Sitzung wird gestartet
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // Gibt es Fehler?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // Fehlerseite anzeigen
        include $erreursImpots;
       // Ende
      $session=array();
      terminerSession($session);
    }//if
  }//if

  // Die Parameter des laufenden Austauschs werden abgerufen
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // Sind alle Parameter vorhanden?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // Anzeige eines leeren Formulars
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // Ende
    terminerSession($session);
  }//if

   // Parameterprüfung
  $requête=vérifier($requête);

   // Gibt es Fehler?
  if(count($requête[erreurs])!=0){
       // Formular anzeigen
    include "$formulaireImpots";
     // Ende
    terminerSession($session);
  }//if

   // Berechnung der zu zahlenden Steuer
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));

   // eine weitere Simulation
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // Formularanzeige
  include "$formulaireImpots";

   // Ende
  terminerSession($session);

  // --------------------------------------------------------
  function vérifier($requête){
       // Überprüfung der Gültigkeit der Abfrageparameter

     // zunächst keine Fehler
    $requête[erreurs]=array();

     // Status gültig?
    $requête[marié]=strtolower($requête[marié]);
    if($requête[marié]!="oui" && $requête[marié]!="non"){
         // ein Fehler
      $requête[erreurs][]="Statut marital [$requête[marié]] incorrect";
    }

     // Gültige Anzahl von Kindern?
    if(! preg_match("/^\s*\d+\s*$/",$requête[enfants])){
        // ein Fehler
      $requête[erreurs][]="Nombre d'enfants [$requête[enfants]] incorrect";
    }

     // Gültiges Gehalt?
    if(! preg_match("/^\s*\d+\s*$/",$requête[salaire])){
        // ein Fehler
      $requête[erreurs][]="Salaire [$requête[salaire]] incorrect";
    }

     // Status der Optionsfelder
    if($requête[marié]=="oui"){
        $requête[chkoui]="checked";
      $requête[chknon]="";
    }else{
        $requête[chknon]="checked";
      $requête[chkoui]="";
    }

     // Die Anfrage wird gesendet
    return $requête;
  }//prüfen

  // --------------------------------------------------------
  function terminerSession($session){
       // Die Sitzung wird gespeichert
    $_SESSION[session]=$session;
     // Ende des Skripts
    exit(0);
  }//terminerSession
  • Zunächst einmal verwaltet diese Anwendung eine Sitzung. Daher wird zu Beginn des Skripts eine Sitzung gestartet:
  session_start();
  • Die Sitzung speichert eine einzige Variable: das Wörterbuch $session. Dieses Wörterbuch hat zwei Felder:
    • objImpots: ein Objekt ImpotsDSN. Dieses Objekt wird in der Sitzung des Clients zwischengespeichert, um unnötige wiederholte Zugriffe auf die Datenbank ODBC zu vermeiden.
    • Simulationen: Die Simulationstabelle dient dazu, die Simulationen früherer Transaktionen von einer Transaktion zur nächsten zu speichern.
  • Sobald die Sitzung gestartet wurde, wird die Variable $session abgerufen. Existiert diese noch nicht, bedeutet dies, dass die Sitzung gerade startet. In diesem Fall werden ein Objekt ImpotsDSN und ein leeres Simulationsarray angelegt. Falls erforderlich, werden Fehler gemeldet.
<?php
...
     // Sitzungsparameter abrufen
  $session=$_SESSION["session"];
   // Ist die Sitzung gültig?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // Eine neue Sitzung wird gestartet
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // Sind Fehler aufgetreten?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // Fehlerseite anzeigen
        include $erreursImpots;
       // Ende
      $session=array();
      terminerSession($session);
    }//if
  }//if
  • Sobald die Sitzung ordnungsgemäß initialisiert wurde, werden die Parameter für den Datenaustausch abgerufen. Sind nicht alle erwarteten Parameter vorhanden, wird ein leeres Formular gesendet.
<?php
...
   // Die Parameter des laufenden Austauschs werden abgerufen
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // Sind alle Parameter vorhanden?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // Anzeige eines leeren Formulars
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // Ende
    terminerSession($session);
  }//if
  • Wenn alle Parameter vorhanden sind, werden sie überprüft. Falls Fehler auftreten, werden diese gemeldet:
<?php
...
   // Parameterprüfung
  $requête=vérifier($requête);

   // Gibt es Fehler?
  if(count($requête[erreurs])!=0){
       // Formular anzeigen
    include "$formulaireImpots";
     // Ende
    terminerSession($session);
  }//if
  • Wenn die Parameter korrekt sind, wird die Steuer berechnet:
<?php
...
   // Berechnung der zu zahlenden Steuer
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));
  • Die laufende Simulation wird zur Simulationstabelle hinzugefügt, die zusammen mit dem Formular an den Kunden gesendet wird:
<?php
...
   // eine weitere Simulation
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // Formularanzeige
  include "$formulaireImpots";

   // Ende
  terminerSession($session);
  • In jedem Fall endet das Skript mit der Speicherung der Variablen $session in der Sitzung. Dies geschieht in der Prozedur terminerSession.
<?php
...

  function terminerSession($session){
       // Sitzung speichern
    $_SESSION[session]=$session;
     // Ende des Skripts
    exit(0);
  }//terminerSession

Zum Abschluss stellen wir noch das Programm zur Anzeige von Datenbankzugriffsfehlern (impots-erreurs.php) vor:

<?php    // Fehlerseite ?>
<html>
    <head>
      <title>Application impôts indisponible</title>
  </head>
  <body background="/poly/impots/images/standard.jpg">
      <h3>Calcul d'impôts</h3>
    <hr>
    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <?php
           // Anzeige der Fehler
        for($i=0;$i<count($requête[erreurs]);$i++){
            echo "<li>".$requête[erreurs][$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

Auch hier wurde das Objekt $requête durch ein Dictionary $requête ersetzt.

4.6. Anwendung „Steuern“: Version 4

Wir werden nun eine eigenständige Anwendung erstellen, die als Web-Client für die zuvor genannte Webanwendung impots fungiert. Die Anwendung wird eine Konsolenanwendung sein, die über ein Fenster DOS gestartet wird:


dos>e:\php43\php.exe cltImpots.php
Syntaxe cltImpots.php urlImpots marié enfants salaire [jeton]

Die Parameter des Web-Clients cltImpots lauten wie folgt:

     // Syntax $0 urlImpots verheiratet Kinder Gehalt Token
   // Client eines Steuerdienstes, der auf urlImpots läuft
   // sendet an diesen Dienst die drei Informationen: verheiratet, Kinder, Gehalt
   // ggf. zusammen mit dem Sitzungstoken, falls dieses übergeben wurde
   // zeigt die Tabelle mit den Simulationsergebnissen der Sitzung an

Hier sind einige Anwendungsbeispiele. Zunächst ein erstes Beispiel ohne Session-Token.

dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php ja 2 200000

Jeton de session=[a6297317667bc981c462120987b8dd18]

Simulations :
[Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]
[oui,2,200000,22504]

Wir haben den zu zahlenden Steuerbetrag (22504 f) erfolgreich abgerufen. Außerdem haben wir das Sitzungs-Token abgerufen. Dieses können wir nun für eine zweite Abfrage verwenden:

dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php ja 3 200000 a6297317667bc981c462120987b8dd18

Jeton de session=[a6297317667bc981c462120987b8dd18]

Simulations :
[Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]
[oui,2,200000,22504]
[oui,3,200000,16400]

Wir erhalten tatsächlich die vom Webserver gesendete Tabelle mit den Simulationen. Das abgerufene Token bleibt natürlich dasselbe. Wenn wir den Webdienst abfragen, während die Datenbank noch nicht gestartet wurde, erhalten wir folgendes Ergebnis:

dos>e:\php43\php.exe cltImpots2.php http://localhost/poly/impots/6/impots.php ja 3 2.000.000

Jeton de session=[8369014d5053212bc42f64bbdfb152ee]

Les erreurs suivantes se sont produites :
Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)

Beim Schreiben eines programmierten Web-Clients muss man genau wissen, was der Server als Antwort auf die verschiedenen möglichen Anfragen eines Clients sendet. Der Server sendet eine Reihe von Zeilen HTML, die nützliche Informationen sowie andere enthalten, die lediglich dem Layout dienen HTML. Reguläre Ausdrücke von PHP können uns dabei helfen, die nützlichen Informationen in der Flut der vom Server gesendeten Zeilen zu finden. Dazu müssen wir das genaue Format der verschiedenen Antworten des Servers kennen. Zu diesem Zweck können wir den Webdienst mit einem Browser aufrufen und den gesendeten Quellcode betrachten. Es ist zu beachten, dass diese Methode keine Auskunft über die Header der Antwort des Webservers gibt. Manchmal ist es jedoch nützlich, diese zu kennen. In diesem Fall kann man einen der beiden generischen Web-Clients verwenden, die im vorigen Kapitel vorgestellt wurden.

Wenn wir die vorherigen Beispiele wiederholen, lautet der vom Browser für die Simulationstabelle empfangene Code HTML:

<h2>Résultats des simulations</h2>
<table border="1">
<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>
<tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>
<tr><td>oui</td><td>3</td><td>200000</td><td>16400</td></tr>
</table>

Es handelt sich um eine Tabelle mit dem Namen HTML, die einzige im gesendeten Dokument. Daher muss der reguläre Ausdruck "|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|" es ermöglichen, die verschiedenen erhaltenen Simulationen im Dokument zu finden. Wenn die Datenbank nicht verfügbar ist, erhält man das folgende Dokument:

    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <li>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</li>
        </ul>
    </font>

Um den Fehler abzurufen, muss der Webclient in der Antwort des Webservers nach den Zeilen suchen, die dem Muster "|<li>(.+?)</li>|" entsprechen.

Wir haben nun die Richtlinien dafür, was zu tun ist, wenn der Benutzer die Steuerberechnung über den vorherigen Konsolen-Client anfordert:

  • Überprüfen, ob alle Parameter gültig sind, und gegebenenfalls Fehler melden.
  • eine Verbindung zu dem als ersten Parameter angegebenen URL herstellen. Dazu folgt man dem bereits vorgestellten und untersuchten Muster des generischen Web-Clients
  • im Antwortstrom des Servers verwenden wir reguläre Ausdrücke, um entweder:
    • Fehlermeldungen zu finden
    • die Ergebnisse der Simulationen zu finden

Der Kundencode cltImpots.php lautet wie folgt:

<?php
     // Syntax $0 urlImpots verheiratet Kinder Gehalt Token
   // Kunde einer Steuerbehörde, die auf urlImpots läuft
   // sendet diesem Dienst die drei Informationen: verheiratet, Kinder, Gehalt
   // ggf. zusammen mit dem Sitzungstoken, falls dieses übergeben wurde
   // zeigt die Tabelle mit den Simulationen der Sitzung an

   // Überprüfung der Anzahl der Argumente
  if(count($argv)!=5 && count($argv)!=6){
       // Fehler
    fwrite(STDERR,"Syntaxe $argv[0] urlImpots marié enfants salaire [jeton]\n");
     // Ende
    exit(1);
  }//if

   // man beachte das URL
  $urlImpots=analyseURL($argv[1]);
  if(isset($urlImpots[erreur])){
       // Fehler
    fwrite(STDERR,"$urlImpots[erreur]\n");
     // Ende
    exit(1);
  }//if

   // Analyse der Benutzeranfrage
  $demande=analyseDemande("$argv[2],$argv[3],$argv[4]");
   // Fehler?
  if($demande[erreur]){
       // Meldung
    echo "$demande[erreur]\n";
     // Fertig
    exit(1);
  }//if

   // Die Anfrage kann gestellt werden – das Sitzungstoken wird verwendet
  $impots=getImpots($urlImpots,$demande,$argv[5]);

   // Das Session-Token wird angezeigt
  echo "Jeton de session=[$impots[jeton]]\n";

   // Fehler?
  if(count($impots[erreurs])!=0){
       //Fehlermeldungen
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//für
  }else{
       // keine Fehler – Anzeige der Simulationen
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//for
  }//if

   // Programmende
  exit(0);

  // --------------------------------------------------------------
  function analyseURL($URL){  
       // überprüft die Gültigkeit von URL $URL
    $url=parse_url($URL);
    // das Protokoll    
    if(strtolower($url[scheme])!="http"){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // der Rechner
    if(! isset($url[host])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // der Port
    if(! isset($url[port])) $url[port]=80;
     // die Anfrage
    if(isset($url["query"])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // Rückgabe
    return $url;
  }//analyseURL

  // -----------------------------------------------------------
  function analyseDemande($demande){
       // $demande: zu analysierende Zeichenfolge
     // muss die Form „verheiratet, Kinder, Gehalt“ haben
    // gültiges Format
    if(! preg_match("/^\s*(oui|non)\s*,\s*(\d{1,3})\s*,\s*(\d+)\s*$/i",$demande,$champs)){
        // ungültiges Format
      return(array(erreur=>"Format (marié, enfants, salaire) invalide."));
    }    
     // ist in Ordnung
    $marié=strtolower($champs[1]);
    return array(marié=>$marié,enfants=>$champs[2],salaire=>$champs[3]);
  }//analyseDemande

  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: URL ist abzufragen
     // $demande: Wörterbuch mit den Feldern Ehepartner, Kinder, Gehalt
     // $jeton: ein mögliches Sitzungstoken

     // Herstellen einer Verbindung über den Port $urlImpots[port] von $urlImpots[host]
    $connexion=fsockopen($urlImpots[host],$urlImpots[port],&$errno,&$erreur);
     // Rückkehr im Fehlerfall
    if(! $connexion){
      return array(erreurs=>array("Echec de la connexion au site ($urlImpots[host],$urlImpots[port]) : $erreur"));
    }//if

         // die Header an den Server gesendet werden HTTP
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));

         // die Antwort des Servers wird gelesen – zunächst die Header HTTP
     // die erste Zeile
    $ligne=fgets($connexion,10000);

    // URL gefunden?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
        // URL nicht erreichbar – Rückmeldung mit Fehler
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//if

     // werden die anderen Header gelesen: HTTP
    while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
       // Suche nach dem Token, falls es noch nicht gefunden wurde      
      if(! $jeton){
           // Suche nach der Zeile „set-cookie“
        if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
            // Das Token wurde gefunden – es wird gespeichert          
          $jeton=$champs[1];            
        }//if
      }//if
    }//nächste Zeile

     // das folgende Dokument wird gelesen
    $document="";
    while($ligne=fread($connexion,10000)){
        $document.=$ligne;
    }//while

     // die Verbindung wird geschlossen
    fclose($connexion);

     // das Dokument wird analysiert
    $impots=getInfos($document);

     // das Ergebnis wird ausgegeben
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
    }//getImpots

  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: Verbindung zum Webserver
     // $url: das abzufragende URL
     // $paramètres: Wörterbuch der zu übermittelnden Parameter

     // Vorbereitung des POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//während
     // das letzte Zeichen entfernt wird
    $post=substr($post,0,-1);

     // Die Anfrage HTTP wird im Format HTTP/1.0 vorbereitet
    $HTTP="POST $url[path] HTTP/1.0\n";
    $HTTP.="Content-type: application/x-www-form-urlencoded\n";
    $HTTP.="Content-length: ".strlen($post)."\n";
    $HTTP.="Connection: close\n";
    if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";
    $HTTP.="\n";
    $HTTP.=$post;

    // die Anfrage HTTP wird gesendet
    fwrite($connexion,$HTTP);
    }//POST

  // --------------------------------------------------------------
  function getInfos($document){
       // $document: Dokument HTML
     // gesucht wird entweder die Fehlerliste
     // oder die Simulationstabelle

     // Aufbereitung des Ergebnisses
    $impots[erreurs]=array();
    $impots[simulations]=array();

     // Gibt es einen Fehler?
     // Modell der Fehlerzeile
    $modErreur="|<li>(.+?)</li>|";
     // Suche im Dokument
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // Abruf der Fehler
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//für
       // fertig
      return $impots;
    }//, falls

     // Beispiel für eine Zeile aus der Simulationstabelle
     // <tr><td>ja</td><td>2</td><td>200000</td><td>22504</td></tr>
    $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";
    // Suche im Dokument
    if(preg_match_all($modSimulation,$document,$champs,PREG_SET_ORDER)){    
         // Abruf der Simulationen
      for($i=0;$i<count($champs);$i++){
          $impots[simulations][]=array($champs[$i][1],$champs[$i][2],$champs[$i][3],$champs[$i][4]);
      }//für
       // Ergebnisrückgabe
      return $impots;
    }//if

    // Es ist nicht normal, hierher zu gelangen
    return $impots;
    }//getInfos
?>

Anmerkungen:

  • Das Programm überprüft zunächst die Gültigkeit der übergebenen Parameter. Dazu verwendet es zwei Funktionen: analyseURL, die die Gültigkeit von URL überprüft, und analyseDemande, die die übrigen Parameter überprüft.
  • Die Steuerberechnung erfolgt durch die Funktion getImpots:
  $impots=getImpots($urlImpots,$demande,$argv[5]);
  • Die Funktion getImpots ist wie folgt deklariert:
<?php
...
  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: URL abfragen
     // $demande: Wörterbuch mit den Feldern Ehepartner, Kinder, Gehalt
     // $jeton: ein mögliches Sitzungstoken
  • (Fortsetzung)
    • $urlImpots ist ein Wörterbuch, das folgende Felder enthält:
      • host: Rechner, auf dem der Webdienst läuft
      • port: dessen Dienstport
      • path: der Pfad der angeforderten Ressource
    • $demande ist ein Wörterbuch, das folgende Felder enthält:
      • verheiratet: ja/nein: Familienstand
      • Kinder: Anzahl der Kinder
      • Gehalt: Jahresgehalt
    • $jeton ist das Session-Token.

Die Funktion gibt als Ergebnis ein Dictionary mit folgenden Feldern zurück:

  • Fehler: Array mit Fehlermeldungen
  • Simulationen: Array von Simulationen, wobei eine Simulation selbst ein Array mit vier Elementen ist (verheiratet, Kinder, Gehalt, Steuern)
  • Token: das Sitzungstoken
  • Sobald das Ergebnis von getImpots vorliegt, zeigt das Programm die Ergebnisse an und beendet sich:
<?php
...

   // Das Session-Token wird angezeigt
  echo "Jeton de session=[$impots[jeton]]\n";

   // Fehler?
  if(count($impots[erreurs])!=0){
       //Fehlermeldungen
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//für
  }else{
       // keine Fehler – Anzeige der Simulationen
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//for
  }//if

   // Programmende
  exit(0);
  • Betrachten wir nun die Funktion getImpots($urlImpots,$demande,$jeton), die
    • eine Verbindung TCP-IP auf dem Port $urlImpots[port] des Rechners $urlImpots[host] herstellen
    • die vom Webserver erwarteten Header HTTP an diesen senden, insbesondere das Session-Token, falls vorhanden
    • eine Anfrage mit der Kennung POST und den im Wörterbuch $demande enthaltenen Parametern an den Webserver senden
    • die Antwort des Webservers analysieren, um darin entweder eine Fehlerliste oder eine Tabelle mit Simulationen zu finden.
  • Das Senden der Header HTTP erfolgt mithilfe einer Funktion POST:
<?php
...
         // Die Header HTTP werden an den Server gesendet
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));
  • Nachdem die Header HTTP gesendet wurden, liest die Funktion getImpots die gesamte Antwort des Servers ein und speichert sie in $document. Die Antwort wird ohne weitere Analyse eingelesen, mit Ausnahme der ersten Zeile, anhand derer wir erkennen können, ob der Webserver die angeforderte URL gefunden hat oder nicht. Wenn die URL gefunden wurde, antwortet der Webserver mit HTTP/1.X 200 OK, wobei X von der verwendeten Version von HTTP abhängt.
<?php
...
    // URL gefunden?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
 <?php
...
       // URL nicht erreichbar – Rückgabe mit Fehler
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//if
  • Das Dokument wird mit der Funktion getInfos analysiert:
<?php
...
     // wird das Dokument analysiert
    $impots=getInfos($document);
  • Das Ergebnis ist ein Wörterbuch mit zwei Feldern:
    • Fehler: Liste der Fehler – kann leer sein
    • Simulationen: Liste der Simulationen – kann leer sein
  • Sobald dies erledigt ist, kann die Funktion getImpots ihr Ergebnis in Form eines Dictionaries ausgeben.
<?php
...
     // das Ergebnis wird zurückgegeben
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
  • Betrachten wir nun die Funktion POST:
<?php
...
  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: Die Verbindung zum Webserver
     // $url: das abzufragende URL
     // $paramètres: Parameter-Dictionary für die Übermittlung

     // Vorbereitung des POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//während
     // das letzte Zeichen entfernt wird
    $post=substr($post,0,-1);

     // Die Anfrage HTTP wird im Format HTTP/1.0 vorbereitet
    $HTTP="POST $url[path] HTTP/1.0\n";
    $HTTP.="Content-type: application/x-www-form-urlencoded\n";
    $HTTP.="Content-length: ".strlen($post)."\n";
    $HTTP.="Connection: close\n";
    if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";
    $HTTP.="\n";
    $HTTP.=$post;

    // die Anfrage HTTP wird gesendet
    fwrite($connexion,$HTTP);
    }//POST

Zwar hatten wir im Rahmen des Programms bereits die Möglichkeit, eine Webressource über einen GET abzufragen, doch hatten wir noch keine Gelegenheit, dies mit einem POST zu tun. Der POST unterscheidet sich vom GET in der Art und Weise, wie Parameter an den Server gesendet werden. Er muss die folgenden HTTP-Header senden:

POST chemin HTTP/1.X
Content-type: application/x-www-form-urlencoded
Content-length: N

mit

  • chemin: Pfad zur angeforderten Webressource, hier $url[path]
  • HTTP/1.X: das gewünschte Protokoll HTTP. Hier wurde HTTP/1.0 gewählt, um die Antwort in einem einzigen Block zu erhalten. HTTP/1.1 ermöglicht die Übermittlung der Antwort in mehreren Blöcken (chunked).
  • N gibt die Anzahl der Zeichen an, die der Client an den Server senden wird.

Die N Zeichen, aus denen sich die Abfrageparameter zusammensetzen, werden unmittelbar hinter der Leerzeile gesendet, die die an den Server gesendeten HTTP-Header abschließt. Diese Parameter haben die Form param1=val1&param2=val2&..., wobei parami der Name des Parameters und vali dessen Wert ist. Gültige Werte können „störende“ Zeichen wie Leerzeichen, das Zeichen &, das Zeichen = usw. enthalten. Diese Zeichen müssen durch die Zeichenfolge %XX ersetzt werden, wobei XX ihr Hexadezimalcode ist. Die Funktion PHP urlencode übernimmt diese Aufgabe. Die umgekehrte Aufgabe wird von urldecode ausgeführt.

Abschließend sei angemerkt, dass ein Token, falls vorhanden, mit dem Header HTTP Cookie: PHPSESSID=Token gesendet wird.

  • Nun müssen wir noch die Funktion untersuchen, die die Antwort des Servers analysiert:
<?php
...
  // --------------------------------------------------------------
  function getInfos($document){
       // $document: Dokument HTML
     // gesucht wird entweder die Fehlerliste
     // oder die Simulationstabelle

     // Aufbereitung des Ergebnisses
    $impots[erreurs]=array();
    $impots[simulations]=array();
  • Zur Erinnerung: Fehler werden vom Server in der Form <li>Fehlermeldung</li> gesendet. Der reguläre Ausdruck „|<li>(.+?)</li>|“ soll es ermöglichen, diese Informationen zu extrahieren. Wir haben hier das Zeichen | zur Abgrenzung des regulären Ausdrucks verwendet und nicht das Zeichen /, da dieses auch im regulären Ausdruck selbst vorkommt. Die Funktion preg_match_all ($modèle, $document, $champs, PREG_SET_ORDER) ermöglicht es, alle Vorkommen von $modèle zu extrahieren, die in $document gefunden wurden. Diese werden in das Array $champs gespeichert. Somit stellt $champs[i] das Vorkommen Nr. i von $modèle in $document dar. In $champs[$i][0] wird die dem Muster entsprechende Zeichenfolge abgelegt. Wenn diese Klammern enthielt, wird die Zeichenfolge, die der ersten Klammer entspricht, in $champs[$i][1] eingefügt, die zweite in $champs[$i][2] und so weiter. Der Code zum Abrufen der Fehler lautet daher wie folgt:
<?php
...

     // Gibt es einen Fehler?
     // Muster der Fehlerzeile
    $modErreur="|<li>(.+?)</li>|";
     // Suche im Dokument
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // Fehlerabruf
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//für
       // fertig
      return $impots;
    }//if

4.7. Steueranwendung: Fazit

Wir haben verschiedene Versionen unserer Client-Server-Anwendung zur Steuerberechnung vorgestellt:

  • Version 1: Der Dienst wird durch eine Reihe von Programmen (PHP) bereitgestellt, der Client ist ein Browser. Er führt nur eine einzige Simulation durch und speichert keine früheren Ergebnisse.
  • Version 2: Es werden einige browserbasierte Funktionen hinzugefügt, indem JavaScript-Skripte in das vom Browser geladene Dokument HTML eingebunden werden. Diese überprüfen die Gültigkeit der Formularparameter.
  • Version 3: Der Dienst kann sich nun durch die Verwaltung einer Sitzung an die verschiedenen von einem Client durchgeführten Simulationen erinnern. Die Schnittstelle HTML wird entsprechend angepasst, um diese anzuzeigen.
  • Version 4: Der Client ist nun eine eigenständige Konsolenanwendung. Dies ermöglicht es uns, wieder auf die Entwicklung programmierter Web-Clients zurückzugreifen.

An dieser Stelle lassen sich einige Anmerkungen machen:

  • Die Versionen 1 bis 3 unterstützen Browser, die lediglich über die Fähigkeit verfügen, JavaScript-Skripte auszuführen. Es sei angemerkt, dass ein Benutzer jederzeit die Möglichkeit hat, die Ausführung dieser Skripte zu unterbinden. Die Anwendung funktioniert dann in Version 1 nur teilweise (die Option „Löschen“ funktioniert nicht) und in den Versionen 2 und 3 überhaupt nicht (die Optionen „Löschen“ und „Berechnen“ funktionieren nicht). Es könnte interessant sein, eine Version des Dienstes zu entwickeln, die ohne JavaScript-Skripte auskommt.

Wenn man einen Webdienst entwickelt, muss man sich fragen, welche Arten von Clients man ansprechen möchte. Wenn man möglichst viele Clients ansprechen möchte, sollte man eine Anwendung schreiben, die an die Browser nur HTML sendet (kein JavaScript und keine Applets). Arbeitet man hingegen in einem Intranet und hat die Kontrolle über die Konfiguration der dortigen Endgeräte, kann man sich erlauben, höhere Anforderungen an den Client zu stellen.

Version 4 ist ein Webclient, der die benötigten Informationen aus dem vom Server gesendeten Datenstrom „HTML“ entnimmt. Sehr oft hat man keinen Einfluss auf diesen Datenstrom. Dies ist der Fall, wenn man einen Client für einen im Netzwerk vorhandenen Webdienst geschrieben hat, der von jemand anderem verwaltet wird. Nehmen wir ein Beispiel. Nehmen wir an, unser Dienst zur Simulation von Steuerberechnungen wurde von einem Unternehmen X entwickelt. Derzeit sendet der Dienst die Simulationen in einer Tabelle mit dem Namen HTML, und unser Client nutzt diese Tatsache, um sie abzurufen. Er vergleicht dabei jede Zeile der Serverantwort mit dem regulären Ausdruck:

     // Beispiel für eine Zeile der Simulationstabelle
     // <tr><td>ja</td><td>2</td><td>200000</td><td>22504</td></tr>
    $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";

Nehmen wir nun an, der Entwickler der Anwendung ändert das Erscheinungsbild der Antwort, indem er die Simulationen nicht in einer Tabelle, sondern in einer Liste in folgender Form darstellt:

Résultat des simulations
<ul>
    <li>oui,2,200000,22504
  <li>non,2,200000,33388
</ul>  

In diesem Fall muss unser Webclient neu geschrieben werden. Das ist die ständige Gefahr, die Webclients von Anwendungen droht, die man nicht selbst kontrolliert. XML kann eine Lösung für dieses Problem bieten:

  • Anstatt HTML zu generieren, erzeugt der Simulationsdienst XML. In unserem Beispiel könnte dies lauten
<simulations>
    <entetes marie="marié" enfants="enfants" salaire="salaire" impot="impôt"/>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />
  <simulation marie="non" enfants="2" salaire="200000" impot="33388" />
</simulations>
  • Dieser Antwort könnte ein Stylesheet zugeordnet werden, das den Browsern vorgibt, wie diese Antwort visuell dargestellt werden soll: XML
  • Programmierte Web-Clients würden dieses Stylesheet ignorieren und die Informationen direkt aus dem Datenstrom der Antwort abrufen: XML

Wenn der Entwickler des Dienstes die visuelle Darstellung der von ihm bereitgestellten Ergebnisse ändern möchte, ändert er das Stylesheet und nicht den XML. Dank des Stylesheets zeigen die Browser die neue visuelle Darstellung an, und die programmierten Web-Clients müssen nicht geändert werden.