Skip to content

3. Вступ до веб-програмування в PHP

3.1. Програмування PHP

Нагадаємо, що PHP є повноцінною мовою програмування і, хоча вона переважно використовується для розробки веб-додатків, її можна застосовувати й в інших контекстах. Документ «PHP на прикладах», доступний за адресою http://shiva.istia.univ-angers.fr/~tahe/pub/php/php.pdf, містить основи цієї мови. Тут передбачається, що ці знання вже є. На простому прикладі покажемо процедуру виконання програми PHP у Windows. Наведений нижче код було збережено під назвою coucou.php.

<?php
    echo "coucou";
?>

Виконання цієї програми відбувається у вікні командного рядка Windows:


dos>"e:\program files\easyphp\php\php.exe" coucou.php
X-Powered-By: PHP/4.3.0-dev
Content-type: text/html

coucou

Слід зауважити, що інтерпретатор PHP за замовчуванням надсилає:

  • інтерпретатор PHP — це php.exe, який зазвичай знаходиться в каталозі <php> інсталяції програмного забезпечення.
  • заголовки HTTP X-Powered-By та Content-type:
  • порожній рядок, що відокремлює заголовки HTTP від решти документа
  • документ, сформований тут із тексту, написаного функцією echo

3.2. Файл конфігурації інтерпретатора PHP

Поведінка інтерпретатора PHP налаштовується за допомогою конфігураційного файлу під назвою php.ini, який у Windows зберігається у самому каталозі Windows. Це файл значного розміру, оскільки в Windows для версії 4.2 PHP він налічує майже 1000 рядків, з яких, на щастя, три чверті — це коментарі. Розглянемо деякі атрибути конфігурації PHP:

short_open_tag = On
дозволяє вставляти інструкції між тегами <? >. У off їх слід вставляти між тегами <?php ... >
asp_tags = Off
у on дозволяє використовувати синтаксис <% =variable %>, який застосовується в технології ASP (Active Server Pages)
expose_php = On
дозволяє надсилати заголовок HTTP X-Powered-By: PHP/4.3.0-dev. У off цей заголовок видалено.
error_reporting  =  E_ALL & ~E_NOTICE
визначає обсяг відстеження помилок. У цьому випадку будуть повідомлятися всі помилки (E_ALL), крім попереджень під час виконання (~E_NOTICE)
display_errors = Off
у on, розміщує помилки у потоці HTML, що надсилається клієнту. Таким чином, вони відображаються у браузері. Рекомендується встановити для цього параметра значення off.
log_errors = On
помилки будуть збережені у файлі
track_errors = On
зберігає останню помилку, що виникла, у змінній $php_errormsg
error_log = E:\Program Files\EasyPHP\php\erreurs.log
визначає файл для збереження помилок (якщо log_errors=on)
register_globals = Off
у on певна кількість змінних стає глобальними. Вважається вразливістю безпеки.
default_mimetype = "text/html"
за замовчуванням генерує заголовок HTTP: Content-type: text/html
include_path    =".;E:\Program Files\EasyPHP\php\pear\"
список каталогів, які будуть обшукані на наявність файлів, необхідних для виконання директив include або require
session.save_path = /temp
каталог, у якому будуть зберігатися файли, що містять дані про різні поточні сесії. Це той диск, на якому було встановлено PHP. Тут /temp означає e:\temp

Цей файл конфігурації впливає на переносимість написаної програми на PHP. Дійсно, якщо веб-додаток має отримати значення поля C з веб-форми, він зможе зробити це різними способами залежно від того, чи має конфігураційна змінна register_globals значення on чи off:

  • off: значення буде отримано за допомогою $HTTP_GET_VARS["C"] або _GET["C"] або $HTTP_POST_VARS["C"] або $_POST["C"] залежно від методу (GET/POST), який використовує клієнт для надсилання значень форми
  • : те саме, що й вище, плюс $C, оскільки значення поля C було перетворено на глобальне у змінній, що має таку саму назву, як і поле

Якщо розробник пише програму, використовуючи нотацію $C, оскільки веб-сервер/PHP, який він використовує, присвоює змінній register_globals значення on, ця програма перестане працювати, якщо її перенести на веб-сервер/PHP, де ця сама змінна має значення off. Тому слід намагатися писати програми, уникаючи використання функцій, що залежать від конфігурації веб-сервера/PHP.

3.3. Налаштування PHP під час виконання

Щоб покращити переносимість програми PHP, можна самостійно встановити деякі конфігураційні змінні PHP. Вони змінюються під час виконання програми і лише для неї. У цьому процесі корисними є дві функції:

ini_get("confVariable")
повертає значення конфігураційної змінної confVariable
ini_set("confVariable","valeur")
встановлює значення конфігураційної змінної confVariable

Ось приклад, у якому встановлюється значення конфігураційної змінної track_errors:

<?php
     // значення конфігураційної змінної track_errors
    echo "track_errors=".ini_get("track_errors")."\n";
  // зміна цього значення
  ini_set("track_errors","off");
   // перевірка
  echo "track_errors=".ini_get("track_errors")."\n";
?>

Під час виконання отримуємо такі результати:

E:\data\serge\web\php\poly\intro>"E:\Program Files\EasyPHP\php\php.exe" conf1.php
Content-type: text/html

track_errors=1
track_errors=off

Значення конфігураційної змінної track_errors спочатку дорівнювало 1 (~on). Ми змінюємо його на off. Слід пам’ятати, що якщо наша програма має спиратися на певні значення конфігураційних змінних, доцільно ініціалізувати їх у самій програмі.

3.4. Умови виконання прикладів

Приклади з цього посібника будуть виконуватися з такою конфігурацією:

  • PC під Windows 2000
  • сервер Apache 1.3
  • PHP 4.3

Налаштування сервера Apache здійснюється у файлі httpd.conf. Наступні рядки вказують Apache завантажити PHP як вбудований модуль Apache та передавати до інтерпретатора PHP усі запити на документи з певними розширеннями, зокрема .php. Це суфікс за замовчуванням, який ми будемо використовувати для наших PHP-програм.

LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"
AddModule mod_php4.c
AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

Крім того, ми визначили для Apache псевдонім poly:

Alias "/poly/" "e:/data/serge/web/php/poly/"
<Directory "e:/data/serge/web/php/poly">
  Options Indexes FollowSymLinks Includes
  AllowOverride All
   #Порядок: allow,deny
  Allow from all
</Directory>

Назвемо <poly> шляхом e:/data/serge/web/php/poly. Якщо ми хочемо за допомогою браузера запросити документ doc.php у сервера Apache, ми використаємо http://localhost/poly/doc.php. Сервер Apache розпізнає в URL псевдонім poly і, отже, пов’яже URL /poly/doc.php з документом <poly>\doc.php.

3.5. Перший приклад

Напишемо перший веб-додаток на PHP. У файлі heure.php зберігається такий текст:

<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Якщо ми відкриємо цю сторінку в браузері, отримаємо такий результат:

Image

Динамічна частина сторінки була згенерована кодом PHP:

            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>

Що саме сталося? Браузер надіслав запит на файл URL http://localhost/poly/intro/heure.php. Веб-сервер (у цьому прикладі — Apache) отримав цей запит і, завдяки суфіксу .php у запитуваному документі, визначив, що повинен перенаправити цей запит до інтерпретатора PHP. Тоді інтерпретатор аналізує документ heure.php і виконує всі фрагменти коду, розташовані між тегами <?php >, і замінює кожну з них рядками, написаними за допомогою інструкцій PHP, echo або print. Таким чином, інтерпретатор PHP виконає наведений вище фрагмент коду та замінить його рядком, записаним інструкцією echo:

2/10/02, 06:13:47

Після виконання всіх фрагментів коду PHP документ PHP перетворюється на звичайний документ HTML, який потім надсилається клієнту.

Слід максимально уникати змішування коду PHP та коду HTML. З цією метою попередню програму можна переписати наступним чином:

<!-- код PHP -->
<?php
     // отримання поточного часу
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- код HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Результат, отриманий у браузері, є ідентичним:

Image

Другий варіант кращий за перший, оскільки в коді PHP менше коду, ніж у коді HTML. Таким чином, структура сторінки стає більш наочною. Можна піти ще далі, розмістивши код PHP та код HTML у двох різних файлах. Код PHP зберігається у файлі heure3.php:

<!-- код PHP -->
<?php
     // отримуємо поточний час
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
   // виводиться відповідь
  include "heure3-page1.php";
?>

Код HTML зберігається у файлі heure3-page1.php:

<!-- код HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Коли браузер запросить документ heure3.php, його завантажить та проаналізує інтерпретатор PHP. При виявленні рядка

  include "heure3-page1.php";

інтерпретатор включить файл heure3-page1.php у вихідний код heure3.php і виконає його. Таким чином, все відбувається так, ніби ми маємо такий код PHP:

<!-- код PHP -->
<?php
     // отримуємо поточний час
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- код HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Отриманий результат такий самий, як і раніше:

Image

Пізніше буде застосовано рішення щодо розміщення кодів PHP та HTML у різних файлах. Воно має низку переваг:

  • структура сторінок, що надсилаються клієнту, не загублена в коді PHP. Таким чином, їх може обслуговувати «веб-дизайнер», який має графічні навички, але мало знається на PHP.
  • код PHP слугує «фронтендом» для запитів клієнтів. Його призначення — обчислювати дані, необхідні для сторінки, яка буде надіслана клієнту у відповідь.

Однак це рішення має недолік: замість завантаження одного документа воно вимагає завантаження декількох документів, що може призвести до зниження продуктивності.

3.6. Отримання параметрів, надісланих веб-клієнтом

3.6.1. за допомогою POST

Розглянемо таку форму, у якій користувач повинен вказати дві відомості: ім’я та вік.

Image

Коли користувач заповнив поля Nom та Age, він натискає кнопку Envoyer, яка має тип submit. Після цього значення з форми надсилаються на сервер. Сервер повертає форму разом із таблицею, що містить перелік отриманих значень:

Image

Браузер запитує форму у наступної програми nomage.php:

<?php
   // чи отримано очікувані параметри?
  $post=isset($_POST["txtNom"]) && isset($_POST["txtAge"]);
  if($post){
    // отримуємо параметри txtNom та txtAge, «надіслані» клієнтом
    $nom=$_POST["txtNom"];
    $age=$_POST["txtAge"];
  } else {
      $nom="";
    $age="";
  }//if
   // відображення сторінки
  include "nomage-p1.php";
?>

Деякі пояснення:

  • поле форми HTML, яке називається «поле», може бути надіслане на сервер методом GET або методом POST. Якщо воно надсилається методом GET, сервер може отримати його у змінній $_GET["champ"] та у змінній $_POST["champ"], якщоякщо він надсилається методом POST.
  • Наявність даних можна перевірити за допомогою функції isset(дані), яка повертає true, якщо дані існують, і false — у протилежному випадку.
  • програма nomage.php створює три змінні: $nom — для назви форми, $age — для віку та $post — для вказівки, чи були «відправлені» значення чи ні. Ці три змінні передаються на сторінку nomage-p1.php. Слід зауважити, що хоча ця сторінка бере участь у формуванні відповіді клієнту, сам клієнт про це не знає. З його точки зору, відповідь надає програма nomage.php.
  • Коли клієнт вперше звертається до додатка nomage.php, $post повертає помилку. Дійсно, під час цього першого виклику жодне значення з форми не передається на сервер.

