Skip to content

4. Przykłady

4.1. Aplikacja „Podatki”: Wprowadzenie

W tym miejscu przedstawiamy aplikację IMPOTS, z której będziemy wielokrotnie korzystać w dalszej części. Jest to aplikacja służąca do obliczania podatku podatnika. Rozważamy uproszczony przypadek podatnika, który ma do zadeklarowania wyłącznie swoje wynagrodzenie:

  • liczba udziałów pracownika wynosi nbParts = nbEnfants/2 + 1, jeśli jest on osobą stanu wolnego, nbEnfants/2+2, jeśli jest żonaty, gdzie nbEnfants to liczba jego dzieci.
  • jeśli ma co najmniej troje dzieci, przysługuje mu dodatkowa połowa udziału
  • oblicza się jego dochód podlegający opodatkowaniu R = 0,72 * S, gdzie S to jego roczne wynagrodzenie
  • oblicza się jego współczynnik rodzinny QF = R / nbParts
  • oblicza się jego podatek I. Rozważmy następującą tabelę:
12620,0
0
0
13190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31810
0,2
3309,5
39 970
0,25
4900
48360
0,3
6898,5
55790
0,35
9316,5
92970
0,4
12106
127860
0,45
16754,5
151250
0,50
23147,5
172 040
0,55
30710
195 000
0,60
39312
0
0,65
49062

Każdy wiersz zawiera 3 pola. Aby obliczyć podatek I, należy znaleźć pierwszy wiersz, w którym QF <= pole1. Na przykład, jeśli QF = 23000, znajdziemy wiersz

    24740        0.15        2072.5

Podatek I wynosi wówczas 0,15*R – 2072,5*nbParts. Jeśli QF jest takie, że relacja QF <= pole1 nigdy nie jest spełniona, wówczas stosuje się współczynniki z ostatniego wiersza. W tym przypadku:

    0                0.65        49062

co daje podatek I = 0,65*R – 49062*nbParts.

Dane określające poszczególne progi podatkowe znajdują się w bazie danych ODBC-MySQL. MySQL to wersja SGBD należąca do domeny publicznej, którą można wykorzystywać na różnych platformach, w tym Windows i Linux. Za pomocą tego pliku SGBD utworzono bazę danych o nazwie dbimpots, zawierającą jedną tabelę o nazwie impots. Dostęp do bazy jest kontrolowany za pomocą loginu i hasła, w tym przypadku admimpots/mdpimpots. Poniższy zrzut ekranu pokazuje, jak korzystać z bazy dbimpots przy użyciu pliku MySQL:


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

Baza danych dbimpots jest przekształcana w źródło danych ODBC w następujący sposób:

  • uruchamia się 32-bitowy menedżer źródeł danych ODBC

Image

  • używa się przycisku [Add], aby dodać nowe źródło danych ODBC

Image

  • wybieramy sterownik MySQL i wykonujemy [Terminer]
  • sterownik MySQL wymaga podania pewnych informacji:
1
nazwa DSN, jaką należy nadać źródłu danych ODBC – może być dowolna
2
komputer, na którym działa SGBD MySQL – w tym przypadku localhost. Warto zauważyć, że bazą danych może być baza zdalna. Lokalne aplikacje korzystające ze źródła danych ODBC nie zauważyłyby tego. Dotyczyłoby to w szczególności naszej aplikacji PHP.
3
baza danych MySQL, z której należy korzystać. MySQL to SGBD, który zarządza relacyjnymi bazami danych, czyli zbiorami tabel połączonych ze sobą relacjami. W tym miejscu podaje się nazwę zarządzanej bazy danych.
4
nazwa użytkownika posiadającego uprawnienia dostępu do tej bazy
5
jego hasło

4.2. Aplikacja podatkowa: klasa ImpotsDSN

Nasza aplikacja będzie oparta na klasie PHP ImpotsDSN, która będzie posiadała następujące atrybuty, konstruktor i metodę:

<?php
...
     // atrybuty
    var $limites;        // tabela limitów
    var $coeffR;        // tabela coeffR
    var $coeffN;        // tabela coeffN
    var $erreurs;        // tabela błędów
     // producent
    function ImpotsDSN($impots){...}
     // metoda
    function calculer($personne){...}
  • W opisie problemu widzieliśmy, że potrzebujemy trzech zestawów danych
12620,0
0
0
13190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31810
0,2
3309,5
39 970
0,25
4900
48360
0,3
6898,5
55790
0,35
9316,5
92970
0,4
12106
127860
0,45
16754,5
151250
0,50
23147,5
172 040
0,55
30710
195 000
0,60
39312
0
0,65
49062

Pierwsza kolumna zostanie umieszczona w atrybucie $limites klasy ImotsDSN, druga w atrybucie $coeffR, a trzecia w $coeffN. Te trzy atrybuty są inicjowane przez konstruktor klasy. Konstruktor ten pobiera dane ze źródła danych ODBC. Podczas tworzenia obiektu mogą wystąpić różne błędy. Błędy te są zgłaszane w atrybucie $erreurs w postaci tablicy komunikatów o błędach.

  • Konstruktor otrzymuje jako parametr słownik $impots zawierający następujące pola:
// dsn: nazwa DSN źródła danych ODBC zawierającego wartości tabel limitów, coeffR, coeffN
// user: nazwa użytkownika posiadającego uprawnienia do odczytu źródła ODBC
// pwd: jego hasło
// tabela: nazwa tabeli zawierającej wartości (wartości graniczne, coeffR, coeffN)
// limity: nazwa kolumny zawierającej wartości graniczne
// coeffR: nazwa kolumny zawierającej wartości coeffR       
// coeffN: nazwa kolumny zawierającej wartości coeffN

Parametr konstruktora dostarcza wszystkich informacji niezbędnych do odczytania danych ze źródła danych ODBC

  • Po utworzeniu obiektu ImpotsDSN użytkownicy klasy będą mogli wywołać metodę calculerImpots tego obiektu. Metoda ta przyjmuje jako parametr słownik $personne:
       // $osoba["marié"]: tak, nie
       // $osoba["enfants"]: liczba dzieci
       // $osoba["salaire"]: roczne wynagrodzenie

