Skip to content

3. Універсальний контролер

3.1. Вступ

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

<?php
     // загальний контролер

  // читання конфігурації
  include 'config.php';

  // підключення бібліотек
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

  // запуск або відновлення сесії
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

  // отримання дії, яку потрібно виконати
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
  $sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";

     // чи є послідовність дій нормальною?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
    // послідовність ненормальна
    $sAction='enchainementinvalide';
  }//if

     // обробка дії
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;

  // відправлення відповіді (перегляду) клієнту
  $sEtat=$dSession['etat']['principal'];
  $scriptVue=$dConfig['etats'][$sEtat]['vue'];
  include $scriptVue;

  // кінець скрипта — сюди не слід потрапляти, якщо тільки це не помилка
  trace ("Erreur de configuration.");
  trace("Action=[$sAction]");
  trace("scriptAction=[$scriptAction]");
  trace("Etat=[$sEtat]");
  trace("scriptVue=[$scriptVue]");
  trace ("Vérifiez que les script existent et que le script [$scriptVue] se termine par l'appel à finSession.");
  exit(0);

  // ---------------------------------------------------------------
  function finSession(&$dConfig,&$dReponse,&$dSession){
    // $dConfig: словник конфігурації
      // $dSession: словник, що містить інформацію про сесію
         // $dReponse: словник аргументів сторінки відповіді

    // запис сеансу
    if(isset($dSession)){
      // параметри запиту заносяться в сесію
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
        // сесія відсутня
      session_destroy();
    }

         // відображення відповіді
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

    // кінець скрипта
    exit(0);
  }//закінчення сесії      

  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
      // перевіряє, чи дозволена поточна дія з огляду на попередній стан
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

    // перевірка дії
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

  //--------------------------------------------------------------------
  function dump($dInfos){
      // виводить словник інформації
    while(list($clé,$valeur)=each($dInfos)){
        echo "[$clé,$valeur]<br>\n";
    }//while
  }//відстеження

  //--------------------------------------------------------------------
  function trace($msg){
      echo $msg."<br>\n";
  }//відстеження

?>

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

Додаток налаштовується у скрипті, який обов’язково має назву config.php. Параметри додатка зберігаються у словнику під назвою $dConfig, який використовується як контролером, так і скриптами дій, моделями та елементарними поданнями.

3.3. Бібліотеки, які слід включити до контролера

Бібліотеки, які потрібно включити в код контролера, розміщуються в таблиці $dConfig['includes']. Контролер включає їх за допомогою такої послідовності коду:

<?php
...
  // читання конфігурації
  include "config.php";

  // включення бібліотек
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

3.4. Управління сесіями

Універсальний контролер автоматично керує сесією. Він зберігає та відновлює вміст сесії за допомогою словника $dSession. Цей словник може містити об’єкти, які необхідно серіалізувати, щоб згодом їх можна було правильно відновити. Ключ, пов’язаний із цим словником, — «session». Отже, відновлення сесії здійснюється за допомогою такого коду:

<?php

  // запуск або відновлення сеансу
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

Якщо дія потребує збереження інформації в сесії, вона додає ключі та значення до словника $dSession. Оскільки всі дії використовують одну й ту саму сесію, існує ризик конфлікту ключів сесії, якщо додаток розробляється незалежно кількома особами. Це є певною складністю. Необхідно створити репозиторій, що містить перелік ключів сесії, який буде спільним для всіх. Ми побачимо, що кожна дія завершується викликом наступної функції finSession:

<?php
... 
 // ---------------------------------------------------------------
  function finSession(&$dConfig,&$dReponse,&$dSession){
    // $dConfig: словник конфігурації
      // $dSession: словник, що містить інформацію про сесію
         // $dReponse: словник аргументів сторінки відповіді

    // запис сеансу
    if(isset($dSession)){
      // параметри запиту додаються до сесії
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
        // сесія відсутня
      session_destroy();
    }

         // відображення відповіді
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

    // кінець скрипта
    exit(0);
  }//закінчення сесії      

Процес може вирішити не продовжувати сеанс. Для цього достатньо не передавати значення параметру $dSession функції finSession, у цьому випадку сеанс видаляється (session_destroy). Якщо словник $dSession існує, він зберігається в сесії, яка потім записується (session_write_close). Отже, поточна операція може зберігати елементи в сесії, додаючи їх до словника $dSession. Слід зазначити, що контролер автоматично зберігає параметри поточного запиту в сесії. Це дозволить відновити їх у разі потреби для обробки наступного запиту.