Сторінка nomage-p1.php виглядає так:

<html>
  <head>
    <title>Formulaire web</title>
  </head>

   <body>
     <center>
       <h3>Un formulaire Web</h3>
       <h4>Récupération des valeurs des champs d'un formulaire</h4>
       <hr>
       <form name="frmPersonne" method="post">
         <table>
           <tr>
             <td>Nom</td>
             <td><input type="text" value="<?php echo $nom ?>" name="txtNom" size="20"></td>
             <td>Age</td>
             <td><input type="text" value="<?php echo $age ?>" name="txtAge" size="3"></td>
           <tr>
         </table>
         <input type="submit" name="cmdEffacer" value="Envoyer">
       </form>
     </center>
     <hr>
     <?php
          // чи були надіслані якісь значення? 
         if ($post) { 
     ?>
         <h4>Valeurs récupérées</h4>
      <table border="1">
          <tr>
            <td>Nom</td><td><?php echo $nom ?></td>
          <td width="10"></td>
            <td>Age</td><td><?php echo $age ?></td>
        <tr>
      </table>
    <?php } ?>
   </body>
</html>

Додаток nomage-p1.php містить форму frmPersonne. Вона визначається тегом:

       <form name="frmPersonne" method="post">

Оскільки атрибут action тегу не визначено, браузер передасть дані форми до URL, до якого він звернувся, щоб отримати її, c.a.d. додаток nomage.php.

Розрізнимо два випадки виклику додатка nomage.php:

  1. Користувач викликає її вперше. Отже, додаток nomage.php викликає додаток nomage-p1.php, передаючи йому значення ($nom,$age,$post)=("","",false). Тоді додаток nomage-p1.php відображає порожню форму.
  2. Користувач заповнює форму та натискає кнопку Envoyer (типу submit). Значення форми (txtNom, txtAge) потім «відправляються» (method="post" у <form>) до програми nomage.php (атрибут action не визначено в <form>). Додаток nomage.php обчислює ($nom, $age,$post) = (txtNom, txtAge, true) і передає їх до додатка nomage-p1.php, який потім відображає вже заповнену форму, а також таблицю отриманих значень.

3.6.2. за допомогою GET

У разі, якщо значення форми передаються на сервер за допомогою GET, додаток nomage.php перетворюється на наступний додаток nomage2.php:

<?php
   // чи отримано очікувані параметри?
  $get=isset($_GET["txtNom"]) && isset($_GET["txtAge"]);
  if($get){
     //— клієнт отримує параметри txtNom та txtAge «GETTés»
    $nom=$_GET["txtNom"];
    $age=$_GET["txtAge"];
  } else {
      $nom="";
    $age="";
  }//якщо
   // сторінка відображається
  include "nomage-p2.php";
?>

Заявка nomage-p2.php є ідентичною заявці nomage-p1.php, за винятком таких деталей:

  • тег form змінено:
       <form name="frmPersonne" method="get">
  • застосунок тепер отримує змінну $get замість $post:
     <?php
          // чи були надіслані якісь значення на сервер? 
         if ($get) { 
     ?>

Під час виконання, коли значення вводяться у форму та надсилаються на сервер, браузер відображає у своєму полі URL той факт, що значення були надіслані методом GET:

Image

3.7. Отримання заголовків HTTP, надісланих веб-клієнтом

Коли браузер надсилає запит до веб-сервера, він надсилає йому певну кількість заголовків HTTP. Іноді буває корисно отримати доступ до них. Спочатку можна скористатися асоціативною таблицею $_SERVER. Вона містить різноманітну інформацію, надану веб-сервером, зокрема заголовки HTTP, надані клієнтом. Розглянемо наступну програму, яка виводить усі значення масиву $_SERVER:

<?php
     // відображення змінних, пов’язаних із веб-сервером
   // надсилання простого тексту
  header("Content-type: text/plain");
   // проходження по асоціативному масиву $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      echo "$clé : $valeur\n";
    }//while
?>

Збережемо цей код у файлі headers.php і відкриємо файл URL у браузері:

Image

Ми отримуємо певну кількість інформації, зокрема заголовки HTTP, надіслані браузером. Це значення, пов’язані з ключами, що починаються з HTTP. Розглянемо детальніше деяку з отриманої вище інформації:

HTTP_ACCEPT
типи документів, що підтримуються веб-клієнтом
HTTP_ACCEPT_CHARSET
типи символів, що приймаються в документах
HTTP_ACCEPT_ENCODING
типи кодувань, що підтримуються для документів
HTTP_ACCEPT_LANGUAGE
допустимі типи мов для документів
HTTP_CONNECTION
тип з'єднання з сервером. Keep-Alive: сервер повинен підтримувати з'єднання відкритим після надання відповіді
HTTP_KEEP_ALIVE
? максимальний час відкритого з’єднання
HOST
хост-комп’ютер, до якого звертається клієнт
HTTP_USER_AGENT
ідентифікатор клієнта
REMOTE_ADDR
адреса клієнта IP
REMOTE_PORT
порт зв'язку, який використовує клієнт
SERVER_PROTOCOL
протокол HTTP, що використовується сервером
REQUEST_METHOD
метод запиту, що використовується клієнтом (GET або POST)
QUERY_STRING
запит ?param1=val1&param2=val2&..., розміщений за запитом URL (метод GET)

Злегка змінивши код попередньої програми, ми можемо отримати лише заголовки HTTP:

<?php
     // виводить змінні, пов'язані з веб-сервером
   // відправляємо простий текст
  header("Content-type: text/plain");
   // обхід асоціативного масиву $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      // заголовок HTTP ?
    if(strtolower(substr($clé,0,4))=="http")
          echo substr($clé,5)." : $valeur\n";
    }//while
?>

Результат, отриманий у браузері, такий:

Image

Якщо потрібно отримати конкретний заголовок HTTP, слід вказати, наприклад, $_SERVER["HTTP_ACCEPT"].

3.8. Отримання інформації про середовище

Веб-сервер/PHP працює в середовищі, про яке можна дізнатися за допомогою масиву $_ENV, що зберігає різні характеристики середовища виконання. Розглянемо таку програму env1.php:

<?php
     // виводить змінні, пов’язані з веб-сервером
   // відправляємо простий текст
  header("Content-type: text/plain");
   // обхід асоціативного масиву $_ENV
     reset($_ENV);
     while (list($clé,$valeur)=each($_ENV)){
      echo "$clé : $valeur\n";
    }//while
?>

У браузері це дає такий результат (частковий вигляд):

Image

Наприклад, вище видно, що веб-сервер/PHP працює під управлінням Windows OS NT.

3.9. Приклади

3.9.1. Динамічне створення форми — 1

Як приклад візьмемо створення форми, що містить лише один елемент управління: комбінований список. Вміст цього комбінованого списку формується динамічно на основі значень, взятих із масиву. Насправді ці значення часто беруться з бази даних. Форма виглядає так:

Image

Якщо для наведеного вище прикладу виконати Envoyer, отримаємо таку відповідь:

Image

Код HTML початкової форми, після її генерації, має такий вигляд:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <option>un</option>
<option>deux</option>
<option>trois</option>
<option>quatre</option>
<option>cinq</option>
<option>six</option>
<option>sept</option>
<option>huit</option>
<option>neuf</option>
<option>dix</option>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

Додаток PHP складається з головної сторінки valeurs.php, яка викликається як для отримання початкової форми (списку значень), так і для обробки значень цієї форми та надання відповіді (обраного значення). Додаток генерує дві різні сторінки:

  • сторінку початкової форми, яка буде згенерована програмою valeurs-p1.php
  • сторінку з відповіддю, що надається користувачеві, яка буде згенерована програмою valeurs-p2.php

Додаток valeurs.php виглядає наступним чином:

<?php

     // масив значень
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

     // чи є очікувані параметри
  $requêteVide=! isset($_POST["cmbValeurs"]);

   // отримуємо вибір користувача
  if ($requêteVide){
       // початковий запит
    include "valeurs-p1.php";
  }else{
       // відповідь на POST 
      $choix=$_POST["cmbValeurs"];
      include "valeurs-p2.php";    
  }
?>

Вона визначає масив значень і викликає valeurs-p1.php для формування початкової форми, якщо запит клієнта був порожнім, або valeurs-p2.php для формування відповіді, якщо запит був дійсним. Програма valeurs1-php має такий вигляд:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              echo "<option>$valeurs[$i]</option>\n";
          }//для
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

Список значень комбінованого поля генерується динамічно на основі масиву $valeurs, переданого valeurs.php. Програма valeurs-p2.php генерує відповідь:

<html>
    <head>
      <title>réponse</title>
  </head>
  <body>
      <h2>Vous avez choisi le nombre <?php echo $choix ?></h2>
  </body>
</html>

Тут ми просто відображаємо значення змінної $choix, яке також передається змінною valeurs.php.

3.9.2. Динамічне формування форми — 2

Повернемося до попереднього прикладу, змінивши його наступним чином. Пропонована форма залишається незмінною:

Image

Чи відрізняється відповідь:

Image

У відповіді повертається форма, а під нею вказано число, обране користувачем. Крім того, саме це число відображається як обране у списку, що виводиться у відповіді.

Код valeurs.php виглядає так:

<?php

     // конфігурація
  ini_set("register_globals","off");

    // таблиця значень
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // отримуємо можливий вибір користувача
  $choix=$_POST["cmbValeurs"];

   // відображаємо відповідь  
  include "valeurs-p1.php";
?>

Слід зауважити, що тут ми подбали про те, щоб налаштувати PHP таким чином, щоб не було глобальних змінних. Зазвичай це розумний запобіжний захід, оскільки глобальні змінні створюють проблеми з безпекою. Альтернативним варіантом є ініціалізація всіх змінних, які використовуються. Це призведе до «перезапису» можливої глобальної змінної з такою самою назвою.

Сторінка форми відображається за допомогою valeurs-p1.php:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              // якщо поточна опція збігається з вибором, її вибираємо
              if (isset($choix) && $choix==$valeurs[$i])
                  echo "<option selected>$valeurs[$i]</option>\n";
            else echo "<option>$valeurs[$i]</option>\n";
          }//for
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // продовження на наступній сторінці
      if(isset($choix)){
          echo "<hr>\n";
          echo "<h3>Vous avez choisi le nombre $choix</h3>\n";
      }
    ?>
  </body>
</html>

Програма-генератор сторінки використовує змінну $choix, передану програмою valeurs.php. Зауважимо, що структура HTML сторінки починає серйозно «забруднюватися» кодом PHP. Фронтенд valeurs.php міг би виконувати більше роботи, як показано в наступній новій версії:

<?php

     // конфігурація
  ini_set("register_globals","off");

    // таблиця значень
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // отримуємо можливий вибір користувача
  $choix=$_POST["cmbValeurs"];

   // обчислюємо список значень для відображення
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // якщо поточний параметр збігається з вибором, його вибираємо
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//for

   // обчислюється друга частина сторінки
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//якщо

   // виводиться відповідь  
  include "valeurs-p2.php";
?>