Pełna klasa wygląda następująco:

<?php
   // definicja klasy objImpots
  class ImpotsDSN{

     // atrybuty: 3 tabele danych
    var $limites;        // tabela limitów
    var $coeffR;        // tabela coeffR
    var $coeffN;        // tabela coeffN
    var $erreurs;        // tabela błędów

     // producent
    function ImpotsDSN($impots){
       // $impots: słownik zawierający następujące pola
       // dsn: nazwa DSN źródła danych ODBC zawierającego wartości tabel limitów, coeffR, coeffN
       // user: nazwa użytkownika posiadającego uprawnienia do odczytu źródła ODBC
       // pwd: jego hasło
       // tabela: nazwa tabeli zawierającej wartości (wartości graniczne, coeffR, coeffN)
       // limity: nazwa kolumny zawierającej wartości graniczne
       // coeffR: nazwa kolumny zawierającej wartości coeffR       
       // coeffN: nazwa kolumny zawierającej wartości coeffN

       // początkowo brak błędów
      $this->erreurs=array();

             // weryfikacja wywołania
      if (! isset($impots[dsn]) || ! isset($impots[user]) ||! isset($impots[pwd]) ||! isset($impots[table]) ||
          ! isset($impots[limites]) ||! isset($impots[coeffR]) ||! isset($impots[coeffN])){
         // błąd
        $this->erreurs[]="Appel incorrect";
         // koniec
        return;
      }//if

       // otwarcie bazy danych DSN
      $connexion=odbc_connect($impots[dsn],$impots[user],$impots[pwd]);
       // błąd?
      if(! $connexion){
         // błąd
        $this->erreurs[]="Impossible d'ouvrir la base DSN [$impots[dsn]] (".odbc_error().")";
         // koniec
        return;
      }//if

             // wysłanie zapytania do bazy danych
      $requête=odbc_prepare($connexion,"select $impots[limites],$impots[coeffR],$impots[coeffN] from $impots[table]");
            if(! odbc_execute($requête)){
         // błąd
        $this->erreurs[]="Impossible d'expoiter la base DSN [$impots[dsn]] (".odbc_error().")";
         // koniec
        odbc_close($connexion);
        return;
      }//if

       // przetwarzanie wyników zapytania
      $this->limites=array();
      $this->coeffR=array();
      $this->coeffN=array();
      while(odbc_fetch_row($requête)){     
          // kolejny wiersz
        $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

       // zamknięcie bazy
      odbc_close($connexion);
    }//konstruktor                

    // --------------------------------------------------------------------------
    function calculer($personne){
       // $osoba["marié"]: tak, nie
       // $osoba["enfants"]: liczba dzieci
       // $osoba["salaire"]: roczne wynagrodzenie

       // czy obiekt jest w prawidłowym stanie?
      if (! is_array($this->erreurs) || count($this->erreurs)!=0) return -1;

            // weryfikacja wywołania
      if (! isset($personne[marié]) || ! isset($personne[enfants]) ||! isset($personne[salaire])){
         // błąd
        $this->erreurs[]="Appel incorrect";
         // koniec
        return -1;
      }//if

       // czy parametry są poprawne?
      $personne[marié]=strtolower($personne[marié]);
      if($personne[marié]!=oui && $personne[marié]!=non){
           // błąd
        $this->erreurs[]="Statut marital [$personne[marié]] incorrect";
      }//if
      if(! preg_match("/^\s*\d{1,3}\s*$/",$personne[enfants])){
          // błąd
        $this->erreurs[]="Nombre d'enfants [$personne[enfants]] incorrect";
      }//if
      if(! preg_match("/^\s*\d+\s*$/",$personne[salaire])){
          // błąd
        $this->erreurs[]="salaire [$personne[salaire]] incorrect";
      }//if
       // błędy?
      if(count($this->erreurs)!=0) return -1;

      // liczba porcji
      if($personne[marié]==oui) $nbParts=$personne[enfants]/2+2;
        else $nbParts=$personne[enfants]/2+1;
       // o pół udziału więcej, jeśli jest co najmniej troje dzieci
      if($personne[enfants]>=3) $nbParts+=0.5;
       // dochód podlegający opodatkowaniu
      $revenuImposable=0.72*$personne[salaire];
       // współczynnik rodzinny
      $quotient=$revenuImposable/$nbParts;
       // jest umieszczany na końcu tabeli limitów w celu zatrzymania następującej po niej pętli
      $this->limites[$this->nbLimites]=$quotient;
       // obliczenie podatku
      $i=0;
      while($quotient>$this->limites[$i]) $i++;
      // ponieważ umieszczono $quotient na końcu tablicy $limites, poprzednia pętla
       // nie może wykraczać poza tablicę $limites
       // teraz można obliczyć podatek
      return floor($revenuImposable*$this->coeffR[$i]-$nbParts*$this->coeffN[$i]);
    }//calculImpots
  }//klasa podatków
?>

Program testowy mógłby wyglądać następująco:

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

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

  $objImpots=new ImpotsDSN($conf);

  // błędy?
  if(count($objImpots->erreurs)!=0){
       // komunikat
    echo "Les erreurs suivantes se sont produites :\n";
    $erreurs=$objImpots->erreurs;
    for($i=0;$i<count($erreurs);$i++){
        echo "$erreurs[$i]\n";
    }//for
    // koniec
    exit(1);
  }//if

   // próby
  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());

  // koniec
  exit(0);

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

       // obliczenie podatku
    $personne=array(marié=>$marié,enfants=>$enfants,salaire=>$salaire);        
      $montant=$objImpots->calculer($personne);
     // błędy?
    if(count($objImpots->erreurs)!=0){
         // komunikat
      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
  • program testowy „dołącza” plik zawierający klasę ImpotsDSN
  • następnie tworzy obiekt objImpots klasy ImpotsDSN, przekazując konstruktorowi obiektu niezbędne informacje
  • po utworzeniu tego obiektu jego metoda calculer zostanie wywołana pięć razy

Otrzymane wyniki są następujące:


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

Od tej pory będziemy korzystać z klasy ImpotsDSN bez ponownego podawania jej definicji.

4.3. Aplikacja Podatki: wersja 1

Przedstawiamy teraz wersję 1 aplikacji IMPOTS. Rozważmy sytuację, w której aplikacja internetowa wyświetla użytkownikowi interfejs HTML w celu uzyskania trzech parametrów niezbędnych do obliczenia podatku:

  • stan cywilny (żonaty/zamężna lub nie)
  • liczba dzieci
  • roczne wynagrodzenie

Image

Formularz jest wyświetlany przez następującą stronę PHP:

<?php //formularz podatkowy ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
           // w formularzu
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//z
      }//usunąć      
        </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
         // czy są jakieś błędy?
      if(count($requête->erreurs)!=0){
           // wyświetlanie błędów
        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>

Strona PHP ogranicza się do wyświetlania informacji przekazanych jej przez główny program aplikacji w zmiennej $requête, która jest obiektem zawierającym następujące pola:

chkoui, chknon
atrybuty przycisków opcji „tak” i „nie” – mogą przyjmować wartości "checked" lub "", aby włączyć lub wyłączyć odpowiedni przycisk opcji
enfants
liczba dzieci podatnika
salaire
jego roczne wynagrodzenie
impots
kwota podatku do zapłaty
erreurs
ewentualna lista błędów – może być pusta.

Strona wysłana do klienta zawiera skrypt JavaScript z funkcją effacer powiązaną z przyciskiem „Effacer”, której zadaniem jest przywrócenie formularza do stanu początkowego: przycisk niezaznaczony, pola wprowadzania danych puste. Należy zauważyć, że tego rezultatu nie można było uzyskać przy użyciu przycisku HTML typu „reset”. W rzeczywistości, gdy stosowany jest ten typ przycisku, przeglądarka przywraca formularz do stanu, w jakim go otrzymała. Natomiast w naszej aplikacji przeglądarka otrzymuje formularze, które mogą nie być puste.

Aplikacja przetwarzająca powyższy formularz wygląda następująco:

<?php
     // przetwarza formularz podatkowy

     // biblioteki
  include "ImpotsDSN.php";

  // konfiguracja aplikacji
  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);

   // pobieranie parametrów
  $requête->marié=$_POST["optMarie"];
  $requête->enfants=$_POST["txtEnfants"];
  $requête->salaire=$_POST["txtSalaire"];

   // czy mamy wszystkie parametry?
  if(! isset($requête->marié) || ! isset($requête->enfants) || ! isset($requête->salaire)){
       // przygotowanie pustego formularza
    $requête->chkoui="";
    $requête->chknon="checked";
    $requête->enfants="";
    $requête->salaire="";
    $requête->impots="";
    $requête->erreurs=array();
     // wyświetlanie formularza
    include $formulaireImpots;
     // koniec
    exit(0);
  }//if

   // sprawdzanie parametrów
  $requête=vérifier($requête);

   // czy są błędy?
  if(count($requête->erreurs)!=0){
       // wyświetlanie formularza
    include "$formulaireImpots";
     // koniec
    exit(0);
  }//if

   // obliczanie odpowiedzi
  $requête=calculerImpots($bdImpots,$requête);

   // czy są jakieś błędy?
  if(count($requête->erreurs)!=0){
       // wyświetlanie formularza błędów
    include "$erreursImpots";
     // koniec
    exit(0);
  }//if

   // wyświetlanie formularza
  include "$formulaireImpots";
   // koniec
  exit(0);

  // --------------------------------------------------------
  function vérifier($requête){
       // sprawdza poprawność parametrów zapytania

     // na początku brak błędów
    $requête->erreurs=array();

     // status prawidłowy?
    $requête->marié=strtolower($requête->marié);
    if($requête->marié!="oui" && $requête->marié!="non"){
         // błąd
      $requête->erreurs[]="Statut marital [$requête->marié] incorrect";
    }

     // czy liczba elementów podrzędnych jest prawidłowa?
    if(! preg_match("/^\s*\d+\s*$/",$requête->enfants)){
        // błąd
      $requête->erreurs[]="Nombre d'enfants [$requête->enfants] incorrect";
    }

     // płaca prawidłowa?
    if(! preg_match("/^\s*\d+\s*$/",$requête->salaire)){
        // błąd
      $requête->erreurs[]="Salaire [$requête->salaire] incorrect";
    }

     // stan przycisków opcji
    if($requête->marié=="oui"){
        $requête->chkoui="checked";
      $requête->chknon="";
    }else{
        $requête->chknon="checked";
      $requête->chkoui="";
    }

     // wysyłamy zapytanie
    return $requête;
  }//sprawdź

  // --------------------------------------------------------
  function calculerImpots($bdImpots,$requête){
       // obliczanie kwoty podatku

     // $bdImpots: słownik zawierający informacje niezbędne do odczytania źródła danych ODBC
     // $requête: zapytanie zawierające informacje o podatniku

     // tworzy się obiekt ImpotsDSN 
    $objImpots=new ImpotsDSN($bdImpots);

    // czy są jakieś błędy?
    if(count($objImpots->erreurs)!=0){
         // błędy umieszczamy w zapytaniu
      $requête->erreurs=$objImpots->erreurs;
      // gotowe
      return $requête;
    }//if

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

     // zwracamy wynik
    return $requête;
  }//calculerImpots