3.5. Відправлення відповіді клієнту

Кінцевою метою функції finSession є надсилання відповіді користувачеві. Ми вже зазначали, що відповідь може мати різні шаблони сторінок. Вони задаються у файлі $dConfig['vuesReponse']. У додатку з двома шаблонами це може виглядати так:

<?php

  $dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
  $dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');

Поточна операція вказує у $dReponse['vuereponse'] бажану модель. Контролер відображає її за допомогою команди:

<?php

         // відображається відповідь
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

Після відправлення цієї відповіді клієнту контролер завершує роботу (exit).

3.6. Виконання дій

Контролер очікує запитів із параметром action=XX. Якщо цей параметр відсутній у запиті, а сам запит має вигляд GET, дія приймає значення «init». Це стосується найпершого запиту, що надсилається до контролера, який має вигляд http://machine:port/chemin/main.php.

<?php
..
  // отримано дію, яку потрібно виконати
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';

За замовчуванням з кожною дією пов’язаний скрипт, який відповідає за її обробку. Наприклад:

<?php
... 
// налаштування дій додатка
  $dConfig['actions']['get:init']=array('url'=>'a-init.php');  
  $dConfig['actions']['post:calculerimpot']=array('url'=>'a-calculimpot.php');
  $dConfig['actions']['get:retourformulaire']=array('url'=>'a-retourformulaire.php');
  $dConfig['actions']['post:effacerformulaire']=array('url'=>'a-init.php');
  $dConfig['actions']['enchainementinvalide']=array('url'=>'a-enchainementinvalide.php');
  $dConfig['actions']['actionInvalide']=array('url'=>'a-actioninvalide.php');          

Заздалегідь визначено дві дії:

enchainementInvalide
у разі, якщо поточна дія не може слідувати за попередньою дією
actionInvalide
випадок, коли запитувана дія відсутня у словнику дій

Дії, властиві додатку, позначаються у формі «метод:дія», де «метод» — це метод GET або POST запиту, а «дія» — запитувана дія, у даному випадку: init, calculerimpot, retourformulaire, effacerformulaire. Слід зауважити, що дія отримується незалежно від методу передачі параметрів (GET або POST) за допомогою послідовності:

<?php

  // отримано дію, яку потрібно виконати
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init'; 

Адже навіть якщо форма відправлена, завжди можна написати:

<form method='post' action='main.php?action=calculerimpot'>
..
</form>

Елементи форми будуть відправлені методом POST (method='post'). Проте запитувана URL-адреса буде такою: main.php?action=calculerimpot. Параметри цього URL будуть отримані зі словника $_GET, тоді як інші елементи форми будуть отримані зі словника $_POST.

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

<?php
...
    // обробка дії
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;

Якщо запитувана дія відсутня у словнику дій, буде виконано скрипт, що відповідає недійсній дії. Після завантаження скрипта дії в контролер він виконується. Слід зауважити, що він має доступ до змінних контролера ($dConfig, $dSession), а також до суперглобальних словників PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). У скрипті міститься логіка додатка та виклики бізнес-класів. У будь-якому випадку дія повинна

  • заповнювати словник $dSession, якщо елементи потрібно зберегти в поточній сесії
  • у $dReponse['vuereponse'] вказати ім’я шаблону відповіді, що має відображатися
  • завершуватися викликом finSession($dConfig, $dReponse, $dSession). Якщо сесію потрібно знищити, дія просто завершиться викликом finSession($dConfig, $dReponse).

З метою забезпечення узгодженості дія може додати до словника $dReponse всю інформацію, необхідну для переглядів. Але це не є обов’язковим. Необхідним є лише значення $dReponse['vuereponse']. Слід зауважити, що будь-який скрипт дії закінчується викликом функції finSession, яка, у свою чергу, закінчується операцією exit. Отже, з скрипту дії повернення неможливе.

3.7. Послідовність дій

Веб-додаток можна розглядати як автомат з кінцевими станами. Різні стани додатка пов’язані з вікнами, що відображаються користувачеві. Користувач за допомогою посилання або кнопки переходить до іншого вікна. Веб-додаток змінив свій стан. Ми бачили, що дія ініціюється запитом типу http://machine:port/chemin/main.php?action=XX. Цей URL має походити з посилання, що міститься у вікні, яке відображається користувачеві. Адже ми хочемо уникнути ситуації, коли користувач безпосередньо вводить URL http://machine:port/chemin/main.php?action=XX, тим самим обходячи шлях, який для нього передбачила програма. Це також стосується випадків, коли клієнтом є програма.

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

  • URL, що містяться у вікні або у вигляді посилань, або у вигляді цілей дій типу «submit»
  • URL — це те, що користувач може ввести безпосередньо у своєму браузері, коли йому відображається цей вигляд.