Сторінка тепер генерується за допомогою програми valeurs-p2.php наступним чином:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
             // виведення списку значень
          echo $HTMLvaleurs;
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // виведення частини 2
      echo $HTMLpart2;
    ?>
  </body>
</html>

Код HTML тепер позбавлений значної частини коду PHP. Однак нагадаємо мету поділу на фронтенд-програму, яка аналізує та обробляє запит клієнта, та програми, що просто відповідають за відображення сторінок, налаштованих за даними, переданими фронтендом: це розділення роботи розробника PHP від роботи графічного дизайнера. Розробник PHP працює над інтерфейсом, а графічний дизайнер — над веб-сторінками. У нашій новій версії графічний дизайнер, наприклад, більше не може працювати над частиною 2 сторінки, оскільки він більше не має доступу до її коду HTML. У першій версії він міг це робити. Отже, жоден із цих двох методів не є ідеальним.

3.9.3. Динамічне формування форми — 3

Ми повернемося до тієї самої проблеми, що й раніше, але цього разу значення беруться з бази даних. У нашому прикладі це база MySQL:

  • база має назву dbValeurs
  • її власником є admDbValeurs із паролем mdpDbValeurs
  • база даних містить єдину таблицю з назвою tvaleurs
  • ця таблиця має лише одне цілочисельне поле з назвою «valeur»

dos> mysql --database=dbValeurs --user=admDbValeurs --password=mdpDbValeurs

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

mysql> describe tvaleurs;
+--------+---------+------+-----+---------+-------+
| Field  | Type    | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| valeur | int(11) |      |     | 0       |       |
+--------+---------+------+-----+---------+-------+

mysql> select * from tvaleurs;
+--------+
| valeur |
+--------+
|      0 |
|      1 |
|      2 |
|      3 |
|      4 |
|      6 |
|      5 |
|      7 |
|      8 |
|      9 |
+--------+
10 rows in set (0.00 sec)

У додатку, що використовує базу даних, зазвичай виконуються такі етапи:

  1. Підключення до SGBD
  2. Відправлення запитів SQL до бази даних SGBD
  3. Обробка результатів цих запитів
  4. Закриття з’єднання з SGBD

Етапи 2 і 3 виконуються повторно, а з’єднання закривається лише після завершення роботи з базою даних. Це досить типова схема для всіх, хто коли-небудь працював з базою даних в інтерактивному режимі. У наступній таблиці наведено інструкції PHP для виконання цих різних операцій за допомогою SGBD та MySQL:

connexion au SGBD
$connexion=mysql_pconnect($hote,$user,$pwd)
$connexion=mysql_connect($hote,$user,$pwd)
$hote: інтернет-ім'я комп'ютера, на якому виконуються SGBD та MySQL. Дійсно, можна працювати з віддаленими SGBD та MySQL.
$user: ім’я відомого користувача SGBD MySQL
$pwd: його пароль
$connexion: створено з’єднання
mysql_pconnect створює постійне з’єднання з SGBD та MySQL. Таке з'єднання не закривається після завершення роботи скрипта. Воно залишається відкритим. Отже, коли знадобиться відкрити нове з'єднання з SGBD, PHP шукатиме існуюче з'єднання, що належить тому самому користувачеві. Якщо його знайде, то використає його. Це дозволяє заощадити час.
mysql_connect створює неперсистивне з’єднання, яке закривається після завершення роботи з SGBD та MySQL.
requêtes SQL
$résultats=mysql_db_query($base,$requête,$connexion)
$base: база даних MySQL, з якою ми будемо працювати
$requête: запит до SQL (insert, delete, update, select, ...)
$connexion: з’єднання з SGBD MySQL
$résultats: результати запиту — відрізняються залежно від того, чи є запит операцією вибору (SELECT) чи операцією оновлення (INSERT, UPDATE, DELETE тощо)
traitement des
 résultats d'un
 select
$résultats=mysql_db_query($base,"select ...",$connexion)
Результатом операції select є таблиця, тобто набір рядків і стовпців. Доступ до цієї таблиці здійснюється через $résultats.
$ligne=mysql_fetch_row($résultats) зчитує рядок з таблиці та поміщає його в $ligne у вигляді масиву. Таким чином, $ligne[i] є i-м стовпцем отриманого рядка. Функцію mysql_fetch_row можна викликати багаторазово. Щоразу вона зчитує новий рядок з таблиці $résultats. Коли досягається кінець таблиці, функція повертає значення false. Таким чином, таблицю $résultats можна використовувати наступним чином:
while($рядок = mysql_fetch_row($результати)) {
// обробляє поточний рядок $рядок
}//while
traitement des 
résultats d'une 
requête de mise à 
jour
$résultats=mysql_db_query($base,"insert ...",$connexion)
Значення $résultats є істинним або хибним залежно від того, чи операція завершилася успішно чи ні. У разі успіху функція mysql_affected_rows дозволяє дізнатися кількість рядків, змінених операцією оновлення.
fermeture de la
 connexion