Uwagi:

  • po pierwsze, dołączono klasę ImpotsDSN. To właśnie ona stanowi podstawę obliczeń podatkowych i zasłania nam dostęp do bazy danych
  • wykonuje się pewną liczbę operacji inicjalizacyjnych mających na celu ułatwienie konserwacji aplikacji. Jeśli parametry ulegną zmianie, należy zmienić ich wartości w tej sekcji. Zazwyczaj te wartości konfiguracyjne są raczej przechowywane w pliku lub bazie danych.
  • Pobieramy trzy parametry formularza odpowiadające polom HTML, optMarie, txtEnfants oraz txtSalaire.
  • Parametry te mogą być całkowicie lub częściowo nieobecne. Dzieje się tak zwłaszcza przy pierwszym wywołaniu formularza. W takim przypadku wysyłany jest po prostu pusty formularz.
  • Następnie sprawdzana jest poprawność trzech pobranych parametrów. Jeśli okażą się nieprawidłowe, formularz jest odsyłany w postaci, w jakiej został wprowadzony, ale z dołączoną listą błędów.
  • Po sprawdzeniu poprawności trzech parametrów można przystąpić do obliczenia podatku. Jest on obliczany przez funkcję calculerImpots. Funkcja ta tworzy obiekt typu ImpotsDSN i wykorzystuje metodę calculer tego obiektu.
  • Utworzenie obiektu ImpotsDSN może się nie powieść, jeśli baza danych jest niedostępna. W takim przypadku aplikacja wyświetla specjalną stronę błędu wskazującą, jakie błędy wystąpiły.
  • Jeśli wszystko przebiegło pomyślnie, formularz jest zwracany w postaci, w jakiej został wypełniony, ale z dodaną kwotą podatku do zapłaty.