Список станів додатка не обов’язково збігається зі списком переглядів. Розглянемо, наприклад, такий елементарний перегляд erreurs.php:

Les erreurs suivantes se sont produites :
<ul>
    <?php
        for($i=0;$i<count($dReponse["erreurs"]);$i++){
            echo "<li class='erreur'>".$dReponse["erreurs"][$i]."</li>\n";
        }//for
    ?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>

Цей елементарний вигляд буде інтегрований у композицію елементарних виглядів, яка сформує відповідь. На цьому вигляді є посилання, яке може бути розміщене динамічно. Отже, представлення erreurs.php може відображатися з n різними посиланнями залежно від обставин. Це призведе до появи n різних станів додатка. У стані № i представлення erreurs.php відображатиметься з посиланням lieni. У цьому стані допустиме лише використання посилання lieni.

Перелік станів додатка та можливих дій у кожному стані буде зафіксовано у словнику $dConfig['etats']:

<?php
...  
// налаштування станів додатка
  $dConfig['etats']['e-formulaire']=array(
       'actionsautorisees'=>array('post:calculerimpot','get:init','post:effacerformulaire'),
    'vue'=>'e-formulaire2.php');
  $dConfig['etats']['e-erreurs']=array(
      'actionsautorisees'=>array('get:retourformulaire','get:init'),
      'vue'=>'e-erreurs2.php');
  $dConfig['etats']['sansetat']=array('actionsautorisees'=>array('get:init'));

Вищезазначена програма має два стани з назвами: «e-formulaire» та «e-erreurs». Додаємо стан під назвою «sansetat», який відповідає початковому запуску програми, коли вона ще не мала стану. У стані E перелік дозволених дій міститься в таблиці $dConfig['etats'][E]['actionsautorisees']. У ньому вказано дозволений для дії метод (get/post) та її назву. У наведеному вище прикладі є чотири можливі дії: get:init, post:alculerimpot, get:retourformulaire та post:effacerformulaire.

Маючи словник $dConfig['etats'], контролер може визначити, чи дозволена поточна дія $sAction у поточному стані додатка. Цей стан формується для кожної дії та зберігається в сесії у $dSession['etat']. Код контролера для перевірки, чи дозволена поточна дія, виглядає так:

<?php
.....
     // чи є послідовність дій нормальною?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
    // послідовність ненормальна
    $sAction='enchainementinvalide';
  }//if

     // обробка дії
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;
..........
  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
      // перевіряє, чи дозволена поточна дія з огляду на попередній стан
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

    // перевірка дії
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

Логіка така: дія $sAction дозволена, якщо вона міститься у списку $dConfig['etats'][$etat]['actionsautorisees'] або якщо цей список відсутній , що дозволяє виконати будь-яку дію. $etat — це стан додатка наприкінці попереднього циклу «запит клієнта/відповідь сервера». Цей стан було збережено в сесії, і його можна знайти саме там. Якщо виявляється, що запитувана дія є недопустимою, виконується скрипт $dConfig['actions']['enchainementInvalide']['url']. Цей скрипт відповідатиме за надсилання відповідної відповіді клієнту.

На етапі розробки словник $dConfig['etats'] можна не заповнювати. У цьому випадку будь-який стан дозволяє виконувати будь-яку дію. Словник можна буде доопрацювати після повного налагодження програми. Він захищатиме програму від несанкціонованих дій.

3.8. Налагодження

Контролер пропонує дві функції налагодження:

  • функція trace дозволяє вивести повідомлення у потік HTML
  • функція dump дозволяє вивести вміст словника в цей самий потік

Будь-який скрипт дії зможе використовувати ці дві функції. Оскільки код скрипта дії включено (include) у код контролера, функції trace та dump будуть доступні для скриптів.

3.9. Висновок

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

  • управління сесією (відновлення, збереження)
  • перевірку правильності запитуваних дій
  • виконання скрипту, пов’язаного з дією
  • надсилання клієнту відповіді, що відповідає результату виконання дії