mysql_close($з'єднання)
$connexion: підключення до SGBD MySQL

Код інтерфейсу valeurs.php виглядає наступним чином:

<?php

     // конфігурація
  ini_set("register_globals","off");
    ini_set("display_errors","off");
    ini_set("track_errors","on");

    // таблиця значень
  list($erreur,$valeurs)=getValeurs();

   // Чи сталася помилка?
  if($erreur){
       // відображення сторінки помилки
    include "valeurs-err.php";
     // кінець
    return;
  }//if

   // отримуємо можливий вибір користувача
  $choix=$_POST["cmbValeurs"];

   // обчислюємо список значень для відображення
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // якщо поточна опція дорівнює вибору, її вибирають
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//for

   // обчислюється друга частина сторінки
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//якщо

   // виводиться відповідь  
  include "valeurs-p1.php";

   // кінець
  return;

  // ------------------------------------------------------------------------
  function getValeurs(){
       // отримує значення з бази даних MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $hote="localhost";
    $table="tvaleurs";
    $champ="valeur";

     // відкриття постійного з'єднання з сервером MySQL
     // або, якщо це неможливо, звичайного з’єднання
    ($connexion=mysql_pconnect($hote,$user,$pwd)) 
        || ($connexion=mysql_connect($hote,$user,$pwd));
    if(! $connexion)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // отримання значень
    $selectValeurs=mysql_db_query($db,"select $champ from $table",$connexion);
    if(! $selectValeurs)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // значення заносяться в масив
    $valeurs=array();
    while($ligne=mysql_fetch_row($selectValeurs)){
        $valeurs[]=$ligne[0];
    }//while

     // закриття з'єднання (якщо воно є постійним, то фактично не буде закрите)
    mysql_close($connexion);

     // повернення результату
    return array("",$valeurs);
  }//getValeurs

?>

Цього разу значення, які потрібно ввести в комбінований список, надаються не масивом, а функцією getValeurs(). Ця функція:

  • відкриває постійне з’єднання (mysql_pconnect) із сервером mySQL, передаючи зареєстроване ім’я користувача та його пароль.
  • Після встановлення з’єднання надсилається запит select для отримання значень, що містяться в таблиці tvaleurs бази даних dbValeurs.
  • Результат запиту select записується в масив $valeurs, який повертається програмі-викликувачу.
  • Фактично функція повертає масив із двома результатами ($erreur, $valeurs), де перший елемент — це можливе повідомлення про помилку або, в іншому випадку, порожній рядок.
  • Програма, що викликає функцію, перевіряє, чи сталася помилка, і якщо так, то відображає сторінку valeurs-err.php. Вона має такий вигляд:
<html>
    <head>
      <title>Erreur</title>
  </head>
  <body>
      <h3>L'erreur suivante s'est produite</h3>
    <font color="red">
        <h4><?php echo $erreur ?></h4>
    </font>
  </body>
</html>
  • якщо помилки не сталося, програма-викликач отримує значення з масиву $valeurs. Отже, ми повертаємося до попередньої проблеми.

Ось два приклади виконання:

  • з помилкою

Image

  • без помилки

Image

3.9.4. Динамічне створення форми — 4

У попередньому прикладі, що сталося б, якби ми змінили SGBD? Наприклад, якби ми перейшли з MySQL на Oracle або SQL Server? Довелося б переписати функцію getValeurs(), яка надає ці значення. Перевага об’єднання коду, необхідного для отримання значень, що мають відображатися у списку, в одну функцію полягає в тому, що код, який потрібно змінити, є чітко визначеним і не розкиданий по всій програмі. Функцію getValeurs() можна переписати так, щоб вона не залежала від використовуваного SGBD. Достатньо, щоб вона працювала з драйвером ODBC від SGBD, а не безпосередньо з SGBD.

На ринку існує безліч баз даних. З метою уніфікації доступу до баз даних у середовищі Windows компанія Microsoft розробила інтерфейс під назвою ODBC (Open DataBase Connectivity). Цей рівень приховує особливості кожної бази даних за стандартним інтерфейсом. У Windows існує безліч драйверів, що полегшують доступ до баз даних. Ось, наприклад, список драйверів, встановлених на комп’ютері з Windows:

Image

SGBD MySQL також має драйвер ODBC. Програма, що використовує драйвери ODBC, може працювати з будь-якою з перерахованих вище баз даних без необхідності перепрограмування.

Зробимо нашу базу даних MySQL dbValeurs доступною через драйвер ODBC. Наведена нижче процедура стосується Windows 2000. Для систем Win9x процедура є дуже схожою. Увімкніть диспетчер ресурсів ODBC:

Image

За допомогою кнопки [Add] додайте нове джерело даних ODBC:

Image

Вибираємо драйвер ODBC MySQL, виконуємо [Terminer], а потім вказуємо характеристики джерела даних:

Image

Windows DSN name 
ім’я, присвоєне джерелу даних ODBC (odbc-valeurs)
MySQL host 
ім'я комп'ютера, на якому розміщено SGBD MySQL, що керує джерелом даних (localhost)
MySQL database name 
ім'я бази даних MySQL, яка є джерелом даних (dbValeurs)
User 
користувач із достатніми правами доступу до бази даних MySQL, яку потрібно керувати (admDbValeurs)
Password 
свій пароль (mdpDbValeurs)

PHP здатний працювати з драйверами ODBC. У наступній таблиці наведено функції, про які варто знати:

connexion au SGBD
$connexion = odbc_pconnect($dsn, $user, $pwd)
$connexion=odbc_connect($dsn,$user,$pwd)
$dsn: ім’я DSN (Data Source Name) комп’ютера, на якому виконується SGBD
$user: ім’я користувача, відомого програмі SGBD
$pwd: його пароль
$connexion: створене з'єднання
odbc_pconnect створює постійне з'єднання з SGBD. Таке з'єднання не закривається після завершення роботи скрипта. Воно залишається відкритим. Отже, коли знадобиться відкрити нове з'єднання з SGBD, PHP шукатиме існуюче з'єднання, що належить тому самому користувачеві. Якщо його знайде, то використає його. Це дозволяє заощадити час.
odbc_connect створює неперсистивне з’єднання, яке закривається після завершення роботи з SGBD.
requêtes SQL
$requêtePréparée=odbc_prepare($connexion,$requête)
$requête: запит SQL (insert, delete, update, select, ...)
$connexion: з’єднання з SGBD
Аналізує запит $requête і готує його до виконання. На такий «підготовлений» запит посилається результат $requêtePréparée. Підготовка запиту до виконання не є обов’язковою, але покращує продуктивність, оскільки аналіз запиту виконується лише один раз. Потім запускається підготовлений запит. Якщо неодноразово запускати непідготовлений запит, його аналіз виконується щоразу, що є зайвим. Після підготовки запит виконується за допомогою
$res=odbc_execute($requêtePréparée)
, що повертає значення «true» або «false» залежно від того, чи завершилося виконання запиту успішно
traitement des
 résultats d'un
 select
Результатом запиту SELECT є таблиця, тобто набір рядків і стовпців. Доступ до цієї таблиці здійснюється через $requêtePréparée.
$res=odbc_fetch_row($requêtePréparée) зчитує рядок із таблиці результатів, отриманої за допомогою select. Повертає значення «true» або «false» залежно від того, чи виконано запит успішно. Елементи отриманого рядка доступні через функцію odbc_result:
$val=odbc_result($requêtePréparée,i): стовпець i щойно прочитаного рядка
$val=odbc_result($requêtePréparée,"nomColonne"): стовпець nomColonne рядка, що щойно було прочитано
Функцію odbc_fetch_row можна викликати багаторазово. Кожного разу вона зчитує новий рядок з таблиці результатів. Коли досягається кінець таблиці, функція повертає значення false. Таким чином, таблицю результатів можна використовувати наступним чином:

while(odbc_fetch_row($résultats)){
    // обробляє поточний рядок за допомогою odbc_result
}//while
fermeture de la 
connexion
odbc_close($з'єднання)
$connexion: з'єднання з SGBD MySQL

Функція getValeurs(), яка відповідає за витяг значень із бази даних ODBC, має такий вигляд:

  // ------------------------------------------------------------------------
  function getValeurs(){
       // отримує значення з бази даних MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $dsn="odbc-valeurs";
    $table="tvaleurs";
    $champ="valeur";

     // відкриває постійне з'єднання з сервером MySQL
     // або, якщо це неможливо, звичайного з’єднання
    ($connexion=odbc_pconnect($dsn,$user,$pwd)) 
        || ($connexion=odbc_connect($dsn,$user,$pwd));
    if(! $connexion)
        return array("1 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // отримання значень
    $selectValeurs=odbc_prepare($connexion,"select $champ from $table");
    if(! odbc_execute($selectValeurs))
        return array("2 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // значення заносяться в масив
    $valeurs=array();   
    while(odbc_fetch_row($selectValeurs)){
        $valeurs[]=odbc_result($selectValeurs,$champ);
    }//while

     // закриття з'єднання (якщо воно є постійним, то фактично не буде закрите)
    odbc_close($connexion);

     // повернення результату
    return array("",$valeurs);
  }//getValeurs

?>

Якщо запустити нову програму без активації бази даних odbc-valeurs, отримаємо такий результат:

Image

Слід зауважити, що код помилки, який повертає драйвер ODBC (odbc_error()=S1000), є не надто зрозумілим. Якщо база даних odbc-valeurs стає доступною, ми отримуємо ті самі результати, що й раніше.

Підсумовуючи, можна сказати, що це рішення є вдалим з точки зору обслуговування додатка. Адже якщо база даних має змінитися, додаток не потребуватиме змін. Системний адміністратор просто створить нове джерело даних ODBC для нової бази. З огляду на обслуговування також було б доцільно встановити параметри доступу до бази ($dsn, $user, $pwd) у окремий файл, який додаток завантажуватиме під час запуску (include).

3.9.5. Отримання значень з форми

Ми вже неодноразово отримували значення з форми, надіслані веб-клієнтом. Наступний приклад демонструє форму, що об’єднує найпоширеніші компоненти HTML, і призначений для отримання параметрів, надісланих браузером клієнта. Форма має такий вигляд:

Image

Вона надходить уже заповненою. Далі користувач може її змінити:

Image

Якщо він натисне кнопку [Envoyer], сервер поверне йому список значень форми:

Image

Форма — це статична сторінка HTML balises.html:

<html>

  <head>
      <title>balises</title>
    <script language="JavaScript">
        function effacer(){
          alert("Vous avez cliqué sur le bouton Effacer");
      }//очищення
        </script>
  </head>

  <body background="/images/standard.jpg">
    <form method="POST" action="parameters.php">
      <table border="0">
        <tr>
          <td>Etes-vous marié(e)</td>
          <td>
              <input type="radio" value="oui" name="R1">Oui
              <input type="radio" name="R1" value="non" checked>Non
          </td>
        </tr>
        <tr>
          <td>Cases à cocher</td>
          <td>
              <input type="checkbox" name="C1" value="un">1
              <input type="checkbox" name="C2" value="deux" checked>2
              <input type="checkbox" name="C3" value="trois">3
          </td>
        </tr>
        <tr>
          <td>Champ de saisie</td>
          <td>
              <input type="text" name="txtSaisie" size="20" value="qqs mots">
          </td>
        </tr>
        <tr>
          <td>Mot de passe</td>
          <td>
              <input type="password" name="txtMdp" size="20" value="unMotDePasse">
          </td>
        </tr>
        <tr>
          <td>Boîte de saisie</td>
          <td>
               <textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>
          </td>
        </tr>
        <tr>
          <td>combo</td>
          <td>
              <select size="1" name="cmbValeurs">
                <option>choix1</option>
                <option selected>choix2</option>
                <option>choix3</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix simple</td>
          <td>
              <select size="3" name="lst1">
                <option selected>liste1</option>
                <option>liste2</option>
                <option>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix multiple</td>
          <td>
              <select size="3" name="lst2[]" multiple>
                <option selected>liste1</option>
                <option>liste2</option>
                <option selected>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>bouton</td>
          <td>
              <input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">
          </td>
        </tr>
        <tr>
          <td>envoyer</td>
          <td>
              <input type="submit" value="Envoyer" name="cmdRenvoyer">
          </td>
        </tr>
        <tr>
          <td>rétablir</td>
          <td>
              <input type="reset" value="Rétablir" name="cmdRétablir">
          </td>
        </tr>
      </table>
      <input type="hidden" name="secret" value="uneValeur">
    </form>
  </body>
</html>

У таблиці нижче наведено опис ролі різних тегів цього документа та значення, яке отримує PHP для різних типів компонентів форми. Значення поля з іменем HTML C може бути надіслане за допомогою POST або GET. У першому випадку воно буде отримано у змінній $_GET["C"], а в другому — у змінній $_POST["C"]. У наведеній нижче таблиці передбачається використання POST.

Перевірка
тегу HTML
значення, отримане за допомогою PHP
formulaire
<form method="POST" >
 
champ de 
saisie
<input type="text" name="txtSaisie" size="20" value="кілька слів">
$_POST["txtSaisie"]: значення, що міститься в полі txtSaisie форми
champ de
 saisie cachée
<input type="password" name="txtMdp" size="20" value="unMotDePasse">
$_POST["txtmdp"]: значення, що міститься в полі txtMdp форми
champ de 
saisie
multilignes
<textarea rows="2" name="areaSaisie" cols="20">
рядок1
рядок 2
рядок 3
</textarea>
$_POST["areaSaisie"]: рядки, що містяться в полі areaSaisie у вигляді єдиного рядка символів: "ligne1\r\nligne2\r\nligne3". Рядки розділяються між собою послідовністю «\r\n».
boutons radio
<input type="radio" value="Так" name="R1">Так
<input type="radio" name="R1" value="no" checked>Ні
$_POST["R1"]: значення (=value) перемикача, позначеного як «так» або «ні» залежно від випадку.
cases à
cocher
<input type="checkbox" name="C1" value="один">1
<input type="checkbox" name="C2" value="два" checked>2
<input type="checkbox" name="C3" value="три">3
$_POST["C1"]: значення (=value) поля, якщо воно відмічене; інакше змінна не існує. Отже, якщо поле C1 відмічено, $_POST["C1"] дорівнює «один», інакше $_POST["C1"] не існує.
Combo
<select size="1" name="cmbValeurs">
<option>варіант1</option>
<option selected>варіант 2</option>
<option>варіант3</option>
</select>
$_POST["cmbValeurs"]: опція, вибрана зі списку, наприклад «варіант3».
liste à
sélection
unique
<select size="3" name="lst1">
<option selected>список1</option>
<option>список2</option>
<option>список3</option>
<option>список4</option>
<option>список5</option>
</select>
$_POST["lst1"]: опція, обрана зі списку, наприклад «liste5».
liste à 
sélection
multiple
<select size="3" name="lst2[]" multiple>
<option>список1</option>
<option>список2</option>
<option selected>список3</option>
<option>список4</option>
<option>список5</option>
</select>
$_POST["lst2"]: масив вибраних із списку опцій, наприклад ["liste3,"liste5"]. Зверніть увагу на особливий синтаксис тегу HTML для цього конкретного випадку: lst2[].
champ caché
<input type="hidden" name="secret" value="uneValeur">
$_POST["secret"]: значення (=value) поля, у даному випадку «uneValeur».

У нашому прикладі значення з форми передаються до програми parameters.php:

    <form method="POST" action="parameters.php">

Код останньої програми такий:

<?php

   // конфігурація
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // метод виклику
  $méthode=$_SERVER["REQUEST_METHOD"];

   // отримання параметрів
   // залежить від способу їх надсилання
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  $R1=$param["R1"];
  $C1=$param["C1"];
  $C2=$param["C2"];
  $C3=$param["C3"];
  $txtSaisie=$param["txtSaisie"];    
  $txtMdp=$param["txtMdp"];
  $areaSaisie=implode("<br>",explode("\r\n",$param["areaSaisie"]));
  $cmbValeurs=$param["cmbValeurs"];
  $lst1=$param["lst1"];
  $lst2=implode("<br>",$param["lst2"]);
  $secret=$param["secret"];

     // запит дійсний?
    $requêteValide=isset($R1) && (isset($C1) || isset($C2) || isset($C3))
        && isset($txtSaisie) && isset($txtMdp) && isset($areaSaisie)
        && isset($cmbValeurs) && isset($lst1) && isset($lst2) 
        && isset($secret);

   // відображення сторінки
    if ($requêteValide)
      include "parameters-p1.php";
    else include "balises.html";                    
?>

Розглянемо детальніше деякі аспекти цієї програми:

  • додаток не залежить від способу передачі значень з форми на сервер. В обох можливих випадках (GET та POST) словник переданих значень посилається на $param.
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  • на основі вмісту поля areaSaisie «рядок1\r\nрядок2\r\n...» створюється масив рядків за допомогою функції explode("\r\n", $param["areaSaisie"]). Отже, ми отримуємо масив [ligne1,ligne2,...]. На його основі за допомогою функції implode створюється рядок "ligne1<br>ligne2<br>...".
  • значення списку з можливістю множинного вибору lst2 є масивом, наприклад ["option3","option5"]. На основі нього за допомогою функції implode створюється символьний рядок «option3<br>option5».
  • програма перевіряє, чи всі параметри задано. Тут слід пам’ятати, що будь-яку функцію URL можна викликати вручну або програмно, і що очікувані параметри не завжди присутні. Якщо параметри відсутні, відображається сторінка balises.html, інакше — сторінка parameters-p1.php. На ній у таблиці відображаються значення, отримані та обчислені в parameters.php:
<html>
    <head>
        <title>Récupération des paramètres d'un formulaire</title>
    </head>
    <body>
        <table border="1">
            <tr>
                <td>R1</td>
                <td><?php echo $R1 ?></td>
            </tr>
            <tr>
                <td>C1</td>
                <td><?php echo $C1 ?></td>
            </tr>
            <tr>
                <td>C2</td>
                <td><?php echo $C2 ?></td>
            </tr>
            <tr>
                <td>C3</td>
                <td><?php echo $C3 ?></td>
            </tr>
            <tr>
                <td>txtSaisie</td>
                <td><?php echo $txtSaisie ?></td>
            </tr>
            <tr>
                <td>txtMdp</td>
                <td><?php echo $txtMdp ?></td>
            </tr>
            <tr>
                <td>areaSaisie</td>
                <td><?php echo $areaSaisie ?></td>
            </tr>
            <tr>
                <td>cmbValeurs</td>
                <td><?php echo $cmbValeurs ?></td>
            </tr>
            <tr>
                <td>lst1</td>
                <td><?php echo $lst1 ?></td>
            </tr>
            <tr>
                <td>lst2</td>
                <td><?php echo $lst2 ?></td>
            </tr>
            <tr>
                <td>secret</td>
                <td><?php echo $secret ?></td>
            </tr>
        </table>
    </body>
    </html>    

3.10. Відстеження сеансу

3.10.1. Проблема

Веб-додаток може складатися з декількох обмінів формами між сервером і клієнтом. У цьому випадку процес відбувається наступним чином:

крок 1

  1. клієнт C1 встановлює з’єднання з сервером і надсилає початковий запит.
  2. сервер надсилає форму F1 клієнту C1 і закриває з’єднання, відкрите на кроці 1.

Крок 2

  1. клієнт C1 заповнює форму та надсилає її назад на сервер. Для цього браузер відкриває нове з’єднання з сервером.
  2. Сервер обробляє дані форми 1, обчислює на їх основі інформацію I1, надсилає форму F2 клієнту C1 і закриває з’єднання, відкрите на кроці 3.

Крок 3

  1. Цикл етапів 3 і 4 повторюється на етапах 5 і 6. По завершенні кроку 6 сервер отримає дві форми F1 та F2 і на їх основі обчислить дані I1 та I2.

Постає таке питання: як серверу зберегти дані I1 та I2, пов’язані з клієнтом C1? Цю проблему називають відстеженням сеансу клієнта C1. Щоб зрозуміти її суть, розглянемо схему серверного додатка TCP-IP, який одночасно обслуговує декількох клієнтів:

У класичному клієнт-серверному додатку TCP-IP:

  • клієнт встановлює з’єднання із сервером
  • через нього обмінюється даними з сервером
  • з’єднання закривається одним із двох учасників

Два важливі моменти цього механізму:

  1. для кожного клієнта створюється окреме з’єднання
  2. це з'єднання використовується протягом усього діалогу сервера зі своїм клієнтом

Те, що дозволяє серверу в будь-який момент знати, з яким клієнтом він працює, — це з’єднання, або, інакше кажучи, «канал», що з’єднує його з клієнтом. Оскільки цей канал призначений для конкретного клієнта, все, що надходить по ньому, походить від цього клієнта, а все, що надсилається по ньому, надходить до того самого клієнта.

Механізм «клієнт-сервер» HTTP відповідає наведеному вище схемі, однак має таку особливість: взаємодія між клієнтом і сервером обмежується єдиним обміном даними між ними:

  • клієнт відкриває з’єднання з сервером і надсилає запит
  • сервер надає відповідь і закриває з’єднання

Якщо в момент часу T1 клієнт C надсилає запит до сервера, він отримує з’єднання C1, яке буде використовуватися для єдиного обміну запитом і відповіддю. Якщо в момент часу T2 цей самий клієнт надсилає другий запит на сервер, він отримає з’єднання C2, яке відрізняється від з’єднання C1. З точки зору сервера між цим другим запитом користувача C та його початковим запитом немає жодної різниці: в обох випадках сервер розглядає клієнта як нового клієнта. Щоб встановити зв’язок між різними з’єднаннями клієнта C із сервером, клієнт C повинен бути «впізнаний» сервером як «постійний користувач», а сервер — отримати інформацію, яку він має про цього постійного користувача.

Уявімо собі систему управління, яка працювала б таким чином:

  • Існує єдина черга
  • Є кілька вікон. Отже, одночасно можна обслуговувати кількох клієнтів. Коли одне вікно звільняється, клієнт залишає чергу, щоб отримати обслуговування в цьому вікні
  • Якщо клієнт звертається вперше, працівник за вікном видає йому жетон із номером. Клієнт може поставити лише одне запитання. Отримавши відповідь, він повинен залишити вікно та стати в кінець черги. Працівник за вікном записує дані цього клієнта в папку з відповідним номером.
  • Коли знову настає його черга, клієнта може обслуговувати інший працівник, ніж попереднього разу. Той просить у нього жетон і дістає справу з номером, що відповідає номеру жетона. Клієнт знову звертається із запитом, отримує відповідь, і до його справи додають інформацію.
  • І так далі... Згодом клієнт отримає відповіді на всі свої запити. Відстеження взаємозв’язку між різними запитами здійснюється за допомогою жетона та пов’язаної з ним справи.

Механізм відстеження сеансу у веб-додатку «клієнт-сервер» аналогічний описаному вище:

  • під час першого запиту клієнт отримує токен від веб-сервера
  • він надаватиме цей токен при кожному наступному запиті для ідентифікації

Токен може мати різні форми:

  • поле, приховане у формі
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь (форму) і розміщує токен у прихованому полі цієї форми. У цей момент з’єднання закривається (клієнт залишає вікно з своїм токеном). Сервер, за необхідності, подбав про прив’язку інформації до цього токена.
    • Клієнт надсилає другий запит, відправляючи форму. Сервер витягує з неї токен. Після цього він може обробити другий запит клієнта, маючи доступ, завдяки токену, до інформації, обчисленої під час першого запиту. До файлу, пов’язаного з токеном, додаються нові дані, клієнту надсилається друга відповідь, і з’єднання закривається вдруге. Токен знову вставляється у форму відповіді, щоб користувач міг надати його під час наступного запиту.
    • І так далі...

Головним недоліком цієї техніки є те, що токен потрібно вставляти у форму. Якщо відповідь сервера не є формою, метод прихованого поля більше не можна використовувати.

  • Метод із використанням файлу cookie
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь, додавши файл cookie до заголовків HTTP цієї відповіді. Це здійснюється за допомогою команди HTTP Set-Cookie:

Set-Cookie: param1=значення1;param2=значення2;....

де parami — це імена параметрів, а valeursi — їхні значення. Серед параметрів буде токен. Дуже часто у файлі cookie міститься лише токен, а інша інформація зберігається сервером у папці, пов’язаній із цим токеном. Браузер, який отримує файл cookie, зберігає його у файлі на диску. Після відповіді сервера з’єднання закривається (клієнт залишає сервіс зі своїм токеном).

  • (продовження)
    • клієнт надсилає другий запит на сервер. Кожного разу, коли надсилається запит на сервер, браузер перевіряє серед усіх своїх файлів cookie, чи є серед них файл від запитуваного сервера. Якщо так, він надсилає його серверу, як і раніше, у вигляді команди HTTP — команди Cookie, синтаксис якої аналогічний синтаксису команди Set-Cookie, що використовується сервером:

Cookie: param1=значення1;param2=значення2;....

Серед заголовків HTTP, надісланих браузером, сервер знайде токен, що дозволить йому впізнати клієнта та знайти пов’язану з ним інформацію.

Це найпоширеніший тип токена. Він має один недолік: користувач може налаштувати свій браузер так, щоб той не приймав файли cookie. У такому разі користувач не матиме доступу до веб-додатків, що використовують файли cookie.

  • переписано з URL
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь. Вона містить посилання, якими користувач повинен скористатися, щоб продовжити роботу з додатком. У URL кожного з цих посилань сервер додає токен у формі URL;token=значення.
    • коли користувач натискає на одне з посилань, щоб продовжити роботу з додатком, браузер надсилає запит на веб-сервер, вказуючи в заголовках HTTP, URL, URL;token=запитане значення. Тоді сервер може отримати цей токен.

3.10.2. API та PHP для відстеження сеансу

Тепер розглянемо основні методи, корисні для відстеження сеансу:

session_start()
запускає сесію, до якої належить поточний запит. Якщо запит ще не належав до жодної сесії, то така сесія створюється.
session_id()
ідентифікатор поточної сесії
$_SESSION[$var]
словник, що зберігає дані сесії. Доступний для читання та запису
session_destroy()
видаляє дані, що містяться в поточній сесії. Ці дані залишаються доступними для поточного обміну даними між клієнтом і сервером, але будуть недоступними під час наступного обміну.

3.10.3. Приклад 1

Ми наводимо приклад, натхненний книгою «Програмування з J2EE», виданою видавництвом Wrox та розповсюджуваною видавництвом Eyrolles. Цей приклад дозволяє побачити, як працює сесія PHP. Головна сторінка виглядає так:

Image

На ній містяться такі елементи:

  • ID сеансу, отриманий за допомогою функції session_id(). Цей ID, згенерований браузером, надсилається клієнту за допомогою файлу cookie, який браузер повертає під час запиту на URL з тієї самої деревоподібної структури. Саме це забезпечує збереження сесії.
  • лічильник, який збільшується з кожним запитом браузера і показує, що сесія дійсно підтримується.
  • посилання, що дозволяє видалити дані, пов’язані з поточною сесією. Це здійснюється за допомогою функції session_destroy()
  • посилання для перезавантаження сторінки

Код програми cycledevie.php виглядає так:

<?php
     //cycledevie.php

     // конфігурація
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // запускаємо сеанс
  session_start();

   // чи потрібно її скасувати?
  $action=$_GET["action"];
  if($action=="invalider"){
       // завершення сеансу
    session_destroy();
  }//if

     // управління лічильником
  if(! isset($_SESSION["compteur"]))
         // лічильник не існує — створюємо його
      $_SESSION["compteur"]=0;
         // лічильник існує — збільшуємо його значення
    else $_SESSION["compteur"]++;

   // отримуємо ID поточної сесії
  $idSession=session_id();

   // отримуємо лічильник
  $compteur=$_SESSION["compteur"];

   // передаємо управління сторінці перегляду
  include "cycledevie-p1.php";
?>

Зверніть увагу на наступні моменти:

  • з моменту запуску програми розпочинається сесія. Якщо клієнт надіслав токен сесії, то відновлюється саме та сесія, що має цей ідентифікатор, а всі пов’язані з нею дані розміщуються у словнику $_SESSION. В іншому випадку створюється новий токен сесії.
  • якщо клієнт надіслав параметр action із значенням «invalider», дані сесії позначаються як такі, що «підлягають видаленню» під час наступного обміну. На відміну від звичайного обміну, вони не будуть збережені на сервері після завершення обміну.
  • З словника $_SESSION отримується лічильник, пов’язаний із сесією, а також сесійний ID (session_id()).
  • Сторінка, яку потрібно надіслати клієнту, генерується програмою cycledevie-p1.php

Сторінка cycledevie-p1.php відображає сторінку, надіслану клієнту:

<html>
    <head>
      <title>Gestion de sessions</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : <?php echo $idSession ?>
    <br>compteur : <?php echo $compteur ?>    
    <br><a href="cycledevie.php?action=invalider">Invalider la session</a>
    <br><a href="cycledevie.php">Recharger la page</a>
  </body>
</html>

Зверніть увагу на код URL, доданий до кожного з цих двох посилань:

  1. cycledevie.php — для перезавантаження сторінки
  2. cycledevie.php?action=invalider — для скасування сеансу. У цьому випадку до URL додається параметр action=invalider. Він буде отриманий серверною програмою cycledevie.php за допомогою команди $action=$_GET["action"].

Перезавантажимо сторінку двічі поспіль:

Image

Лічильник успішно збільшився. Ідентифікатор сеансу ID не змінився. Тепер скасуємо сеанс:

Image

Бачимо, що ідентифікатор сесії ID втрачено, але лічильник знову збільшився. Перезавантажимо сторінку:

Image

Бачимо, що ми починаємо знову з тим самим ідентифікатором сесії ID, що й раніше. Лічильник же обнуляється. Отже, функція session_destroy() не має негайного ефекту. Дані поточної сесії видаляються лише для обміну даними між клієнтом і сервером, що йде слідом за тим, під час якого було здійснено видалення. Сесія ID не змінилася, що, ймовірно, свідчить про те, що session_destroy() не запускає нову сесію зі створенням нового ID. Файл cookie з токеном сеансу був відправлений назад клієнтським браузером, і PHP відновив сеанс за цим токеном, у якому вже не було даних.

Попередні тести проводилися з браузером Netscape, налаштованим на використання файлів cookie. Тепер налаштуємо його так, щоб він не використовував ці файли. Це означає, що він не зберігатиме та не відправлятиме файли cookie, надіслані сервером. Отже, очікується, що сесії більше не працюватимуть. Спробуємо з першим обміном:

Image

Ми отримали сесійний ID, згенерований функцією session_start(). Перезавантажимо сторінку за посиланням:

Image

На диво, наведені вище результати показують, що сесія триває і лічильник працює правильно. Як це можливо, адже кукі вимкнено, тож обмін токенами між сервером і браузером більше не відбувається? Ідентифікатор URL зі знімка екрана вище дає нам відповідь:

http://localhost/poly/sessions/1/cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c

Це URL з посилання «Оновити сторінку». Перевіримо вихідний код сторінки, що відображається браузером:

<a href="cycledevie.php?action=invalider&PHPSESSID=587ce26f943a288d8f41212e30fed13c">Invalider la session</a>
<a href="cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c">Recharger la page</a>

Нагадаємо, що початковий код обох посилань на сторінці cycledevie-p1.php такий:

    <a href="cycledevie.php?action=invalider">Invalider la session</a>
    <a href="cycledevie.php">Recharger la page</a>

Отже, інтерпретатор PHP самостійно переписав URL обох посилань, додавши до них токен сеансу. Таким чином, цей токен успішно передається браузером під час активації посилань. Це пояснює, чому навіть без увімкнених файлів cookie сеанс залишається правильно керованим.

3.10.4. Приклад 3

Ми пропонуємо написати PHP-додаток, який буде клієнтом попереднього додатка compteur. Він викликатиме його N разів поспіль, де N передаватиметься як параметр. Наша мета — продемонструвати запрограмований веб-клієнт та спосіб управління токеном сесії. Нашою відправною точкою буде загальний веб-клієнт, що викликається наступним чином:

clientweb URL GET/HEAD

  • URL: запитувана URL-адреса
  • GET/HEAD: GET — щоб отримати код HTML зі сторінки, HEAD — щоб обмежитися лише заголовками, HTTP

Ось приклад із URL http://localhost/poly/sessions/2/cycledevie.php. Ця програма є тією, що вже описувалася, з невеликою відмінністю:

   // встановлюємо шлях до файлу cookie
    session_set_cookie_params(0,"/poly/sessions/2");
  // запускаємо сесію
  session_start();

Функція session_set_cookie_params дозволяє встановити певні параметри файлу cookie, який міститиме сесійний токен. Перший параметр — це термін дії файлу cookie. Нульовий термін дії означає, що файл cookie видаляється браузером, який його отримав, після закриття браузера. Другий параметр — це шлях до сервера URL, на який браузер повинен відправити файл cookie. У наведеному вище прикладі, якщо браузер отримав файл cookie від машини localhost, він надішле цей файл cookie будь-якій машині URL, що знаходиться в дереві http://localhost/poly/sessions/2/.

dos>e:\php43\php.exe clientweb.php http://localhost/poly/sessions/2/cycledevie.php GET
HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 13:58:16 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : 48d5aaa0e99850b17c33a6e22d38e5c4    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Recharger la page</a>
  </body>
</html>

Програма clientweb відображає все, що вона отримує від сервера. Вище показано команду HTTP Set-cookie, за допомогою якої сервер надсилає файл cookie своєму клієнту. У цьому випадку файл cookie містить дві інформації:

  • PHPSESSID — це токен сеансу
  • path, що визначає URL, до якої належить файл cookie. path=/poly/sessions/2 вказує браузеру, що він повинен повертати файл cookie серверу щоразу, коли надсилатиме запит на URL, що починається з /poly/sessions/2, з того комп’ютера, який надіслав йому цей файл cookie.
  • Файл cookie також може визначати термін дії. У даному випадку ця інформація відсутня. Отже, файл cookie буде видалено після закриття браузера. Термін дії файлу cookie може становити, наприклад, N днів. Поки файл cookie є дійсним, браузер надсилатиме його щоразу, коли буде переглядатися будь-який із файлів URL з його домену (Path). Візьмемо, наприклад, інтернет-магазин CD. Він може відстежувати пересування клієнта по своєму каталогу та поступово визначати його вподобання: наприклад, класичну музику. Ці вподобання можна зберегти у файлі cookie, термін дії якого становить 3 місяці. Якщо цей самий клієнт повернеться на сайт через місяць, браузер надішле файл cookie серверному додатку. На основі інформації, що міститься у файлі cookie, додаток зможе адаптувати згенеровані сторінки до вподобань клієнта.

Далі наведено код веб-клієнта.

<?php

     // конфігурація
    dl("php_curl.dll");    // бібліотека CURL

     // синтаксис: $0 URL GET
   // потрібні три аргументи
  if($argc != 3){
       // повідомлення про помилку
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // зупинка
    exit(1);
  }//if

   // третій аргумент має бути GET або HEAD
  $header=strtolower($argv[2]);
  if($header!="get" && $header!="head"){
       // повідомлення про помилку
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
     // зупинка
    exit(2);
  }//if

   // перший аргумент — це URL
  $URL=strtolower($argv[1]);

  // підготовка з'єднання  
  $connexion=curl_init($URL);
   // налаштування з'єднання
  curl_setopt($connexion,CURLOPT_HEADER,1);  
  if($header=="head") curl_setopt($connexion,CURLOPT_NOBODY,1);
  // виконання з'єднання
  curl_exec($connexion);
   // закриття з'єднання
  curl_close($connexion);
   // кінець
  exit(0);  
?>

Попередня програма використовує бібліотеку CURL:

$connexion=curl_init($URL)
ініціалізує об’єкт CURL із цільовим об’єктом URL
curl_setopt ($connexion,option,valeur)
встановлює значення деяких параметрів з’єднання. Наведемо два, що використовуються в програмі:
CURLOPT_HEADER=1: дозволяє отримувати HTTP-заголовки, надіслані сервером
CURLOPT_NOBODY=1: дозволяє ігнорувати документ, надісланий сервером, за заголовками HTTP
curl_exec($connexion)
встановлює з'єднання з $URL із заданими параметрами. Виводить на екран усе, що надсилає сервер
curl_close($connexion)
закриває з'єднання

Попередня програма досить проста. Однак бібліотека CURL не дозволяє детально обробляти відповідь сервера, наприклад, аналізувати її рядок за рядком. Наступна програма виконує те саме, що й попередня, але з використанням базових мережевих функцій PHP. Вона стане відправною точкою для написання клієнта до нашого додатка cycledevie.php.

<?php

     // синтаксис: $0 URL GET/HEAD
  // потрібні три аргументи
  if($argc != 3){
       // повідомлення про помилку
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // зупинка
    exit(1);
  }//if

   // підключення та відображення результату
  $résultats=getURL($argv[1],$argv[2]);
  if(isset($résultats->erreur)){
       // помилка
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  }else{
       // виведення відповіді сервера
    echo $résultats->réponse;
  }//if  

   // кінець
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$header){

      // підключається до $URL
     // створює GET або HEAD залежно від значення заголовка
     // відповідь сервера формує результат функції

     // аналіз URL
    $url=parse_url($URL);
     //— протокол
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//якщо
     // машина
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // порт
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // шлях
    $chemin=$url["path"];
     // запит
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if

     // аналіз $header
    $header=strtoupper($header);
      if($header!="GET" && $header!="HEAD"){
          // повідомлення про помилку
        $résultats->erreur="méthode [$header] doit être GET ou HEAD";
         // зупинка
        return $résultats;
      }//if

     // відкриття з'єднання на порту $port з $hote
    $connexion=fsockopen($hote,$port,&$errno,&$erreur);
     // повернення у разі помилки
    if(! $connexion){
      $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
      return $résultats;
    }//if

     // $connexion представляє двонаправлений канал зв’язку
     // між клієнтом (цією програмою) та веб-сервером, до якого звертаються
     // цей канал використовується для обміну командами та інформацією
     // протокол взаємодії — HTTP

     // клієнт надсилає команду get для запиту URL /
    // синтаксис get: URL HTTP/1.0
    // заголовки (headers) протоколу HTTP повинні закінчуватися порожнім рядком
    fputs($connexion, "$header $chemin HTTP/1.0\n\n");

     // сервер тепер відповість на каналі $connexion. Він надішле всі
     // ці дані, а потім закриє канал. Отже, клієнт зчитує все, що надходить з $connexion
     // до закриття каналу
    $résultats->réponse="";
    while($ligne=fgets($connexion,10000))
      $résultats->réponse.=$ligne;

     // клієнт, у свою чергу, закриває з’єднання
    fclose($connexion);
     // повернення
    return $résultats;
  }//getURL

?>

Прокоментуємо деякі моменти цієї програми:

  • програма приймає два параметри:
    • посилання URL у форматі http, вміст якого потрібно відобразити на екрані;
    • метод GET або HEAD, який слід використовувати залежно від того, чи потрібно лише заголовки HTTP (HEAD), чи також тіло документа, пов’язане з URL (GET).
  • Обидва параметри передаються до функції getURL. Ця функція повертає об’єкт $résultats. Він містить поле erreur, якщо сталася помилка, або поле réponse в іншому випадку. Поле erreur призначене для зберігання можливого повідомлення про помилку. Поле réponse — це відповідь веб-сервера, до якого було здійснено звернення.
  • Функція getURL аналізує URL та $URL за допомогою функції parse_url. Інструкція $url=parse_url($URL) створить асоціативний масив $url з такими можливими ключами:
    • scheme: протокол URL (http, ftp, ..)
    • host: комп’ютер URL
    • port: порт URL
    • path: шлях до URL
    • querystring: параметри URL

URL-адреса вважається правильною, якщо має вигляд http://machine[:port][/chemin].

  • Параметр $header також перевіряється
  • після перевірки та підтвердження правильності параметрів створюється з’єднання TCP на машині ($hote, $port), потім надсилається команда HTTP, GET або HEAD залежно від параметра $header.
  • потім зчитується відповідь сервера, яка записується в $résultats->відповідь.

Виконання програми дає такі результати:

dos>"e:\php43\php.exe" geturl.php http://localhost/poly/sessions/2/cycledevie.php get

HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 14:56:55 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=ea0d2673811ed069e7289d86933a4c0a; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : ea0d2673811ed069e7289d86933a4c0a    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Recharger la page</a>
  </body>
</html>

Уважний читач, мабуть, помітив, що відповідь сервера відрізняється залежно від використовуваної клієнтської програми. У першому випадку сервер надіслав заголовок HTTP: Transfer-Encoding: chunked, який у другому випадку не був надісланий. Це пов’язано з тим, що другий клієнт надіслав заголовок HTTP: get URL HTTP/1.0, який запитує URL і вказує, що він працює з протоколом HTTP версії 1.0, що змушує сервер відповісти йому тим самим протоколом. Однак заголовок HTTP Transfer-Encoding: chunked належить до протоколу HTTP версії 1.1. Тому сервер не використав його у своїй відповіді. Це свідчить про те, що перший клієнт надіслав свій запит, вказавши, що працює з протоколом HTTP версії 1.1.

Тепер ми створюємо програму clientCompteur, яка викликається наступним чином:

clientCompteur URL N [JSESSIONID]

  • URL: URL-адреса додатка cycledevie
  • N: кількість викликів, які потрібно здійснити до цього додатка
  • PHPSESSID: необов’язковий параметр — токен сеансу

Мета програми — N разів викликати додаток cycledevie.php, керуючи сесійним файлом cookie та щоразу відображаючи значення лічильника, яке повертає сервер. Після завершення N викликів його значення має дорівнювати N-1. Ось перший приклад виконання:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 3
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 2]

Програма виводить:

  • заголовки HTTP, які вона надсилає на сервер у вигляді --> entêteEnvoyé
  • заголовки HTTP, які вона отримує у вигляді <-- entêteReçu
  • значення лічильника після кожного виклику

Бачимо, що під час першого виклику:

  • клієнт не надсилає кукі
  • сервер надсилає його (Set-Cookie:)

Під час наступних звернень:

  • клієнт систематично надсилає файл cookie, який він отримав від сервера під час першого запиту. Саме це дозволить серверу впізнати його та збільшити значення лічильника.
  • сервер, у свою чергу, більше не надсилає cookie

Ми запускаємо попередню програму, передаючи вищезазначений токен як третій параметр:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 1 2425e00d1d65c2bdcbafc1ce6244f7ea

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:32:03 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 3]

Тут видно, що вже під час першого запиту клієнта сервер отримує дійсний сесійний файл cookie. Можливо, це вказує на потенційну уразливість. Якщо мені вдасться перехопити в мережі сесійний токен, я зможу видати себе за того, хто ініціював цю сесію. У нашому прикладі перший запит (без сесійного токена) представляє користувача, який ініціює сесію (можливо, за допомогою логіна та пароля, що надають йому право на отримання токена), а другий запит (із сесійним токеном) — користувача, який «викрав» сесійний токен із першого запиту. Якщо поточна операція є банківською, це може стати серйозною проблемою...

Код клієнта виглядає так:

<?php

     // синтаксис: $0 URL N [PHPSESSID]
   // потрібні три аргументи
  if($argc!=3 && $argc!=4){
       // повідомлення про помилку
    fputs(STDERR,"Syntaxe : $argv[0] URL N [PHPSESSID]");
    // зупинка
    exit(1);
  }//if

   // отримання параметрів
  $URL=$argv[1];
  $N=$argv[2];
  $PHPSESSID=$argv[3];

  // підключення та відображення результату
  $résultats=getURL($URL,$N,$PHPSESSID);
  if(isset($résultats->erreur)){
       // помилка
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  } 

   // завершення
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$N,$PHPSESSID){

       // підключається до URL
     // виконує GET або HEAD залежно від значення заголовка
     // відповідь сервера формує результат функції

     // аналіз URL
    $url=parse_url($URL);
     //— протокол
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//якщо
     // машина
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if
     // порт
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // шлях
    $chemin=$url["path"];
     // запит
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//if

         // перевірка $N
    if (! preg_match("/^\d+$/",$N)){
        // помилка
      $résultats->erreur="nombre [$N] erroné";
       // кінець
      return $résultats;
    }//if

     // виконуються виклики $N до $URL
    for($i=0;$i<$N;$i++){
      // відкриття з'єднання на порту $port сервера $hote
      $connexion=fsockopen($hote,$port,&$errno,&$erreur);
       // повернення у разі помилки
      if(! $connexion){
        $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
        return $résultats;
      }//if

       // $connexion представляє двонаправлений канал зв’язку
       // між клієнтом (цією програмою) та веб-сервером, до якого звертаються
       // цей канал використовується для обміну командами та інформацією
       // протокол взаємодії — HTTP

       // клієнт надсилає заголовки HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: хост:порт
      envoie($connexion, "Host: $hote:$port\n");
       // Connection: close
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // порожній рядок
      envoie($connexion,"\n");      

       // сервер тепер відповідатиме на каналі $connexion. Він надішле всі
       // свої дані, а потім закриє канал.
       // Клієнт спочатку зчитує заголовки HTTP, що закінчуються порожнім рядком
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // відлуння рядка
        echo "<-- $ligne\n";
         // пошук токена, якщо його ще не знайдено
        if(! $PHPSESSID){
             // пошук рядка set-cookie
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // токен знайдено — його запам'ятовуємо
            $PHPSESSID=$champs[1];            
          }//if
        }//if
        // пошук режиму передачі документа
        if(! $CHUNKED){
             // пошук рядка Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // передача частинами
            $CHUNKED=1;                        
          }//if
        }//if
      }//наступний рядок

       // відображення рядка
      echo "<-- $ligne\n";

       // читання документа залежить від способу його надсилання
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);

       // пошук лічильника в документі
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // лічильник знайдено — його відображаємо
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//if

       // клієнт закриває з'єднання
      fclose($connexion);
    }//for i
    }//getURL

  //--------------------------
  function getDoc($connexion){
      // читання документа на $connexion
    $doc="";
    while($ligne=fread($connexion,10000))
        $doc.=$ligne;
     // кінець
    return $doc;
  }//getDoc

  //--------------------------
  function getChunkedDoc($connexion){
       // читання документа на $connexion
     // цей документ надсилається частинами у вигляді
     // кількість символів у фрагменті в шістнадцятковій системі числення
     // продовження фрагмента
         // порожній рядок

     // у першому рядку зчитується розмір фрагмента
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // зчитування наступного документа
    $doc="";
    while($taille!=0){
         // зчитування фрагмента довжиною $taille символів
      $doc.=fread($connexion,$taille);
       // порожній рядок
      fgets($connexion,10000);
       // наступний фрагмент
       // зчитується розмір фрагмента
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//while
     // завершено
    return $doc;
  }// getChunkedDoc  

  //--------------------------
  function envoie($flux,$msg){
       // надсилає $msg на $flux
    fwrite($flux,$msg);
     // відобразити на екрані
    echo "--> $msg";
  }//надсилає
?>

Розберемо основні моменти цієї програми:

  • потрібно виконати N обмінів між клієнтом і сервером. Саме тому вони знаходяться в циклі
            for($i=0;$i<$N;$i++){
  • під час кожного обміну клієнт відкриває з’єднання TCP-IP із сервером. Після встановлення з’єднання він надсилає серверу заголовки HTTP свого запиту:
<?php
...
       // клієнт надсилає заголовки HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: хост:порт
      envoie($connexion, "Host: $hote:$port\n");
       // Connection: close
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // порожній рядок
      envoie($connexion,"\n");      

Якщо токен PHPSESSID доступний, він надсилається у вигляді файлу cookie, інакше — ні. Слід зазначити, що клієнт вказав, що працює з протоколом HTTP/1.1. Це пояснює, чому згодом сервер надішле йому заголовок HTTP: Transfer-Encoding: chunked, який належить до протоколу HTTP/1.1, але не до протоколу HTTP/1.0.

  • Після відправлення запиту клієнт очікує відповіді від сервера. Спочатку він аналізує заголовки HTTP цієї відповіді. У ній він шукає два рядки:
Cookie : 
Transfer-Encoding: chunked

Рядок «Cookie:» — це заголовок HTTP, який містить сесійний токен PHPSESSID. Клієнт повинен отримати його, щоб надіслати серверу під час наступного обміну даними. Рядок Transfer-Encoding: chunked, якщо він присутній, вказує, що сервер надсилатиме документ частинами. Кожна частина надсилається клієнту у такому форматі:

[nombre de caractères du document en hexadécimal]
[document]
[ligne vide]

Якщо рядок Transfer-Encoding: chunked відсутній, документ надсилається одним разом після порожнього рядка заголовків HTTP. Отже, залежно від наявності чи відсутності цього рядка, спосіб отримання документа буде відрізнятися. Код обробки заголовків HTTP є таким:

<?php
...
       // Клієнт спочатку зчитує заголовки HTTP, що закінчуються порожнім рядком
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // відображення рядка
        echo "<-- $ligne\n";
         // пошук токена, якщо його ще не знайдено
        if(! $PHPSESSID){
             // пошук рядка set-cookie
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // токен знайдено — його запам'ятовуємо
            $PHPSESSID=$champs[1];            
          }//if
        }//if
        // пошук режиму передачі документа
        if(! $CHUNKED){
             // пошук рядка Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // передача частинами
            $CHUNKED=1;                        
          }//if
        }//if
      }//наступний рядок
  • коли токен буде знайдено вперше, його більше не шукатимуть під час наступних звернень до сервера. Після обробки заголовків HTTP у відповіді переходять до документа, що йде за заголовками HTTP. Цей документ читається по-різному залежно від способу передачі:
<?php
...

       // читання документа залежить від способу його надсилання
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);
  • У отриманому документі $document шукаємо рядок, що містить значення лічильника. Цей пошук також здійснюється за допомогою регулярного виразу:
<?php
...
       // пошук лічильника в документі
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // лічильник знайдено — його відображаємо
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//if
  • У разі, якщо сервер надсилає документ одним пакетом, його отримання є простим:
<?php
...

  //--------------------------
  function getDoc($connexion){
       // читання документа за адресою $connexion
    $doc="";
    while($ligne=fgets($connexion,10000))
        $doc.=$ligne;
     // кінець
    return $doc;
  }//getDoc
  • У разі, якщо сервер надсилає документ частинами, його читання стає складнішим:
<?php
...
  function getChunkedDoc($connexion){
       // читання документа на $connexion
     // цей документ надсилається частинами у вигляді
     // кількість символів у фрагменті в шістнадцятковій системі числення
     // продовження фрагмента

     // у першому рядку зчитується розмір фрагмента
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // зчитується наступний документ
    $doc="";
    while($taille!=0){
         // читання фрагмента з $taille символів
      $doc.=fread($connexion,$taille);
       // порожній рядок
      fgets($connexion,10000);
       // наступний фрагмент
       // зчитується розмір фрагмента
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//while
     // завершено
    return $doc;
  }// getChunkedDoc   

Нагадаємо, що фрагмент документа надсилається у вигляді

[taille]
[partie de document]
[ligne vide]

Отже, спочатку зчитується розмір документа. Коли розмір відомий, функція fread отримує завдання зчитати $taille символів із потоку $connexion, а потім — порожній рядок, що йде за ним. Це повторюється доти, доки сервер не надішле інформацію про те, що він надсилатиме документ розміром 0.

3.10.5. Приклад 4

У попередньому прикладі веб-клієнт повертає токен у вигляді файлу cookie. Ми бачили, що він також може повернути його безпосередньо в запиті URL у форматі URL;PHPSESSID=xxx. Перевіримо це. Програма clientCompteur.php перетворюється на clientCompteur2.php і модифікується наступним чином:

<?php
...
....
       // клієнт надсилає заголовки HTTP
      // get URL HTTP/1.1
      if($PHPSESSID)
          envoie($connexion, "GET $chemin?PHPSESSID=$PHPSESSID HTTP/1.1\n");
        else envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: хост:порт
      envoie($connexion, "Host: $hote:$port\n");
       // З'єднання: закрито
      envoie($connexion, "Connection: close\n");
       // порожній рядок
      envoie($connexion,"\n");      
....

Отже, клієнт запитує URL лічильника через GET URL;PHPSESSID=xx HTTP/1.1 і більше не надсилає cookie. Це єдина зміна. Ось результати першого запиту:


dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 2

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=573212ba82303d7903caf8944ee7a86f; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php?PHPSESSID=573212ba82303d7903caf8944ee7a86f HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

Під час першого запиту клієнт запитує URL без сесійного токена. Сервер відповідає, надсилаючи йому токен. Після цього клієнт повторно запитує той самий URL, додавши до нього отриманий токен. Бачимо, що лічильник збільшився, що свідчить про те, що сервер правильно розпізнав, що це та сама сесія.

3.10.6. Приклад 5

Цей приклад демонструє додаток, що складається з трьох сторінок, які ми назвемо page1, page2 та page3. Користувач повинен отримати їх у такому порядку:

  • page1 — це форма, що вимагає введення інформації: імені
  • page2 — це форма, отримана у відповідь на відправлення форми з page1. Вона вимагає введення другої інформації: віку
  • page3 — це документ HTML, який відображає ім’я, отримане на page1, та вік, отриманий на page2.

Відбувається три обміни даними між клієнтом і сервером:

  • під час першого обміну клієнт запитує форму «page1», яку надсилає сервер
  • під час другого обміну клієнт надсилає форму «page1» (ім’я) на сервер. У відповідь він отримує форму «page2» або знову форму «page1», якщо вона містила помилки.
  • під час третього обміну клієнт надсилає форму «page2» (вік) на сервер. У відповідь він отримує форму «page3» або знову форму «page2», якщо вона містила помилки. Документ «page3» відображає ім’я та вік. Ім’я було отримано сервером під час другого обміну і з того часу «забуто». Для збереження імені під час обміну 2 використовується сесія, щоб воно було доступним під час обміну 3.

Сторінка «page1», отримана під час першого обміну, має такий вигляд:

Image

Заповнюємо поле імені:

Image

Натискаємо кнопку [Suite] і отримуємо таку сторінку page2:

Image

Заповнюємо поле «Вік»:

Image

Натискаємо кнопку [Suite] і отримуємо таку сторінку page3:

Image

Коли сторінка page1 надсилається на сервер, він може повернути її з кодом помилки, якщо поле імені порожнє:

Image

Коли сторінка page2 надсилається на сервер, він може повернути її з кодом помилки, якщо вік є недійсним:

Image

Додаток складається з шести програм:

etape1.php
викликає page1.php
page1.php
відображає сторінку1. Форма сторінки1 обробляється програмою etape2.php.
etape2.php
обробляє значення форми сторінки1. Якщо є помилки, сторінка1 відображається знову за допомогою page1.php, інакше за допомогою page2.php відображається сторінка2.
page2.php
відображає сторінку2. Форму сторінки2 обробляє etape3.php.
etape3.php
обробляє значення форми на сторінці 2. Якщо є помилки, сторінка 2 відображається знову за допомогою page2.php, інакше сторінка 3 відображається за допомогою page3.php.
page3.php
відображає сторінку3.

Етап 1 додатка обробляється наступною програмою etape1.php:

<?php
   // etape1.php

   // конфігурація
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // запуск сеансу
  session_start();
  $_SESSION["session"]="";    //скидання змінної сеансу

   // підготовка сторінки 1
    $requête->nom="";
  $requête->erreurs=array();
   // відображення сторінки 1
    include "page1.php";
   // завершення  
  exit(0);
?>    

Звернімо увагу на наступні моменти:

  • додаток потребує відстеження сеансу. Тому кожен етап сеансу запускає новий сеанс.
  • Інформація про сесію, яку потрібно зберегти, буде зберігатися в об’єкті $session.
  • Інформація, необхідна для відображення різних (трьох) сторінок додатка, буде розміщена в об’єкті $requête.

Програма page1.php відображає інформацію, що міститься в об’єкті $requête:

<? // page1.php ?>

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/3</h3>
    <form name="frmNom" method="POST" action="etape2.php">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<? echo $requête->nom ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // помилки?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){ ?>
            <li><? echo $requête->erreurs[$i] ?>
        <? }//для ?>
        </ul>
     <? }//if ?>
  </body>