Strona błędów wygląda następująco:

<?php    // strona błędu ?>
<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
           // wyświetlanie błędów
        for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

Oto kilka przykładów błędów. Po pierwsze, podano nieprawidłowe hasło:

Image

W formularzu podano nieprawidłowe dane:

Image

Wreszcie podano prawidłowe wartości:

Image

4.4. Aplikacja Podatki: wersja 2

W poprzednim przykładzie poprawność parametrów txtEnfants i txtSalaire formularza jest sprawdzana przez serwer. W tym przypadku proponujemy sprawdzenie ich za pomocą skryptu JavaScript umieszczonego na stronie formularza. Wówczas to przeglądarka dokonuje weryfikacji parametrów. Serwer jest angażowany tylko wtedy, gdy parametry są prawidłowe. Pozwala to zaoszczędzić „przepustowość”. Strona wyświetlająca impots-form.php wygląda następująco:

<?php //formularz podatkowy ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
........
      }//wyczyść

      //------------------------------
      function calculer(){
           // sprawdzanie parametrów przed wysłaniem ich na serwer
        with(document.frmImpots){
          //liczba dzieci
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // szablon nie został zweryfikowany
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
           //wynagrodzenie
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // szablon nie został zweryfikowany
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//jeśli
          // w porządku – wysyłamy formularz na serwer
          submit();
        }//with
      }//obliczyć            
        </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>

Należy zwrócić uwagę na następujące zmiany:

  • przycisk Calculer nie jest już typu submit, lecz typu button powiązanego z funkcją o nazwie calculer. To właśnie ta funkcja przeprowadzi analizę pól txtEnfants i txTsalaire. Jeśli są one poprawne, wartości z formularza zostaną wysłane na serwer (submit), w przeciwnym razie wyświetlony zostanie komunikat o błędzie.

Oto przykładowy komunikat wyświetlany w przypadku błędu:

Image

4.5. Aplikacja podatkowa: wersja 3

Nieznacznie modyfikujemy aplikację, aby wprowadzić do niej pojęcie sesji. Zakładamy teraz, że aplikacja służy do symulacji obliczeń podatkowych. Użytkownik może w ten sposób symulować różne „konfiguracje” podatnika i sprawdzić, jaka byłaby kwota podatku do zapłaty w każdym z tych przypadków. Poniższa strona internetowa przedstawia przykładowy wynik:

Image

Kod wyświetlający powyższą stronę wygląda następująco:

<?php //formularz podatkowy ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
...
      }//usunąć

      //------------------------------
      function calculer(){
...
      }//obliczyć            
        </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
         // czy są jakieś błędy?
      if(count($requête[erreurs])!=0){
           // wyświetlanie błędów
        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 
         // czy są symulacje?
      else if(count($requête[simulations])!=0){
          // wyświetlanie symulacji
        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>

Program wyświetlający wykazuje niewielkie różnice w stosunku do poprzedniej wersji:

  • słownik otrzymuje słownik $requête zamiast obiektu $requête. Należy zatem wpisać $requête[enfants], a nie $requête->dzieci.
  • Słownik ten posiada pole o nazwie simulations, które jest tablicą dwuwymiarową. $requête[simulations][i] to symulacja nr i. Jest to tablica składająca się z czterech ciągów znaków: stan cywilny, liczba dzieci, roczne wynagrodzenie, podatek do zapłaty.

Program główny uległ natomiast bardziej znaczącej zmianie:

<?php
     // przetwarza formularz podatkowy

     // biblioteki
  include "ImpotsDSN.php";

   // rozpoczęcie sesji
  session_start();

   // konfiguracja aplikacji
  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);

     // pobieranie parametrów sesji
  $session=$_SESSION["session"];
   // czy sesja jest ważna?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // rozpoczynamy nową sesję
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // czy wystąpiły błędy?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // wyświetlanie strony błędów
        include $erreursImpots;
       // koniec
      $session=array();
      terminerSession($session);
    }//if
  }//if

  // pobieramy parametry bieżącej wymiany
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // czy mamy wszystkie parametry?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // wyświetlenie pustego formularza
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // koniec
    terminerSession($session);
  }//if

   // sprawdzanie parametrów
  $requête=vérifier($requête);

   // czy są jakieś błędy?
  if(count($requête[erreurs])!=0){
       // wyświetlenie formularza
    include "$formulaireImpots";
     // koniec
    terminerSession($session);
  }//if

   // obliczenie podatku do zapłaty
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));

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

   // wyświetlenie formularza
  include "$formulaireImpots";

   // koniec
  terminerSession($session);

  // --------------------------------------------------------
  function vérifier($requête){
       // sprawdza poprawność parametrów zapytania

     // na początku brak błędów
    $requête[erreurs]=array();

     // status prawidłowy?
    $requête[marié]=strtolower($requête[marié]);
    if($requête[marié]!="oui" && $requête[marié]!="non"){
         // błąd
      $requête[erreurs][]="Statut marital [$requête[marié]] incorrect";
    }

     // czy liczba elementów podrzędnych jest prawidłowa?
    if(! preg_match("/^\s*\d+\s*$/",$requête[enfants])){
        // błąd
      $requête[erreurs][]="Nombre d'enfants [$requête[enfants]] incorrect";
    }

     // płaca prawidłowa?
    if(! preg_match("/^\s*\d+\s*$/",$requête[salaire])){
        // błąd
      $requête[erreurs][]="Salaire [$requête[salaire]] incorrect";
    }

     // stan przycisków opcji
    if($requête[marié]=="oui"){
        $requête[chkoui]="checked";
      $requête[chknon]="";
    }else{
        $requête[chknon]="checked";
      $requête[chkoui]="";
    }

     // wysyłamy zapytanie
    return $requête;
  }//sprawdź

  // --------------------------------------------------------
  function terminerSession($session){
       // zapisywanie sesji
    $_SESSION[session]=$session;
     // koniec skryptu
    exit(0);
  }//terminerSession
  • Po pierwsze, ta aplikacja obsługuje sesję. Sesję uruchamia się więc na początku skryptu:
  session_start();
  • Sesja przechowuje tylko jedną zmienną: słownik $session. Słownik ten posiada dwa pola:
    • objImpots: obiekt ImpotsDSN. Obiekt ten jest zapisywany w sesji klienta, aby uniknąć niepotrzebnych kolejnych dostępów do bazy danych ODBC.
    • symulacje: tablica symulacji służąca do zapamiętywania symulacji poprzednich transakcji między kolejnymi transakcjami.
  • Po uruchomieniu sesji pobierana jest zmienna $session. Jeśli zmienna ta jeszcze nie istnieje, oznacza to, że sesja właśnie się rozpoczyna. Wówczas tworzony jest obiekt ImpotsDSN oraz pusta tablica symulacji. W razie potrzeby zgłaszane są błędy.