</html>
  • сторінка отримує об’єкт $requête, що містить два поля: nom та erreurs. Вона відображає значення цих двох полів.
  • Крім того, на ній відображається форма. Значення з цієї форми (ім’я) надсилаються методом POST до програми etape2.php:
    <form name="frmNom" method="POST" action="etape2.php">

Додаток etape2.php відповідає за обробку значень форми зі сторінки1 та повторне відображення сторінки1 у разі помилок (неправильне ім’я), а в іншому випадку — за відображення сторінки2 для отримання віку.

<?php
   // etape2.php

   // налаштування
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // запуск сеансу
  session_start();

   // зазвичай має бути параметр «ім’я»
   // зареєстрований у запиті
  $requête->nom=$_POST["nom"];

   // якщо параметрів немає, надсилаємо сторінку1 без помилок
  if (! isset($requête->nom)){
      $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//if

  // якщо параметр «nom» є — перевіряємо його правильність
  $page=calculerPage($requête);

   // чи були помилки?
  if(count($page->erreurs)!=0){
     // сторінка 1 з помилками
    $requête->erreurs=$page->erreurs;
    include "page1.php";
    exit(0);
  }//якщо

  // помилок немає — зберігаємо ім’я в сесії
  unset($session);
  $session->nom=$requête->nom;
  $_SESSION["session"]=$session;

   // відображення сторінки 2
  $requête->age="";
  $requête->erreurs=array();
  include "page2.php";

   // кінець
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // перевіряє правильність запиту $requête
     // повертає масив помилок у $page->помилки

     // спочатку помилок немає
    $page->erreurs=array();
     // ім'я не може бути порожнім
    if (preg_match("/^\s*$/",$requête->nom)){
      $page->erreurs[]="Vous n'avez pas indiqué de nom";
    }
     // повернутися на сторінку
    return $page;
  }//calculerPage