<?php
...
     // pobieramy parametry sesji
  $session=$_SESSION["session"];
   // czy sesja jest ważna?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // rozpoczynamy nową sesję
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // czy wystąpiły błędy?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // wyświetlanie strony błędów
        include $erreursImpots;
       // koniec
      $session=array();
      terminerSession($session);
    }//if
  }//if
  • Po pomyślnym zainicjowaniu sesji pobierane są parametry wymiany. Jeśli nie ma wszystkich oczekiwanych parametrów, wysyłany jest pusty formularz.
<?php
...
   // pobieramy parametry bieżącej wymiany
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // czy mamy wszystkie parametry?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // wyświetlenie pustego formularza
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // koniec
    terminerSession($session);
  }//if
  • Jeśli wszystkie parametry są poprawne, zostają one zweryfikowane. W przypadku wystąpienia błędów są one zgłaszane:
<?php
...
   // sprawdzanie parametrów
  $requête=vérifier($requête);

   // czy są jakieś błędy?
  if(count($requête[erreurs])!=0){
       // wyświetlenie formularza
    include "$formulaireImpots";
     // koniec
    terminerSession($session);
  }//if
  • jeśli parametry są prawidłowe, podatek jest obliczany:
<?php
...
   // obliczenie podatku do zapłaty
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));
  • trwająca symulacja zostaje dodana do tabeli symulacji, a ta jest wysyłana do klienta wraz z formularzem:
<?php
...
   // kolejna symulacja
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // wyświetlenie formularza
  include "$formulaireImpots";

   // koniec
  terminerSession($session);
  • W każdym przypadku skrypt kończy się zapisaniem zmiennej $session w sesji. Odbywa się to w procedurze terminerSession.
<?php
...

  function terminerSession($session){
       // zapisywanie sesji
    $_SESSION[session]=$session;
     // koniec skryptu
    exit(0);
  }//terminerSession

Na zakończenie przedstawimy program wyświetlający błędy dostępu do bazy danych (impots-erreurs.php):