?>
  • etape2 спочатку перевіряє, чи має він очікуваний параметр nom. Якщо це не так, він змушує перезавантажити порожню сторінку1. Такий випадок можливий, якщо etape2 викликається безпосередньо клієнтом, який не передає йому параметрів.
  • Якщо параметр nom присутній, перевіряється його правильність. Це здійснюється за допомогою процедури calculerPage, завданням якої є створення об’єкта $page із полем erreurs, що є масивом помилок. Можлива лише одна помилка, але ми хотіли продемонструвати, що можна обробляти список помилок.
  • Якщо є помилки, сторінка page1 відображається знову разом із списком помилок.
  • Якщо помилок немає, ім’я зберігається в об’єкті $session, який зберігає дані, пов’язані з поточною сесією. Потім відображається сторінка page2.

Програма page2.php відображає сторінку 2:

<? // page2.php ?>

<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/3</h3>
    <form name="frmAge" method="POST" action="etape3.php">
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<? echo $requête->age ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // помилки?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i];
           }//для 
        ?>
        </ul>
      </font>
     <? } ?>
  </body>
</html>

Принцип роботи цієї сторінки дуже схожий на принцип роботи page2.php. Вона відображає вміст об’єкта $requête, що містить поля nom, age та erreurs. Вона відображає форму, значення якої будуть оброблятися etape3.php.

    <form name="frmAge" method="POST" action="etape3.php">