<?php    // strona błędu ?>
<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
           // wyświetlanie błędów
        for($i=0;$i<count($requête[erreurs]);$i++){
            echo "<li>".$requête[erreurs][$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

Również w tym przypadku obiekt $requête został zastąpiony słownikiem $requête.

4.6. Aplikacja podatkowa: wersja 4

Teraz stworzymy samodzielną aplikację, która będzie klientem internetowym poprzedniej aplikacji internetowej impots. Aplikacja będzie aplikacją konsolową uruchamianą z okna DOS:


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

Parametry klienta internetowego cltImpots są następujące:

     // składnia $0 urlImpots żonaty dzieci wynagrodzenie token
   // klient serwisu podatkowego działającego na urlImpots
   // przesyła do tej usługi trzy informacje: stan cywilny, dzieci, wynagrodzenie
   // ewentualnie wraz z tokenem sesji, jeśli został przekazany
   // wyświetla tabelę symulacji dla bieżącej sesji

Oto kilka przykładów zastosowania. Najpierw pierwszy przykład bez tokenu sesji.

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

Jeton de session=[a6297317667bc981c462120987b8dd18]

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

Prawidłowo pobraliśmy kwotę należnego podatku (22504 f). Pobraliśmy również token sesji. Teraz możemy go wykorzystać do drugiego zapytania:

dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php tak 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]

Otrzymujemy tablicę symulacji przesłanych przez serwer WWW. Pobany token pozostaje oczywiście ten sam. Jeśli wywołamy usługę internetową, gdy baza danych nie została uruchomiona, otrzymujemy następujący wynik:

dos>e:\php43\php.exe cltImpots2.php http://localhost/poly/impots/6/impots.php tak 3 2 000 000

Jeton de session=[8369014d5053212bc42f64bbdfb152ee]

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

Podczas tworzenia programowego klienta internetowego konieczna jest dokładna wiedza na temat tego, co serwer wysyła w odpowiedzi na różne możliwe żądania klienta. Serwer wysyła zestaw wierszy HTML zawierający informacje przydatne oraz inne, które służą wyłącznie do formatowania strony HTML. Wyrażenia regularne PHP mogą pomóc nam w odnalezieniu przydatnych informacji w strumieniu wierszy wysyłanych przez serwer. W tym celu musimy znać dokładny format poszczególnych odpowiedzi serwera. Możemy to sprawdzić, wysyłając zapytanie do serwisu internetowego za pomocą przeglądarki i przeglądając wysłany kod źródłowy. Należy zauważyć, że metoda ta nie pozwala na poznanie nagłówków HTTP zawartych w odpowiedzi serwera internetowego. Czasami warto je znać. W takim przypadku można skorzystać z jednego z dwóch ogólnych klientów internetowych omówionych w poprzednim rozdziale.

Jeśli powtórzymy poprzednie przykłady, oto kod HTML otrzymany przez przeglądarkę dla tabeli symulacji:

<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>

Jest to tabela o nazwie HTML, jedyna w przesłanym dokumencie. Wyrażenie regularne „|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|” powinno zatem umożliwiać odnalezienie w dokumencie różnych otrzymanych symulacji. Gdy baza danych jest niedostępna, otrzymujemy następujący 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>

Aby odzyskać błąd, klient internetowy będzie musiał wyszukać w odpowiedzi serwera internetowego wiersze odpowiadające wzorcowi: „|<li>(.+?)</li>|”.

Mamy teraz wytyczne dotyczące tego, co należy zrobić, gdy użytkownik zażąda obliczenia podatku z poziomu poprzedniego klienta konsolowego:

  • sprawdzić, czy wszystkie parametry są prawidłowe, i w razie potrzeby zgłosić błędy.
  • nawiązać połączenie z URL podanym jako pierwszy parametr. W tym celu należy postępować zgodnie z wzorem ogólnego klienta internetowego, który został już przedstawiony i omówiony
  • w strumieniu odpowiedzi serwera, użyjmy wyrażeń regularnych, aby:
    • znalezienie komunikatów o błędach
    • znaleźć wyniki symulacji

Kod klienta cltImpots.php jest następujący:

<?php
     // składnia $0 urlImpots żonaty dzieci wynagrodzenie token
   // klient serwisu podatkowego działającego na platformie urlImpots
   // przesyła do tej usługi trzy informacje: stan cywilny, dzieci, wynagrodzenie
   // ewentualnie wraz z tokenem sesji, jeśli został przekazany
   // wyświetla tabelę symulacji dla bieżącej sesji

   // sprawdza liczbę argumentów
  if(count($argv)!=5 && count($argv)!=6){
       // błąd
    fwrite(STDERR,"Syntaxe $argv[0] urlImpots marié enfants salaire [jeton]\n");
     // koniec
    exit(1);
  }//if

   // odnotowuje się URL
  $urlImpots=analyseURL($argv[1]);
  if(isset($urlImpots[erreur])){
       // błąd
    fwrite(STDERR,"$urlImpots[erreur]\n");
     // koniec
    exit(1);
  }//if

   // analiza żądania użytkownika
  $demande=analyseDemande("$argv[2],$argv[3],$argv[4]");
   // błędy?
  if($demande[erreur]){
       // komunikat
    echo "$demande[erreur]\n";
     // koniec
    exit(1);
  }//if

   // można wysłać żądanie – używamy tokenu sesji
  $impots=getImpots($urlImpots,$demande,$argv[5]);

   // wyświetlamy token sesji
  echo "Jeton de session=[$impots[jeton]]\n";

   // czy są jakieś błędy?
  if(count($impots[erreurs])!=0){
       //komunikaty o błędach
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//dla
  }else{
       // brak błędów – wyświetlanie symulacji
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//for
  }//if

   // koniec programu
  exit(0);

  // --------------------------------------------------------------
  function analyseURL($URL){  
       // analiza poprawności pliku URL $URL
    $url=parse_url($URL);
    // protokół    
    if(strtolower($url[scheme])!="http"){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // urządzenie
    if(! isset($url[host])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // port
    if(! isset($url[port])) $url[port]=80;
     // żądanie
    if(isset($url["query"])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//if
     // powrót
    return $url;
  }//analyseURL

  // -----------------------------------------------------------
  function analyseDemande($demande){
       // $demande: ciąg do analizy
     // musi mieć postać: żonaty, dzieci, wynagrodzenie
    // poprawny format
    if(! preg_match("/^\s*(oui|non)\s*,\s*(\d{1,3})\s*,\s*(\d+)\s*$/i",$demande,$champs)){
        // nieprawidłowy format
      return(array(erreur=>"Format (marié, enfants, salaire) invalide."));
    }    
     // w porządku
    $marié=strtolower($champs[1]);
    return array(marié=>$marié,enfants=>$champs[2],salaire=>$champs[3]);
  }//analyseDemande

  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: należy sprawdzić URL
     // $demande: słownik zawierający pola „mąż”, „dzieci”, „wynagrodzenie”
     // $jeton: ewentualny token sesji

     // nawiązanie połączenia na porcie $urlImpots[port] z $urlImpots[host]
    $connexion=fsockopen($urlImpots[host],$urlImpots[port],&$errno,&$erreur);
     // powrót w przypadku błędu
    if(! $connexion){
      return array(erreurs=>array("Echec de la connexion au site ($urlImpots[host],$urlImpots[port]) : $erreur"));
    }//if

         // wysyłamy nagłówki HTTP do serwera
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));

         // odczytujemy odpowiedź serwera – najpierw nagłówki HTTP
     // pierwszy wiersz
    $ligne=fgets($connexion,10000);

    // URL znaleziono?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
        // URL niedostępne – zwracany jest błąd
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//if

     // odczytujemy pozostałe nagłówki HTTP
    while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
       // wyszukiwanie tokenu, jeśli jeszcze go nie znaleziono      
      if(! $jeton){
           // wyszukiwanie wiersza „set-cookie”
        if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
            // znaleziono token – zapisujemy go          
          $jeton=$champs[1];            
        }//if
      }//if
    }//następna linia

     // odczytujemy następujący dokument
    $document="";
    while($ligne=fread($connexion,10000)){
        $document.=$ligne;
    }//while

     // zamyka się połączenie
    fclose($connexion);

     // analizujemy dokument
    $impots=getInfos($document);

     // zwracamy wynik
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
    }//getImpots

  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: połączenie z serwerem WWW
     // $url: URL do zapytania
     // $paramètres: słownik parametrów do wysłania

     // przygotowujemy plik POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//while
     // usuwamy ostatni znak
    $post=substr($post,0,-1);

     // przygotowujemy żądanie HTTP w formacie HTTP/1.0
    $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;

    // wysyła się żądanie HTTP
    fwrite($connexion,$HTTP);
    }//POST

  // --------------------------------------------------------------
  function getInfos($document){
       // $document: dokument HTML
     // szukamy albo listy błędów
     // albo tabeli symulacji

     // przygotowanie wyniku
    $impots[erreurs]=array();
    $impots[simulations]=array();

     // czy wystąpił błąd?
     // szablon wiersza błędu
    $modErreur="|<li>(.+?)</li>|";
     // wyszukiwanie w dokumencie
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // pobieranie błędów
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//dla
       // zakończone
      return $impots;
    }//jeśli

     // przykładowy wiersz z tabeli symulacji
     // <tr><td>tak</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>|";
    // wyszukiwanie w dokumencie
    if(preg_match_all($modSimulation,$document,$champs,PREG_SET_ORDER)){    
         // pobieranie symulacji
      for($i=0;$i<count($champs);$i++){
          $impots[simulations][]=array($champs[$i][1],$champs[$i][2],$champs[$i][3],$champs[$i][4]);
      }//dla
       // zwrot wyniku
      return $impots;
    }//if

    // nie jest normalne, że się tu znalazłem
    return $impots;
    }//getInfos
?>

Uwagi:

  • program rozpoczyna się od sprawdzenia poprawności otrzymanych parametrów. Wykorzystuje w tym celu dwie funkcje: analyseURL, która sprawdza poprawność parametru URL, oraz analyseDemande, która sprawdza pozostałe parametry.
  • Obliczenie podatku odbywa się za pomocą funkcji getImpots:
  $impots=getImpots($urlImpots,$demande,$argv[5]);
  • Funkcja getImpots jest zadeklarowana w następujący sposób:
<?php
...
  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: należy sprawdzić URL
     // $demande: słownik zawierający pola „mąż”, „dzieci”, „wynagrodzenie”
     // $jeton: ewentualny token sesji
  • (ciąg dalszy)
    • $urlImpots to słownik zawierający następujące pola:
      • host: komputer, na którym znajduje się usługa internetowa
      • port: port usługi
      • path: ścieżka do żądanego zasobu
    • $demande to słownik zawierający następujące pola:
      • marié: tak/nie: stan cywilny
      • dzieci: liczba dzieci
      • wynagrodzenie: roczne wynagrodzenie
    • $jeton to token sesji.

Funkcja zwraca słownik zawierający następujące pola:

  • błędy: tablica komunikatów o błędach
  • symulacje: tablica symulacji, przy czym każda symulacja jest z kolei tablicą zawierającą cztery elementy (stan cywilny, dzieci, wynagrodzenie, podatek)
  • token: token sesji
  • Po uzyskaniu wyniku funkcji getImpots program wyświetla wyniki i kończy działanie:
<?php
...

   // wyświetla się token sesji
  echo "Jeton de session=[$impots[jeton]]\n";

   // czy są jakieś błędy?
  if(count($impots[erreurs])!=0){
       //komunikaty o błędach
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//dla
  }else{
       // brak błędów – wyświetlanie symulacji
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//for
  }//if

   // koniec programu
  exit(0);
  • Przeanalizujmy teraz funkcję getImpots($urlImpots,$demande,$jeton), która ma
    • utworzyć połączenie TCP-IP na porcie $urlImpots[port] maszyny $urlImpots[host]
    • wysłać do serwera WWW nagłówki HTTP, których oczekuje, w szczególności token sesji, jeśli taki istnieje
    • wysłać do serwera WWW żądanie o nagłówku POST wraz z parametrami zawartymi w słowniku $demande
    • przeanalizować odpowiedź serwera WWW w celu znalezienia w niej albo listy błędów, albo tabeli symulacji.
  • Wysyłanie nagłówków HTTP odbywa się za pomocą funkcji POST:
<?php
...
         // wysyłamy nagłówki HTTP na serwer
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));
  • Po wysłaniu nagłówków HTTP funkcja getImpots odczytuje całą odpowiedź serwera i umieszcza ją w $document. Odpowiedź jest odczytywana bez analizy, z wyjątkiem pierwszego wiersza, który pozwala nam ustalić, czy serwer WWW znalazł żądany plik URL. Jeśli plik URL został znaleziony, serwer WWW odpowiada HTTP/1.X 200 OK, gdzie X zależy od używanej wersji pliku HTTP.
<?php
...
    // znaleziono URL?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
 <?php
...
       // URL niedostępny – powrót z błędem
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//if
  • dokument jest analizowany za pomocą funkcji getInfos:
<?php
...
     // analizujemy dokument
    $impots=getInfos($document);
  • Wynikiem jest słownik zawierający dwa pola:
    • błędy: lista błędów – może być pusta
    • symulacje: lista symulacji – może być pusta
  • Po wykonaniu tej operacji funkcja getImpots może zwrócić wynik w postaci słownika.
<?php
...
     // zwracamy wynik
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
  • Przeanalizujmy teraz funkcję POST:
<?php
...
  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: połączenie z serwerem internetowym
     // $url: URL, do którego należy wysłać zapytanie
     // $paramètres: słownik parametrów do wysłania

     // przygotowujemy plik POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//while
     // usuwamy ostatni znak
    $post=substr($post,0,-1);

     // przygotowujemy żądanie HTTP w formacie HTTP/1.0
    $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;

    // wysyłamy żądanie HTTP
    fwrite($connexion,$HTTP);
    }//POST

Chociaż w ramach programu mieliśmy już okazję zażądać zasobu internetowego za pomocą kodu GET, nie mieliśmy jeszcze okazji zrobić tego za pomocą kodu POST. Kod POST różni się od kodu GET sposobem wysyłania parametrów do serwera. Musi on wysyłać następujące nagłówki HTTP:

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

wraz z

  • chemin: ścieżka do żądanego zasobu internetowego, w tym przypadku $url[path]
  • HTTP/1.X: żądany protokół HTTP. W tym przypadku wybrano HTTP/1.0, aby otrzymać odpowiedź w jednym bloku. HTTP/1.1 pozwala na wysłanie odpowiedzi w kilku blokach (chunked).
  • N określa liczbę znaków, które klient zamierza wysłać do serwera

N znaków tworzących parametry zapytania jest wysyłanych bezpośrednio po pustej linii, która kończy nagłówki HTTP wysyłane do serwera. Parametry te mają postać param1=val1&param2=val2&..., gdzie parami to nazwa parametru, a vali to jego wartość. Dopuszczalne wartości mogą zawierać „przeszkadzające” znaki, takie jak spacje, znak &, znak = itp. Znaki te należy zastąpić ciągiem %XX, gdzie XX to ich kod szesnastkowy. Funkcja PHP urlencode wykonuje to zadanie. Odwrotną operację wykonuje funkcja urldecode.