Отже, програма etape3.php обробляє значення з форми на сторінці 2, які тут зведені до віку:

<?php
   // etape3.php

   // конфігурація
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // запуск сеансу
  session_start();

   // отримуємо параметри «ім'я» та «вік»
  $requête->age=$_POST["age"];
  $session=$_SESSION["session"];
  $requête->nom=$session->nom;

   // зазвичай повинні бути ім'я та вік
  if (! isset($requête->age) || ! isset($requête->nom)){
     // якщо виклик некоректний, надсилаємо сторінку 1
    $_SESSION["session"]="";    // для впевненості
    $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//if

  // параметр «age» є — перевіряємо його правильність
  $page=calculerPage($requête);

   // чи були помилки?
  if(count($page->erreurs)!=0){
     // сторінка 2 з помилками
    $requête->erreurs=$page->erreurs;
    include "page2.php";
    exit(0);
  }//якщо

  // помилки немає — збереження віку в сесії
  $session->age=$requête->age;
  $_SESSION["session"]=$session;

   // відображення сторінки 3
  include "page3.php";
   // кінець
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // перевіряє правильність запиту $requête
     // повертає масив помилок у $page->помилки

     // спочатку помилок немає
    $page->erreurs=array();
     // вік повинен мати правильний формат
    if (! preg_match("/^\s*\d{1,3}\s*$/",$requête->age)){
      $page->erreurs[]="âge incorrect";
    }
     // повернутися на сторінку
    return $page;
  }//calculerPage
?>
  • програма спочатку отримує ім'я з сесії (з сторінки1) та вік з форми на сторінці2. Якщо будь-яка з цих даних відсутня, відображається сторінка1.
  • Потім перевіряється правильність віку. Якщо вік вказано неправильно, знову відображається сторінка «page2» зі списком помилок. Якщо вік вказано правильно, відображається сторінка «page3». На ній просто відображаються два значення (ім’я, вік), отримані з двох форм (сторінка «page1», сторінка «page2»).
<? // page3.php ?>

<html>
  <head>
    <title>page 3</title>
  </head>
  <body>
    <h3>Page 3/3</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><? echo $requête->age ?></font></td>
        </tr>
      </table>
  </body>
</html>