Na koniec należy zauważyć, że jeśli posiadamy token, jest on wysyłany z nagłówkiem HTTP Cookie: PHPSESSID=token.

  • Pozostaje nam jeszcze przeanalizować funkcję, która analizuje odpowiedź serwera:
<?php
...
  // --------------------------------------------------------------
  function getInfos($document){
       // $document: dokument HTML
     // szukamy albo listy błędów
     // albo tabeli symulacji

     // przygotowanie wyniku
    $impots[erreurs]=array();
    $impots[simulations]=array();
  • Należy pamiętać, że błędy są wysyłane przez serwer w postaci <li>komunikat o błędzie</li>. Wyrażenie regularne „|<li>(.+?)</li>|” powinno umożliwić pobranie tych informacji. W tym przypadku użyliśmy znaku | do wyodrębnienia wyrażenia regularnego zamiast znaku /, ponieważ ten ostatni występuje również w samym wyrażeniu regularnym. Funkcja preg_match_all ($modèle, $document, $champs, PREG_SET_ORDER) pozwala pobrać wszystkie wystąpienia $modèle znalezione w $document. Są one umieszczane w tablicy $champs. Zatem $champs[i] reprezentuje wystąpienie nr i elementu $modèle w $document. W tabeli $champs[$i][0] umieszczany jest ciąg znaków odpowiadający wzorcowi. Gdyby zawierał on nawiasy, ciąg znaków odpowiadający pierwszemu nawiasowi umieszczany jest w pliku $champs[$i][1], drugi w $champs[$i][2] i tak dalej. Kod służący do pobrania błędów będzie zatem następujący:
<?php
...

     // czy wystąpił błąd?
     // szablon wiersza błędu
    $modErreur="|<li>(.+?)</li>|";
     // wyszukiwanie w dokumencie
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // pobieranie błędów
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//for
       // koniec
      return $impots;
    }//if

4.7. Aplikacja podatkowa: Podsumowanie

Przedstawiliśmy różne wersje naszej aplikacji klient-serwer do obliczania podatków:

  • wersja 1: usługa jest obsługiwana przez zestaw programów PHP, a klientem jest przeglądarka. Przeprowadza ona tylko jedną symulację i nie zapamiętuje poprzednich.
  • wersja 2: dodano kilka funkcji po stronie przeglądarki poprzez umieszczenie skryptów JavaScript w dokumencie HTML ładowanym przez przeglądarkę. Skrypt ten sprawdza poprawność parametrów formularza.
  • wersja 3: umożliwiono usłudze zapamiętywanie różnych symulacji przeprowadzonych przez klienta poprzez zarządzanie sesją. Interfejs HTML został odpowiednio zmodyfikowany, aby je wyświetlać.
  • wersja 4: klient jest teraz samodzielną aplikacją konsolową. Pozwala nam to powrócić do tworzenia programowanych klientów internetowych.

W tym miejscu można sformułować kilka uwag:

  • wersje od 1 do 3 obsługują przeglądarki, które nie mają żadnych innych możliwości poza wykonywaniem skryptów JavaScript. Należy zauważyć, że użytkownik zawsze ma możliwość zablokowania wykonywania tych skryptów. Wówczas aplikacja będzie działać tylko częściowo w wersji 1 (opcja „Usuń” nie będzie działać) oraz w ogóle nie będzie działać w wersjach 2 i 3 (opcje „Usuń” i „Oblicz” nie będą działać). Interesujące mogłoby być stworzenie wersji serwisu, która nie wykorzystuje skryptów JavaScript.

Tworząc serwis internetowy, należy zadać sobie pytanie, do jakich typów klientów kierujemy naszą ofertę. Jeśli chcemy dotrzeć do jak największej liczby klientów, stworzymy aplikację, która wysyła do przeglądarek wyłącznie kod HTML (bez JavaScriptu ani apletów). Jeśli pracujemy w intranecie i mamy kontrolę nad konfiguracją stanowisk w nim, możemy pozwolić sobie na większe wymagania wobec klienta.

Wersja 4 to klient internetowy, który wyszukuje potrzebne informacje w strumieniu HTML wysyłanym przez serwer. Bardzo często nie mamy kontroli nad tym strumieniem. Dzieje się tak, gdy piszemy klienta dla istniejącej w sieci usługi internetowej, którą zarządza ktoś inny. Weźmy przykład. Załóżmy, że nasza usługa symulacji obliczeń podatkowych została stworzona przez firmę X. Obecnie usługa ta wysyła symulacje w tabeli o nazwie HTML, a nasz klient wykorzystuje ten fakt, aby je pobrać. W ten sposób porównuje każdy wiersz odpowiedzi serwera z wyrażeniem regularnym:

     // szablon wiersza tabeli symulacji
     // <tr><td>tak</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>|";

Załóżmy teraz, że twórca aplikacji zmienia wygląd odpowiedzi, umieszczając symulacje nie w tabeli, ale na liście w następującej postaci:

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

W takim przypadku nasz klient internetowy będzie wymagał przeróbki. To właśnie stanowi stałe zagrożenie dla klientów internetowych aplikacji, nad którymi nie mamy kontroli. XML może stanowić rozwiązanie tego problemu:

  • zamiast generować HTML, usługa symulacji wygeneruje XML. W naszym przykładzie mogłoby to wyglądać następująco:
<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>
  • do tej odpowiedzi można by przypisać arkusz stylów, wskazujący przeglądarkom, jak ta odpowiedź powinna wyglądać XML
  • klienty internetowe ignorowałyby ten arkusz stylów i pobierałyby informacje bezpośrednio ze strumienia odpowiedzi XML

Jeśli twórca serwisu zechce zmienić wygląd dostarczanych wyników, zmieni arkusz stylów, a nie sam kod XML. Dzięki arkuszowi stylów przeglądarki wyświetlą nowy wygląd, a programowe klienty internetowe nie będą wymagały modyfikacji.