7. Приклад з практики: управління базою товарів у мережі Інтернет
Коди цього прикладу з практики доступні за посиланням |ICI|.
Цілі:
- написати клас для управління базою даних статей
- написати веб-додаток на основі цього класу
- ознайомитися зі стильовими таблицями
- запропонувати початкову методологію розробки простих веб-додатків
- ознайомитися з використанням JavaScript у клієнтському браузері
Джерела: Основна ідея цього прикладу з практики взята з книги «Записки програміста — PHP/MySQL» Жана-Філіпа Лебоефа, виданої видавництвом Eyrolles.
7.1. Вступ
Продавець бажає керувати товарами, які він продає у своєму магазині. У нього вже є програма ACCESS, яка виконує цю роботу, але його приваблює можливість роботи в Інтернеті. Він має обліковий запис у провайдера інтернет-доступу, який дозволяє своїм клієнтам встановлювати скрипти PHP у своїх особистих папках. Це дає їм змогу створювати динамічні веб-сайти. Крім того, ці самі клієнти мають обліковий запис MySQL, що дозволяє їм створювати таблиці, які можуть надавати дані для їхніх скриптів PHP. Отже, продавець має обліковий запис MySQL з логіном admarticles та паролем mdparticles. Він володіє базою даних dbarticles, до якої має всі права. Таким чином, наш продавець має достатньо ресурсів, щоб розмістити свою систему управління товарами в Інтернеті. За вашої допомоги, як фахівців у галузі веб-розробки, він береться за цю справу.
7.2. База даних
Наш підприємець створює такий макет головного веб-інтерфейсу, який він хотів би мати:

Буде два типи користувачів:
- адміністратори, які зможуть виконувати будь-які дії з таблицею товарів (додавати, редагувати, видаляти, переглядати тощо). Вони зможуть користуватися всіма пунктами меню, наведеного вище. Зокрема, вони зможуть надсилати будь-який запит SQL за допомогою опції [Requête SQL].
- звичайні користувачі (не адміністратори), які матимуть обмежені права: право додавати, редагувати, видаляти, переглядати. Вони можуть мати лише деякі з цих прав, наприклад, лише право на перегляд.
Оскільки існують різні типи користувачів бази даних, які не мають однакових прав, необхідна аутентифікація. Саме тому головна сторінка починається з неї. Щоб визначити, хто є хто і хто має право на які дії, будуть використовуватися дві таблиці: USERS та DROITS. Таблиця USERS матиме таку структуру:
![]() |
|
Вміст таблиці може бути таким:

Таблиця DROITS визначає права користувачів, які не є адміністраторами та знаходяться в таблиці USERS. Її структура така:
![]() |
|
Зміст таблиці може бути таким:

Примітки:
- Користувач U, який міститься в таблиці USERS і відсутній у таблиці DROITS, не має жодних прав.
- У нашому прикладі користувачі матимуть доступ лише до однієї таблиці — таблиці ARTICLES. Але наш передбачливий комерсант все ж додав поле «table» до структури таблиці DROITS, щоб мати можливість згодом додавати нові таблиці до свого додатка.
- Навіщо керувати правами у наших власних таблицях, якщо ми виходимо з припущення, що будемо використовувати базу даних MySQL, яка сама (і краще за нас) здатна керувати цими правами у своїх власних таблицях? Просто тому, що наш продавець не має прав адміністратора на базу даних MySQL, які дозволили б йому створювати користувачів і надавати їм права. Не забуваймо, що база даних MySQL розміщена у провайдера, а продавець є лише її звичайним користувачем без жодних прав адміністратора (на щастя). Однак він має всі права на базу даних під назвою dbarticles, до якої наразі отримує доступ за допомогою логіна admarticles та пароля mdparticles. Саме в цій базі розміщені всі таблиці додатка.
Таблиця ARTICLES містить інформацію про товари, що продаються продавцем. Її структура така:
![]() |
|
Його вміст, який спочатку використовується для тестування, може бути таким:

7.3. Обмеження проекту
Торговець переносить тут локальний додаток ACCESS у веб-додаток. Він не знає, чим цей додаток стане в майбутньому і як він розвиватиметься. Однак він хотів би, щоб новий додаток був простим у використанні та масштабованим. Саме з цієї причини його ІТ-консультант під час проектування таблиць передбачив, що можуть існувати:
- різні користувачі з різними правами: це дозволить підприємцю делегувати певні завдання іншим особам, не надаючи їм при цьому прав адміністратора
- у майбутньому — інші таблиці, крім таблиці ARTICLES
Цей самий консультант висуває й інші пропозиції:
- він знає, що при розробці програмного забезпечення необхідно чітко розділяти рівні представлення та обробки. Архітектура веб-додатку часто виглядає так:
![]() |
Інтерфейсом користувача тут є веб-браузер, але це може бути також автономний додаток, який через мережу надсилатиме запити HTTP до веб-сервісу та оформлюватиме результати, які той йому надсилає. Логіка додатка складається зі скриптів, що обробляють запити користувача, у даному випадку це скрипти PHP. Джерелом даних часто є база даних, але це може бути також каталог LDAP або віддалений веб-сервіс. Розробнику вигідно зберігати значну незалежність між цими трьома компонентами, щоб у разі зміни одного з них інші два не потребували змін або потребували їх у мінімальному обсязі. Тому ІТ-консультант підприємця пропонує наступне:
- Бізнес-логіку додатка розмістимо в класі PHP. Таким чином, наведений вище блок [Logique applicative] складатиметься з таких елементів:
![]() |
У блоці [Logique Applicative] можна виділити
- блок [IE=Interface d'Entrée], який є вхідною точкою додатка. Він однаковий незалежно від типу клієнта.
- блок [Classes métier], який об’єднує класи, необхідні для логіки додатка. Вони не залежать від клієнта.
- блок генераторів сторінок відповідей [IS1 IS2 ... IS=Interface de Sortie]. Кожен генератор відповідає за форматування результатів, наданих логікою додатка, для певного типу клієнта: код HTML для браузера або мобільного телефону WAP, код XML для автономного додатка тощо.
Ця модель забезпечує високу незалежність від клієнтів. Незалежно від того, чи змінюється клієнт, чи потрібно вдосконалити спосіб подання результатів, потрібно буде створити або адаптувати саме генератори виводу [IS].
- У веб-додатку незалежність між рівнем представлення та рівнем обробки можна покращити за допомогою таблиць стилів. Вони визначають вигляд веб-сторінки в браузері. Щоб змінити цей вигляд, достатньо змінити відповідну таблицю стилів. Не потрібно втручатися в логіку обробки. Тому тут ми будемо використовувати таблицю стилів.
- На наведеній вище діаграмі саме бізнес-клас буде взаємодіяти з джерелом даних. За припущенням, цим джерелом тут є база даних MySQL. Щоб забезпечити можливість переходу на іншу базу даних, ми використаємо бібліотеку PEAR, яка надає класи доступу до баз даних, незалежні від їхнього фактичного типу. Отже, якщо наш підприємець розбагатіє настільки, що зможе встановити у своїй компанії веб-сервер IIS від Microsoft, він зможе замінити базу MySQL на SQL Server, практично не змінюючи (або змінюючи мінімально) бізнес-клас.
7.4. Клас «товари»
Клас «товари» можна визначити таким чином:
<?php
// клас товарів, що працює на базі товарів, яка складається з таких таблиць
// товари: (код, назва, ціна, stockActuel, stockMinimum)
// користувачі: (логін, пароль, адміністратор)
// права: (логін, таблиця, додавати, редагувати, видаляти, переглядати)
// це користувач класу, який повинен надати логін/пароль, що дозволяють виконувати будь-які операції з базою даних
// отже, він уже має всі права на базу даних. Це означає, що тут не потрібно вживати
// вживати тут особливих заходів безпеки
// бібліотеки
require_once 'DB.php';
class articles{
// атрибути
var $sDSN; // рядок підключення
var $sDatabase; // назва бази даних
var $oDB; // підключення до бази даних
var $aErreurs; // список помилок
var $oRésultats; // результат запиту SELECT
var $connecté; // логічне значення, яке вказує, чи встановлено з'єднання з базою даних
var $sQuery; // останній виконаний запит
var $sUser; // ідентифікатор користувача, що здійснив підключення
var $bAdmin; // має значення «true», якщо користувач є адміністратором
var $dDroits; // словник його прав table ->> array(переглядати, додавати, видаляти, редагувати)
// конструктор
function articles($dDSN,$sUser,$sMdp){
// $dDSN: словник, що визначає зв’язок, який слід встановити
// $dDSN['sgbd']: тип об’єкта SGBD, до якого потрібно підключитися
// $dDSN['host']: ім’я хост-машини, на якій він розміщений
// $dDSN['database']: ім’я бази даних, до якої потрібно підключитися
// $dDSN['admin']: логін власника бази даних, до якої потрібно підключитися
// $dDSN['mdpadmin'] : його пароль
// $sUser: логін користувача, який хоче користуватися базою статей
// $sMdp: його пароль
// створює в $oDB з'єднання з базою даних, визначеною $dDSN, під ім'ям користувача $dDSN['admin']
// якщо з'єднання встановлено успішно і користувач $sUser пройшов автентифікацію
// завантажує права в $bAdmin та $dDroits — права користувача $sUser
// записує в $sDSN рядок підключення до бази даних
// встановлює в $sDataBase ім’я бази даних, до якої здійснюється підключення
// встановлює для $connecté значення «true»
// якщо підключення не вдалося або якщо користувач $sUser не ідентифікований належним чином
// додає відповідні повідомлення про помилки до списку $aErreurs
// за необхідності закриває з'єднання
// встановлює для $connecté значення «false»
...
}//конструктор
// ------------------------------------------------------------------
function connect(){
// (повторне) підключення до бази
...
}//підключення
// ------------------------------------------------------------------
function disconnect(){
// закриття з'єднання з базою $sDSN
...
}//відключення
// -------------------------------------------------------------------
function execute($sQuery,$bAdmin){
// $sQuery: запит на виконання
// $bAdmin: «true», якщо запит на виконання подано від імені адміністратора
...
}//виконати
// --------------------------------------------------------------------------
function addArticle($dArticle){
// додає товар $dArticle (код, назва, ціна, stockActuel, stockMinimum) до таблиці товарів
...
}//додати
// ----------------------------------------------------------------------
function modifyArticle($dArticle){
// змінює товар $dArticle (код, назва, ціна, stockActuel, stockMinimum) у таблиці товарів
...
}//update
// ----------------------------------------------------------------------
function deleteArticle($sCode){
// видаляє товар з таблиці товарів
//, код якого $sCode
...
}//видалити
// ----------------------------------------------------------------------
function vérifierArticle(&$dArticle){
// перевіряє правильність товару $dArticle (код, назва, ціна, stockActuel, stockMinimum)
...
}//перевірити
// --------------------------------------------------------------------------
function selectArticles($dQuery){
// виконує запит SELECT до таблиці товарів
// вона має три компоненти
// список стовпців у $dQuery['colonnes']
// фільтрація в $dQuery['where']
// порядок відображення в $dQuery['orderby']
...
}//selectArticles
// --------------------------------
function existeArticle($sCode){
// повертає TRUE, якщо товар з кодом $sCode існує в таблиці товарів
...
}//existeArticle
// --------------------------------------
function existeUser($sUser,$sMdp){
// перевірка наявності користувача $sUser з паролем $sMdp
// повертає (int $iErreur, string $sAdmin, hashtable $dDroits)
// $iErreur = -1 у разі будь-якої помилки під час роботи з базою даних — тоді заповнюється список $aErreurs
// $iErreur = 1, якщо користувача не знайдено (не існує або неправильний пароль)
// $iErreur = 2, якщо користувач існує, але не має жодних прав у таблиці прав
// $iErreur = 3, якщо користувач існує і є адміністратором
// $iErreur = 0, якщо користувач існує, але не є адміністратором
// $sAdmin = «y», якщо користувач існує і є адміністратором ($iErreur == 3), інакше дорівнює порожньому рядку
// $dDroits — це словник прав користувача, якщо він не є адміністратором ($iErreur==0)
// інакше це порожній масив
// ключами словника є таблиці, до яких користувач має права
// значення, пов’язане з цією таблицею, у свою чергу є словником, де ключами є права
// (переглянути, додати, змінити, видалити), а значеннями — рядки «y» (так) або «n» (ні) залежно від випадку
...
}//existeUser
// --------------------------------------
function getCodes(){
// повертає таблицю кодів
....
}//getCodes
}//класифікує
?>
Коментарі
- Клас «articles» використовує бібліотеку PEAR::DB для доступу до бази даних, звідки походить команда
Це включення передбачає, що скрипт DB.php має знаходитися в одному з каталогів, вказаних в опції include_path конфігураційного файлу PHP.
- Конструктор повинен знати, до якої бази даних відбувається підключення та під яким обліковим записом. Ця інформація надається йому у словнику $dDSN. Нагадаємо, що вихідним припущенням було те, що база даних називалася dbarticles і належала користувачеві admarticles з паролем mdparticles. Нагадаємо також, що ця програма підтримує декількох користувачів із різними правами. Тут існує неоднозначність, яку потрібно усунути. З’єднання дійсно відкрито під ідентифікатором admarticles, і самесаме під цим іменем будуть виконуватися всі операції з базою даних dbarticles, оскільки це єдине ім’я, яке знає SGBD MySQL, що має достатні права для управління базою даних dbarticles. Щоб «імітувати» наявність різних користувачів, ми змусимо користувача admarticles працювати з правами користувача, логін ($sUser) та пароль ($sMdp) якого передаються як параметри конструктору. Таким чином, перед виконанням операції з базою товарів ми перевіримо, чи користувач ($sUser, $sMdp) дійсно має права на її виконання. Якщо так, то операцію за нього виконає користувач admarticles.
- Логін та пароль адміністратора бази товарів повинні передаватися конструктору. Це розумна запобіжна міра. Якби ці два параметри були «жорстко» вписані в код класу, будь-який користувач цього класу міг би легко видати себе за адміністратора бази товарів. Адже клас PHP не захищений. Також атрибут $bAdmin класу, який вказує, чи є користувач ($sUser, $sMdp), для якого ми працюємо, адміністратором чи ні, цілком може бути встановлений безпосередньо ззовні, як у наступному прикладі:
$oArticles=new articles($dDSN,$sUser,$sMdp)
// тут $sUser було визнано користувачем, який не є адміністратором бази
$oArticle->bAdmin=TRUE;
// тепер $sUser став адміністратором
PHP не є JAVA або C# а клас PHP є лише структурою даних, дещо досконалішою за словник, але яка не забезпечує безпеки справжнього класу, де атрибут bAdmin був би оголошений приватним або захищеним, що унеможливило б його зміну ззовні. Оскільки користувач класу повинен знати логін та пароль адміністратора бази статей, лише останній може використовувати цей клас. Отже, попередня операція більше не представляє для нього жодного інтересу. Клас існує виключно для того, щоб полегшити йому розробку. Важливим наслідком цього є те, що немає необхідності вживати заходів безпеки. Ще раз наголошуємо: той, хто використовує клас articles, обов’язково є адміністратором бази статей.
- Клас обробляє помилки підключення до бази даних або будь-які інші помилки єдиним способом — заповнюючи атрибут $aErreurs одним або кількома повідомленнями про помилки. Тому після кожної операції користувач класу повинен перевіряти цей список.
- Методи addArticle, updateArticle, deleteArticle, selectArticles та execute безпосередньо випливають із макету веб-інтерфейсу, представленого раніше. Вони, фактично, відповідають опціям запропонованого меню. Методи addArticle та modifyArticle базуються на методі vérifierArticle для перевірки того, чи стаття, яку збираються додати або змінити, містить правильні дані. У тому ж дусі метод existeArticle дозволяє перевірити, чи не намагаються додати статтю, яка вже існує. Цього методу можна було б уникнути, якби використовувалася таблиця статей, у якій код є первинним ключем. У такому випадку саме метод SGBD повідомить про невдале додавання через наявність дубліката. Ймовірно, це буде зроблено за допомогою малозрозумілого повідомлення про помилку англійською мовою.
- Статтю, яку потрібно змінити або видалити, можна ідентифікувати за її унікальним кодом. Метод getCodes дозволяє отримати всі ці коди.
- Метод disconnect закриває з’єднання з базою даних, яке було відкрито під час створення об’єкта. Тут не бачимо сенсу у використанні методу connect, який відновлює з’єднання з базою даних. Це дозволить відкривати та закривати це з’єднання за бажанням за допомогою одного й того самого об’єкта. Сенс цього стає очевидним лише у поєднанні з веб-додатком. Він створює об’єкт articles, який зберігає у сесії. Хоча додаток зможе зберігати більшість атрибутів об’єкта під час послідовних обмінів даними між клієнтом і сервером, він не здатний зберегти атрибут, що позначає відкрите з’єднання. Тому його доведеться відкривати заново під час кожного нового обміну даними між клієнтом і сервером. Ми вимагатимемо постійного з’єднання, щоб відкрите з’єднання зберігалося в пулі з’єднань і залишалося відкритим постійно. Таким чином, коли скрипт вимагатиме нового з’єднання, воно буде отримано з пулу з’єднань. Отже, ми отримуємо той самий результат, що й у випадку, якби сесія могла зберегти відкрите з’єднання.
- Метод existeUser дозволяє розробнику перевірити, чи дійсно існує користувач $sUser, ідентифікований за паролем $sMdp. Якщо так, то метод дозволяє визначити, чи є він адміністратором (що вказано в таблиці USERS), і зберігає цю інформацію в атрибуті $bAdmin. Якщо він не є адміністратором, метод отримає його права з таблиці DROITS і збереже їх в атрибуті $dDroits, який є двоіндексним словником: $dDroits[$table][$droit] дорівнює 'y», якщо користувач $sUser має право $droit на таблицю $table, і дорівнює «n» в іншому випадку.
Напишіть клас articles. Доступ до бази даних буде здійснюватися за допомогою бібліотеки </mark>[<u><span style="color: #0563c1">PEAR::DB,](https://pear.php.net/package/DB/) яка дозволяє не враховувати конкретний тип бази даних.
7.5. Структура додатка WEB
Тепер, коли ми маємо «бізнес-клас» для управління базою товарів, ми можемо використовувати його в різних середовищах. Тут пропонується використовувати його у веб-додатку. Давайте ознайомимося з ним на прикладі різних сторінок:
7.5.1. Типова сторінка додатка
Повернемося до вже представленої головної сторінки:
1234

Усі сторінки додатка матимуть наведену вище структуру — таблицю з двома рядками та трьома стовпцями, що складається з чотирьох зон:
- зона 1 утворює перший рядок таблиці. Вона призначена для заголовка, який може супроводжуватися зображенням. Три стовпці цього рядка тут об’єднані.
- другий рядок має три зони, по одній на стовпець:
- зона 2 містить опції меню. Вона, у свою чергу, містить таблицю з одним стовпцем і кількома рядками. Опції меню розміщені в рядках таблиці.
- зона 3 порожня і служить лише для відокремлення зон 2 та 4. Можна було б зробити це по-іншому.
- Зона 4 — це та, що містить динамічну частину сторінки. Саме ця частина змінюється від однієї дії до іншої, тоді як інші залишаються незмінними.
Скрипт PHP, що генерує цей шаблон сторінки, матиме назву main.php і може виглядати так:
<html>
<head>
<title>Gestion d'articles</title>
<link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
</head>
<body background="<?php echo $dConfig['urlBackGround'] ?>">
<table>
<tr height="60">
<td colspan="3" align="left" valign="top" >
<h1><?php echo $main["title"] ?></h1>
</td>
</tr>
<tr>
<td>
<table>
<tr>
<td class="menutitle" >
<a href="<?php echo $main["liens"]["login"] ?>" ?>Authentification</a>
</td>
</tr>
<tr>
<td><br /></td>
</tr>
<tr>
<td class="menutitle" >
Utilisation
</td>
</tr>
<tr height="10"></tr>
<tr>
<td class="menublock" >
<img alt="-" src="../images/radio.gif" />
<a href="<?php echo $main["liens"]["addArticle"] ?>" ?>
Ajouter un article
</a>
</td>
</tr>
<tr>
<td class="menublock" >
<img alt="-" src="../images/radio.gif" />
<a href="<?php echo $main["liens"]["updateArticle"] ?>">
Modifier un article
</a>
</td>
</tr>
<td class="menublock" >
<img alt="-" src="../images/radio.gif" />
<a href="<?php echo $main["liens"]["deleteArticle"] ?>">
Supprimer un article
</a>
</td>
</tr>
<tr>
<td class="menublock" >
<img alt="-" src="../images/radio.gif" />
<a href="<?php echo $main["liens"]["selectArticle"] ?>">
Lister des articles
</a>
</td>
</tr>
<tr>
<td><br /></td>
</tr>
<tr>
<td class="menutitle" >
Administration
</td>
</tr>
<tr height="10"></tr>
<tr>
<td class="menublock" >
<img alt="-" src="../images/radio.gif" />
<a href="<?php echo $main["liens"]["sql"] ?>" >
Requête SQL
</a>
</td>
</tr>
</table>
</td>
<td>
<img alt="/" src="../images/pix.gif" width="10" height="1" />
</td>
<td>
<fieldset>
<legend><?php echo $main["légende"] ?></legend>
<?php
include $main["contenu"];
?>
</fieldset>
</td>
</tr>
</table>
</body>
</html>
Параметризовані зони сторінки виділено у наведеному вище списку. Типова сторінка налаштовується кількома способами:
- за допомогою словника $main із такими ключами:
- title: заголовок, який потрібно розмістити в зоні 1 сторінки
- liens: словники посилань, які потрібно згенерувати у стовпці меню. Ці посилання пов’язані з опціями меню зони 2
- contenu: URL-адреса сторінки, яку потрібно відобразити в зоні 4
- за допомогою словника $dConfig, що містить інформацію, взяту з конфігураційного файлу додатка під назвою config.php
- за допомогою класів, що входять до стильового аркуша, який використовується сторінкою:
На цій сторінці використовуються такі класи стилів:
- menutitle: для основного пункту меню
- menublock: для другорядного пункту меню
Зміна одного з параметрів змінює зовнішній вигляд сторінки. Так, зміна $main['title'] змінить заголовок зони 1.
7.5.2. Типова обробка запиту клієнта
Клієнт взаємодіє з додатком за допомогою посилань у зоні 2 типової сторінки. Ці посилання матимуть такий вигляд:
позначає поточну дію з-поміж таких:
| |||||||||||
дія може складатися з декількох етапів — позначає поточний етап | |||||||||||
токен сеансу на момент його початку — дозволяє серверу отримувати інформацію, збережену в сеансі під час попередніх обмінів |
Атрибут action у формах матиме такий самий вигляд. Наприклад, на головній сторінці у зоні 4 розміщено форму входу. Тег HTML цієї форми визначено так:
Обробка запиту клієнта здійснюється головним скриптом додатка під назвою apparticles.php. Його завдання — сформувати відповідь для клієнта. Він завжди діятиме однаково:
- завдяки назві дії та поточній фазі він передасть запит спеціалізованій функції. Ця функція обробить запит і згенерує відповідну сторінку відповіді. Для кожного запиту клієнта може бути кілька можливих сторінок відповіді: сторінка1, сторінка2, …, сторінкаn. Ці сторінки містять інформацію, яку має обчислити функція. Отже, це сторінки з параметрами. Вони будуть згенеровані скриптами page1.php, page2.php, ..., pagen.php.
- З метою забезпечення єдиного формату змінні частини сторінок, що мають відображатися в зоні 4 типової сторінки, також будуть розміщені у словнику $main.
Припустимо, що у відповідь на запит сервер повинен надіслати клієнту сторінку pagex.php. Він діятиме наступним чином:
- він розмістить у словнику $main значення, необхідні для сторінки pagex.php
- він помістить у $main['contenu'], що позначає URL сторінки, яку потрібно відобразити, у зону 4 типової сторінки, URL з pagex.php
- він запросить відображення типової сторінки за допомогою команди
Тоді шаблонна сторінка відобразиться з кодом скрипта pagex.php у зоні 4, який буде оброблено для формування вмісту зони 4. Нагадаємо, що ця зона є простою коміркою таблиці. Тому код HTML, згенерований pagex.php, не повинен починатися з тегів <HTML>, <HEAD>, <BODY>, ... Ці теги вже були виведені на початку типової сторінки. Ось, наприклад, як міг би виглядати скрипт login.php, що генерує зону 4 головної сторінки:
<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
<table>
<tr>
<td>login</td>
<td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
</tr>
<tr>
<td>mot de passe</td>
<td><input type="password" value="" name="txtMdp" class="text"></td>
<td><input type="submit" value="Connexion" class="submit"></td>
</tr>
</table>
</form>
Як бачимо, сторінка:
- зведена до форми
- налаштована як за допомогою словника $main, так і за допомогою таблиці стилів.
7.5.3. Файл конфігурації
Завжди доцільно налаштовувати додатки якомога ретельніше, щоб уникнути необхідності втручання в код лише тому, що, наприклад, вирішено змінити шлях до скрипта чи зображення. Отже, головний додаток apparticles.php завантажуватиме файл конфігурації config.php під час запуску:
У цей файл ми внесемо директиви конфігурації, призначені для PHP, та ініціалізації глобальних змінних:
<?php
// конфігурація PHP
ini_set("register_globals","off");
ini_set("display_errors","off");
ini_set("expose_php","off");
ini_set("session.use_cookies","0"); // кукі відсутні
// базова конфігурація товарів
$dConfig["DSN"]=array(
"sgbd"=>"mysql",
"admin"=>"admarticles",
"mdpadmin"=>"mdparticles",
"host"=>"localhost",
"database"=>"dbarticles"
);
// URL-адреси сторінок
$dConfig['urlBackGround']="../images/standard.jpg";
$dConfig["urlPageStyle"]="mystyle.css";
$dConfig["urlAppArticles"]="apparticles.php";
$dConfig["urlPageMain"]="main.php";
$dConfig["urlPageLogin"]="login.php";
$dConfig["urlPageErreurs"]="erreurs.php";
$dConfig["urlPageInfos"]="infos.php";
$dConfig["urlPageAddArticle"]="addarticle.php";
$dConfig["urlPageUpdateArticle1"]="updatearticle1.php";
$dConfig["urlPageUpdateArticle2"]="updatearticle2.php";
$dConfig["urlPageDeleteArticle1"]="deletearticle1.php";
$dConfig["urlPageDeleteArticle2"]="deletearticle2.php";
$dConfig["urlPageSelectArticle1"]="selectarticle1.php";
$dConfig["urlPageSelectArticle2"]="selectarticle2.php";
$dConfig["urlPageSQL1"]="sql1.php";
$dConfig["urlPageSQL2"]="sql2.php";
$dConfig["urlPageSQL3"]="sql3.php";
// посилання на головній сторінці
$main["liens"]["login"]="$sUrlAppArticles?action=authentifier&phase=0";
$main["liens"]["addArticle"]="$sUrlAppArticles?action=addArticle&phase=0";
$main["liens"]["updateArticle"]="$sUrlAppArticles?action=updateArticle&phase=0";
$main["liens"]["deleteArticle"]="$sUrlAppArticles?action=deleteArticle&phase=0";
$main["liens"]["selectArticle"]="$sUrlAppArticles?action=selectArticle&phase=0";
$main["liens"]["sql"]="$sUrlAppArticles?action=sql&phase=0";
// $main зберігається в налаштуваннях
$dConfig["main"]=$main;
?>
7.5.4. Таблиця стилів, пов'язана зі сторінкою-шаблоном
Ми бачили, що відповідь сервера мала єдиний формат — main.php. Можна було помітити, що цей скрипт генерує «сиру» сторінку без будь-яких візуальних ефектів. Це добре з кількох причин:
- розробник не мусить турбуватися про графічне оформлення сторінки, яку він створює. Адже він не обов’язково має навички для створення привабливих графічних сторінок. Тут він може повністю зосередитися на коді.
- полегшується обслуговування скриптів. Якби вони містили атрибути оформлення, ні структура коду, ні структура оформлення не були б чітко видимими. Графічне оформлення сторінок часто доручають графічному дизайнеру. Йому, ймовірно, не сподобалося б шукати у скрипті, якого він не розуміє, де саме знаходяться атрибути оформлення, які він має змінити.
Проте не можна нехтувати графічним оформленням сторінок. Адже саме це приваблює користувачів до сайту. У цьому випадку оформлення передається у стильовий аркуш. Сторінка main.php у своєму коді вказує стильовий аркуш, який слід використовувати для її відображення:
<head>
<title>Gestion d'articles</title>
<link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
</head>
У цьому документі використовується такий стильовий шаблон:
BODY {
background : url(../images/standard.jpg);
border : 2px none #FFDAB9;
font-family : Garamond;
font-size : 16px;
margin-left : 0px;
padding-left : 20px;
}
INPUT {
background : #EEE8AA;
border : 1px solid #EE82EE;
font-family : Garamond;
font-size : 18px;
}
INPUT.submit{
font-family : "Times New Roman";
font-size : 16px;
background : #FA8072;
border : 2px double Green;
font-weight : bold;
text-align : center;
vertical-align : middle;
cursor : pointer;
}
TD.menutitle{
background-image : url(../images/menugelgd.gif);
height : 23px;
text-align : center;
vertical-align : middle;
background : url(../images/menugelgd.gif) no-repeat center;
}
TD.menublock{
background : url(../images/bandegrismenugd.gif) repeat-x;
text-align : left;
vertical-align : middle;
}
A {
font-family : "Comic Sans MS";
color : #FF7F50;
font-size : 15px;
text-decoration : none;
}
A:HOVER {
background : #FFA07A;
color : Red;
}
FIELDSET {
border : 1px solid #A0522D;
background : #FFE4C4;
margin : 10px 10px 10px 10px;
padding-left : 10px;
padding-right : 10px;
padding-bottom : 10px;
}
LEGEND{
background : #FFA500;
}
TH {
background : #228B22;
text-align : center;
vertical-align : middle;
}
TD.libellé{
border : 1px solid #008B8B;
color : #339966;
}
H1 {
font : bold 20px/30px Garamond;
color : #FF7F50;
background : #D1E1F8;
background-attachment : fixed;
text-align : center;
vertical-align : middle;
font-family : Garamond;
}
SELECT.TEXT {
background : #6495ED;
text-align : center;
color : Aqua;
}
Ми не будемо заглиблюватися в деталі цього стильового шаблону. Приймемо його таким, яким він є. Трохи далі ми побачимо, як його створювати та змінювати. Для цього існують спеціальні програми. Проте зазначимо роль атрибутів оформлення, що використовуються в цьому шаблоні:
Атрибут: | визначає оформлення тегу HTML: |
<BODY> | |
<H1> (Заголовок 1) | |
<A> (Посилання) | |
визначає атрибути відображення анкора, коли користувач наводить на нього курсор миші | |
<FIELDSET> — цей тег не розпізнається всіма браузерами | |
<LEGEND> — цей тег не розпізнається всіма браузерами | |
<INPUT> | |
<INPUT class="TEXT"> | |
<INPUT class="SUBMIT"> | |
<TH> (Заголовок таблиці) | |
<TD class="menutitle"> (Дані таблиці) | |
<TD class="menublock"> | |
<TD class="libellé"> |
Давайте на прикладі розглянемо, як можна написати ці правила оформлення. У цьому прикладі ми будемо використовувати програму TopStyle Lite, яку можна безкоштовно завантажити на сайті URL http://www.bradsoft.com. Після завантаження таблиці стилів з’являється вікно з трьома областями:
- область редагування тексту. Атрибути оформлення можна задавати вручну, за умови знання правил написання таблиць стилів, які відповідають стандарту під назвою CSS (Cascading Style Sheets).
- у зоні 2 відображаються властивості атрибута, що створюється, які можна редагувати. Це найпростіший метод. Він дозволяє уникнути необхідності знати точні назви атрибутів оформлення, яких дуже багато
- у зоні 3 показано візуальний вигляд атрибута, що створюється
![]() |
У зоні 1 вище скопіюємо та вставимо атрибут INPUT.submit в атрибут INPUT.fantaisie. Цей атрибут визначатиме оформлення тегу HTML <INPUT class="fantaisie">
![]() |
Використаємо зону 2, щоб змінити деякі властивості атрибута INPUT.fantaisie:
![]() |
Відтепер будь-який тег <INPUT ... class="fantaisie">, знайдений на сторінці HTML, пов’язаній із попереднім стильовим шаблоном, буде відображатися так, як показано у прикладі зони 3 вище.
Таблиці стилів мають велике значення. Їх використання дозволяє змінити «зовнішній вигляд» веб-додатка, змінивши лише один елемент — таблицю стилів. Старі браузери не підтримують таблиці стилів. Наведена нижче директива <link ..> буде проігнорована деякими з них:
<head>
<title>Gestion d'articles</title>
<link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
</head>
У нашому додатку це дасть таку головну сторінку:

Це мінімальна сторінка без графічного оформлення. Може бути й гірше. Деякі версії браузерів розпізнають таблиці стилів, але неправильно їх інтерпретують. У результаті може вийти спотворена й непридатна для використання сторінка. Тому постає питання про тип клієнтського браузера. Існують методи, які допомагають визначити тип клієнтського браузера. Вони не є повністю надійними. Тоді можна написати різні таблиці стилів для різних браузерів або навіть створити версію без таблиці стилів для браузерів, які їх ігнорують. Звичайно, це ускладнює процес розробки. Цю важливу проблему тут було проігноровано.
За допомогою таблиць стилів можна створити персоналізоване середовище для користувачів нашого додатка. Ми могли б запропонувати їм сторінку з кількома можливими стилями оформлення. Вони могли б вибрати той, який їм найбільше підходить. Цей вибір можна було б зберегти в базі даних. Коли користувач знову увійде в систему, додаток можна було б запустити з таблицею стилів, яку він обрав.
7.5.5. Модуль введення даних додатка
Клієнти знатимуть про додаток лише його модуль введення: apparticles.php. Основні принципи його роботи такі:
- Запит клієнта отримується та аналізується. Він може бути налаштований або ні. Якщо запит містить параметри, очікувані параметри такі: action=[action]&phase=[phase]&PHPSESSID=[PHPSESSID]
- Якщо запит не містить параметрів або отримані параметри не відповідають очікуваним, сервер надсилає у відповідь сторінку автентифікації (логін, пароль). Як тільки користувач правильно авторизується, створюється сесія. Вона слугуватиме для зберігання інформації протягом усього обміну даними між клієнтом і сервером.
- Якщо запит правильно розпізнано, його обробляє модуль, який залежить як від дії, так і від поточної фази.
- Усі звернення до бази даних здійснюються через бізнес-клас articles.php.
- Обробка запиту завжди завершується відправкою клієнту сторінки main.php, у якій у $main['contenu'] вказаноURL сторінки, яку потрібно розмістити в зоні 4 типової сторінки.
Структура скрипта apparticles.php може виглядати наступним чином:
<?php
// управління таблицею товарів
include "config.php";
include "articles.php";
// необхідні дії
$sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
$sAction=strtolower($sAction);
// можливий етап
$sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";
// сесія
session_start();
$dSession=$_SESSION["session"];
// чи є активна сесія?
if(! isset($dSession)){
// аутентифікація користувача
if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
// неправильний запит
authentifier_0($dConfig);
}//if — сесія відсутня
// відновлюємо сесію
$dSession=unserialize($dSession);
// обробка запиту
// ----- автентифікація
if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
// ----- додавання статті
if($sAction=="addarticle" && $sPhase=="0") addArticle_0($dConfig,$dSession);
if($sAction=="addarticle" && $sPhase=="1") addArticle_1($dConfig,$dSession);
if($sAction=="addarticle" && $sPhase=="2") addArticle_2($dConfig,$dSession);
// ----- оновлення товару
if($sAction=="updatearticle" && $sPhase=="0") updateArticle_0($dConfig,$dSession);
if($sAction=="updatearticle" && $sPhase=="1") updateArticle_1($dConfig,$dSession);
if($sAction=="updatearticle" && $sPhase=="2") updateArticle_2($dConfig,$dSession);
if($sAction=="updatearticle" && $sPhase=="3") updateArticle_3($dConfig,$dSession);
// ----- видалення статті
if($sAction=="deletearticle" && $sPhase=="0") deleteArticle_0($dConfig,$dSession);
if($sAction=="deletearticle" && $sPhase=="1") deleteArticle_1($dConfig,$dSession);
if($sAction=="deletearticle" && $sPhase=="2") deleteArticle_2($dConfig,$dSession);
// ----- перегляд товарів
if($sAction=="selectarticle" && $sPhase=="0") selectArticle_0($dConfig,$dSession);
if($sAction=="selectarticle" && $sPhase=="1") selectArticle_1($dConfig,$dSession);
if($sAction=="selectarticle" && $sPhase=="2") selectArticle_2($dConfig,$dSession);
// ----- відправлення запиту SQL
if($sAction=="sql" && $sPhase=="0") sql_0($dConfig,$dSession);
if($sAction=="sql" && $sPhase=="1") sql_1($dConfig,$dSession);
if($sAction=="sql" && $sPhase=="2") sql_2($dConfig,$dSession);
// помилка — відображається сторінка авторизації
session_destroy();
authentifier_0($dConfig,"0");
...
?>
Слід звернути увагу на наступні моменти:
- функції, що обробляють конкретний запит клієнта, завершуються генерацією сторінки-відповіді та інструкцією exit, яка припиняє виконання скрипта apparticles.php. Іншими словами, з цих функцій не відбувається «повернення».
- функції приймають один або два параметри:
- $dConfig — це словник, що містить інформацію з конфігураційного файлу config.php. Усі функції використовують його.
- $dSession — це словник, що містить інформацію про сесію. Він існує лише тоді, коли сесія була створена, тобто після успішної автентифікації користувача. Саме тому функції автентифікації не мають цього параметра.
7.5.6. Сторінка помилок
Будь-яка програмна програма повинна вміти правильно обробляти помилки, що можуть виникнути. Веб-додаток не є винятком із цього правила. У цьому випадку, у разі помилки, ми розмістимо наступну сторінку erreurs.php у зоні 4 типової сторінки:
Les erreurs suivantes se sont produites :
<ul>
<?php
for($i=0;$i<count($main["erreurs"]);$i++){
echo "<li>".$main["erreurs"][$i]."</li>\n";
}//для
?>
</ul>
<a href="<?php echo $main["href"] ?>"><?php echo $main["lien"] ?></a>
Вона містить список помилок, визначений у $main['erreurs']. Крім того, вона може містити посилання для повернення, як правило, на сторінку, що передувала сторінці з помилками. Це посилання визначається текстом $main['lien'] та URL $main['href']. Щоб уникнути появи цього посилання, достатньо вказати порожній рядок у полі $main['lien']. Ось приклад сторінки помилок у випадку, коли користувач вводить неправильні дані для входу:

7.5.7. Інформаційна сторінка
Іноді потрібно надати користувачеві просту інформацію, наприклад, про те, що авторизація пройшла успішно. Для цього використовується така сторінка: infos.php:
Щоб відобразити інформацію у відповідь на запит клієнта,
- потрібно помістити інформацію з $main['infos']
- перенесемо URL з infos.php у $main['contenu']
Ось, наприклад, інформація, що повертається, коли користувач успішно пройшов аутентифікацію:

7.6. Принцип роботи додатка
Тепер ми маємо чітке уявлення про загальну структуру додатка, який потрібно написати. Залишилося описати маршрути користувача в додатку, дії, які він може виконувати, та відповіді, які він отримує від сервера. Зробивши це, ми зможемо написати функції, що обробляють різні запити клієнта. Далі ми розглянемо роботу додатка на прикладі сторінок, що відображаються користувачеві у відповідь на певні його дії. Кожного разу ми будемо вказувати такі дані:
початкова дія користувача, яка призвела до відображення відповіді | |
параметри, надіслані браузером клієнта на сервер у відповідь на ручну дію користувача | |
скрипт, що генерує зону 4 типової сторінки |
7.6.1. Аутентифікація
Перш ніж почати користуватися додатком, користувач повинен пройти аутентифікацію за допомогою наступної сторінки:

1 — початковий запит на URL apparticles.php 2 — використання опції «Автентифікація» в меню 3 — прямий запит на URL articles.php із неправильними параметрами | |
1 — відсутність параметрів 2 — action=authentifier?phase=0 3 — список неправильних параметрів | |
login.php |
На головній сторінці посилання [Ajouter un article] має такий вигляд: action=addarticle?phase=0. Інші посилання мають такий самий вигляд із action=(authentifier, updatearticle, deletearticle, selectarticle, sql). Користувач заповнює форму та натискає кнопку [Connexion]:

Відповідь має такий вигляд:

bouton [Connexion] | |
action=authentifier?phase=1 | |
infos.php |
Заголовок сторінки було змінено, щоб вказати логін користувача та його права адміністратора/користувача. Крім того, усі посилання в зоні 2 було змінено, щоб відобразити факт початку сеансу. До них було додано параметр PHPSESSID=[PHPSESSID].
Якщо сервер не зміг ідентифікувати клієнта, той отримає іншу відповідь:

bouton [Connexion] | |
action=authentifier?phase=1 | |
erreurs.php |
Посилання [Retour à la page de login] — це посилання на URL apparticles.php?action=authentifier&phase=2&txtLogin=x. Це посилання повертає клієнта на сторінку входу, де поле для входу заповнюється значенням параметра txtLogin:

lien [Retour à la page de login] | |
action=authentifier?phase=2&txtLogin=x | |
login.php |
7.6.2. Додавання статті
Посилання в меню [Ajouter un article] відкриває наступну сторінку в зоні 4 типової сторінки:

lien [Ajouter un article] | |
action=addArticle?phase=0&PHPSESSID=[PHPSESSID] | |
addarticle.php |
Користувач заповнює поля та надсилає все на сервер за допомогою кнопки [Ajouter], яка має тип submit. На стороні клієнта перевірка не проводиться. Її здійснює сервер. У відповідь сервер може надіслати сторінку з повідомленням про помилку, як у прикладі нижче:
Запит | Відповідь |
![]() | ![]() |
bouton [Ajouter] | |
action=addArticle?phase=1&PHPSESSID=[PHPSESSID] | |
erreurs.php |
За посиланням [Retour à la page d'ajout d'article] можна повернутися на сторінку введення даних:
Запит | Відповідь |
![]() | ![]() |
lien [Retour à la page d'ajout d'article] | |
action=addArticle?phase=2&PHPSESSID=[PHPSESSID] | |
article.php |
Якщо додавання відбулося без помилок, користувач отримує повідомлення з підтвердженням:
Запит | Відповідь |
![]() | ![]() |
bouton [Ajouter] | |
action=addArticle?phase=1&PHPSESSID=[PHPSESSID] | |
infos.php |
7.6.3. Перегляд статей
Посилання в меню [Lister des articles] відкриває наступну сторінку в зоні 4 типової сторінки:

посилання в меню [Lister des articles] | |
action=selectArticle?phase=0&PHPSESSID=[PHPSESSID] | |
select1.php |
Запит select [colonnes] from articles where [where] orderby [orderby] буде відправлений до таблиці статей, де [colonnes], [where] та [orderby] є значеннями вищезазначених полів. Наприклад:
Запит |
![]() |
Відповідь |
![]() |
bouton [Afficher] | |
action=selectArticle?phase=1&PHPSESSID=[PHPSESSID] | |
select2.php |
Запит може бути помилковим, і в цьому випадку клієнт отримує сторінку з повідомленням про помилку:
Запит |
![]() |
Відповідь |
![]() |
У обох випадках (незалежно від наявності помилок) посилання [Retour à la page de sélection d'articles] дозволяє повернутися на сторінку select1.php:
Запит |
![]() |
Відповідь |
![]() |
lien [Retour à la page de sélection d'articles] | |
action=selectArticle?phase=2&PHPSESSID=[PHPSESSID] | |
select1.php |
7.6.4. Редагування статей
Посилання в меню [Modifier un article] відкриває наступну сторінку в зоні 4 шаблону сторінки:

посилання в меню [Modifier un article] | |
action=updateArticle?phase=0&PHPSESSID=[PHPSESSID] | |
updatearticle1.php |
Вибираємо код статті, яку потрібно змінити, у випадаючому списку та вводимо [OK], щоб змінити статтю з цим кодом:
Запит | Відповідь |
![]() | ![]() |
bouton [OK] | |
action=updateArticle?phase=1&PHPSESSID=[PHPSESSID] | |
updatearticle2.php |
Отримавши картку статті, яку потрібно змінити, користувач може внести свої зміни:
Запит | Відповідь |
![]() | ![]() |
bouton [Modifier] | |
action=updateArticle?phase=2&PHPSESSID=[PHPSESSID] | |
infos.php |
Користувач може припуститися помилок під час редагування:
Запит | Відповідь |
![]() | ![]() |
За посиланням [Retour à la page de modification d'article] можна повернутися на сторінку введення даних:

lien [Retour à la page de modification d'article] | |
action=updateArticle?phase=3&PHPSESSID=[PHPSESSID] | |
updatearticle2.php |
7.6.5. Видалення статті
Посилання в меню [Supprimer un article] відкриває наступну сторінку в зоні 4 типової сторінки:

посилання в меню [Supprimer un article] | |
action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID] | |
deletearticle1.php |
Користувач вибирає код статті, яку потрібно видалити, зі списку, що розгортається:
Запит | Відповідь |
![]() | ![]() |
bouton [OK] | |
action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID] | |
deletearticle2.php |
Користувач підтверджує видалення статті за допомогою кнопки [Supprimer]:
Запит | Відповідь |
![]() | ![]() |
bouton [Supprimer] | |
action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID] | |
infos.php |
7.6.6. Відправлення запитів адміністратора
Посилання в меню [Requête SQL] відкриває наступну сторінку в зоні 4 типової сторінки:

посилання в меню [Requête SQL] | |
action=sql?phase=0&PHPSESSID=[PHPSESSID] | |
sql1.php |
Введіть текст запиту SQL у поле введення та натисніть кнопку [Exécuter], щоб виконати його. Тільки адміністратор може надсилати такі запити, як показано в наступному прикладі:
Запит | Відповідь |
![]() | ![]() |
bouton [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
erreurs.php |
За посиланням [Retour à la page d'émission de requêtes SQL] можна повернутися на сторінку введення даних:

lien [Retour à la page d'émission de requêtes SQL] | |
action=sql?phase=2&PHPSESSID=[PHPSESSID] | |
sql1.php |
Якщо ви є адміністратором і запит є синтаксично правильним:
Запит |
![]() |
отримуємо результат запиту:
Відповідь |
![]() |
bouton [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
sql2.php |
Можна надсилати запити на оновлення таблиць:
Запит |
![]() |
Відповідь |
![]() |
bouton [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
infos.php |
7.6.7. Завдання
Написати скрипти та функції, необхідні для роботи додатка:
ідентифікатор | тип | роль |
скрипт | точка входу для обробки запитів клієнтів | |
функція | обробляє запит із параметрами action=authentifier&phase=0 | |
функція | обробляє запит із параметрами action=authentifier&phase=1 | |
функція | обробляє запит із параметрами action=authentifier&phase=2 | |
функція | обробляє запит із параметрами action=addArticle&phase=0 | |
функція | обробляє запит із параметрами action=addArticle&phase=1 | |
функція | обробляє запит із параметрами action=addArticle&phase=2 | |
функція | обробляє запит із параметрами action=updatearticle&phase=0 | |
функція | обробляє запит із параметрами action=updatearticle&phase=1 | |
функція | обробляє запит із параметрами action=updatearticle&phase=2 | |
функція | обробляє запит із параметрами action=updatearticle&phase=3 | |
функція | обробляє запит із параметрами action=deletearticle&phase=0 | |
функція | обробляє запит із параметрами action=deletearticle&phase=1 | |
функція | обробляє запит із параметрами action=deletearticle&phase=2 | |
функція | обробляє запит із параметрами action=selectarticle&phase=0 | |
функція | обробляє запит із параметрами action=selectarticle&phase=1 | |
функція | обробляє запит із параметрами action=selectarticle&phase=2 | |
функція | обробляє запит із параметрами action=sql&phase=0 | |
функція | обробляє запит із параметрами action=sql&phase=1 | |
функція | обробляє запит із параметрами action=sql&phase=2 | |
скрипт | генерує сторінку типу | |
скрипт | генерує сторінку входу | |
скрипт | генерує сторінку помилок | |
скрипт | генерує інформаційну сторінку | |
скрипт | генерує сторінку додавання статті | |
скрипт | генерує сторінку 1 редагування статті | |
скрипт | генерує сторінку 2 редагування статті | |
скрипт | генерує сторінку 1 видалення статті | |
скрипт | генерує сторінку 2 видалення статті | |
скрипт | генерує сторінку 1 вибору статей | |
скрипт | генерує сторінку 2 добірки статей | |
скрипт | генерує сторінку 1 з переліку запитів | |
скрипт | генерує сторінку 2 з переліком запитів |
7.7. Розвиток додатка
На даний момент ми маємо додаток, який виконує свої функції з прийнятною ергономікою. Ми будемо вдосконалювати його в різних аспектах:
- SGBD
- його безпеку
- зовнішній вигляд
- його продуктивність
7.7.1. Зміна типу бази даних
У нашому дослідженні передбачалося, що використовуваний SGBD був MySQL. Змініть на SGBD і покажіть, що єдину зміну потрібно внести у визначення змінної $dDSN у конфігураційному файлі config.php.
7.7.2. Підвищення безпеки
Під час розробки веб-додатку ніколи не слід припускати, що клієнтом є браузер і що запит, який він надсилає, контролюється формою, надісланою йому перед цим запитом. Будь-яка програма може бути клієнтом веб-додатку і, отже, надсилати до нього будь-які запити — як з параметрами, так і без них. Тому додаток повинен перевіряти все.
Якщо звернутися до коду скрипта apparticles.php, можна побачити,
- що без сесії не може відбуватися жодна дія, крім автентифікації. Сесія існує лише в тому випадку, якщо користувач успішно пройшов автентифікацію. Нагадаємо, що сесія ідентифікується досить довгим рядком символів, який називається токеном сесії та має такий вигляд: 176a43609572907333118333edf6d1fb. Цей токен можна надіслати до додатка різними способами, наприклад, використовуючи налаштований URL:
apparticles.php?PHPSESSID=176a43609572907333118333edf6d1fb.
Програма, яка б неодноразово запитувала попередній URL, випадковим чином змінюючи токен у надії знайти правильний, з великою ймовірністю витратить багато днів на генерацію правильної комбінації, оскільки кількість можливих комбінацій надзвичайно велика. До того часу сесія, тривалість якої обмежена, найімовірніше, вже закінчиться. Інший ризик полягає в тому, що токен, який передається у відкритому вигляді через мережу, може бути перехоплений. Цей ризик є реальним. У такому разі можна використовувати зашифроване з’єднання між сервером та клієнтом.
- щоб після запуску сесії дозволялися лише певні дії. Токен URL із параметрами action=tricher&phase=0&PHPSESSID=[PHPSESSID] буде відхилено, оскільки дія «tricher» не є дозволеною. Коли параметри (action, phase) не розпізнаються, наш додаток відображає сторінку автентифікації.
Однак додаток не перевіряє, чи дозволені дії послідовно виконуються правильно. Наприклад, такі дві дії:
- action=addArticle&phase=0&PHPSESSID=[PHPSESSID]
- action=updateArticle&phase=1&PHPSESSID=[PHPSESSID]
є двома дозволеними діями. Однак дія 2 не може слідувати за дією 1.
Як відстежувати послідовність запитів URL, що надходять від браузера клієнта?
Для цього можна скористатися двома змінними PHP: $_SERVER['REQUEST_URI] та $_SERVER['HTTP_REFERER] — це дві інформації, що надсилаються клієнтськими браузерами у їхніх заголовках HTTP.
$_SERVER['REQUEST_URI]: це URI, запитуваний клієнтом. Наприклад
$_SERVER['HTTP_REFERER]: Це URL, який відображався у браузері до того, як браузер почав запитувати новий URL (попередній URI). Наприклад, якщо браузер, який відображав згаданий вище файл URI, надсилає новий запит на сервер, змінна $_SERVER['HTTP_REFERER'] цього сервера матиме значення
Щоб перевірити, чи дві дії нашого додатка виконуються послідовно, можна вчинити так:
Під час дії 1:
- записуємо запитуваний код URI (URI1) та вносимо його до сесії
Під час дії 2:
- отримуємо HTTP-REFERER з дії 2. З нього виводиться URI (URI2) на основі URL, який раніше відображався у браузері, що надсилає запит.
- отримуємо URI URI1, який зберігався в сесії і є URI дії, запитуваної раніше на сервері
- Якщо дія 2 є наступною за дією 1, то має виконуватися умова URI2=URI1. Якщо це не так, то запит на виконання дії відхиляється, і відображається сторінка автентифікації.
- у сесії фіксується код URI URI2 поточної дії для перевірки наступної дії. І так далі.
Ось приклад. Після автентифікації вибираємо посилання [Ajouter un article]:

URL цієї сторінки:
http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=addArticle&phase=0&PHPSESSID=006a63e6027f16c70b63cdae93405eeb
Прямо в полі [Adresse] браузера ми змінюємо URL наступним чином:
http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=deleteArticle&phase=1&PHPSESSID=006a63e6027f16c70b63cdae93405eeb
Після цього ми потрапляємо на сторінку авторизації:

Це потребує пояснення. Коли ми запитуємо URL, вводячи його безпосередньо в поле адреси браузера, браузер не надсилає заголовок HTTP_REFERER. Тому наш додаток не знаходить там URI з попередньої дії — URI, який він запам’ятав у сесії. Тоді він у відповідь повертає сторінку автентифікації.
Цей механізм ефективний для веб-браузерів, але абсолютно не діє для програмного клієнта. Останній може надіслати будь-який заголовок HTTP_REFERER на свій розсуд. Таким чином, він може «обдурити» систему, стверджуючи, що він дійсно пройшов певний етап, хоча насправді цього не зробив. Тому необхідно переконатися, що послідовність етапів дотримується. Так, якщо запитувана дія — action=addArticle&phase=1 (введення даних), то попередньою дією обов’язково має бути action=deleteArticle&phase=0 (початковий запит на сторінку введення даних) або action=addArticle&phase=2 (повернення до введення даних після помилкового додавання). Аналогічно, якщо запитувана дія — action=addArticle&phase=2 (додавання), то попередньою дією має бути action=addArticle&phase=1 (введення даних). Можна змусити користувача дотримуватися цих послідовностей.
Тоді як перший механізм є загальним і може застосовуватися до будь-якого додатка, другий вимагає специфічного кодування для кожного додатка і є більш обтяжливим: потрібно переглянути всі можливі дії користувача та їхні послідовності. Останні можна зберегти у словнику, як показано в наступному коді:
// аутентифікація
$dPrec['authentifier']['0']=array();
$dPrec['authentifier']['1']=array(
array('action'=>'authentifier','phase'=>'0'),
array('action'=>'authentifier','phase'=>'2')
);
$dPrec['authentifier']['2']=array(
array('action'=>'authentifier','phase'=>'1'),
);
// додавання статті
$dPrec['addarticle']['0']=array();
$dPrec['addarticle']['1']=array(
array('action'=>'addarticle','phase'=>'0'),
array('action'=>'addarticle','phase'=>'2')
);
$dPrec['addarticle']['2']=array(
array('action'=>'addarticle','phase'=>'1'),
);
// редагування статті
$dPrec['updatearticle']['0']=array();
$dPrec['updatearticle']['1']=array(
array('action'=>'updatearticle','phase'=>'0'),
);
$dPrec['updatearticle']['2']=array(
array('action'=>'updatearticle','phase'=>'1'),
array('action'=>'updatearticle','phase'=>'3')
);
$dPrec['updatearticle']['3']=array(
array('action'=>'updatearticle','phase'=>'2'),
);
// видалення товару
$dPrec['deletearticle']['0']=array();
$dPrec['deletearticle']['1']=array(
array('action'=>'deletearticle','phase'=>'0'),
);
$dPrec['deletearticle']['2']=array(
array('action'=>'deletearticle','phase'=>'1'),
);
// вибір статей
$dPrec['selectarticle']['0']=array();
$dPrec['selectarticle']['1']=array(
array('action'=>'selectarticle','phase'=>'0'),
array('action'=>'selectarticle','phase'=>'2')
);
$dPrec['selectarticle']['2']=array(
array('action'=>'selectarticle','phase'=>'1'),
);
// запит адміністратора
$dPrec['sql']['0']=array();
$dPrec['sql']['1']=array(
array('action'=>'sql','phase'=>'0'),
array('action'=>'sql','phase'=>'2')
);
$dPrec['sql']['2']=array(
array('action'=>'sql','phase'=>'1'),
);
$dPrec['action']['phase'] — це таблиця, що містить дії, які можуть передувати дії та фазі, що слугують індексом для словника. Ці попередні дії також представлені словником із двома ключами: «action» та «phase». Якщо дії може передувати будь-яка дія, то $dPrec['action']['phase'] буде порожнім масивом. Відсутність дії у словнику означає, що вона не дозволена. Розглянемо наведену вище дію «автентифікувати»:
// аутентифікація
$dPrec['authentifier']['0']=array();
$dPrec['authentifier']['1']=array(
array('action'=>'authentifier','phase'=>'0'),
array('action'=>'authentifier','phase'=>'2')
);
$dPrec['authentifier']['2']=array(
array('action'=>'authentifier','phase'=>'1'),
);
Наведений вище код означає, що перед дією action=authentifier&phase=0 може йти будь-яка дія, що дії action=authentifier&phase=1 може передувати дія action=authentifier&phase=0 або action=authentifier&phase=2, а дії action=authentifier&phase=2 може передувати дія action=authentifier&phase=1.
Напишіть таку функцію:
// ---------------------------------------------------------------
function enchainementOK(&$dConfig, &$dSession, $sAction, $sPhase){
// перевіряє, чи поточна дія ($sAction, $sPhase) може слідувати за попередньою дією
// збережена в $dSession['précédent']
// словник дозволених ланцюжків знаходиться в $dConfig['précédents']
// повертає TRUE, якщо послідовність можлива, інакше — FALSE
....
Ця функція дозволяє головному додатку перевірити правильність послідовності дій:
<?php
// управління таблицею товарів
include "config.php";
include "articles.php";
// сесія
session_start();
$dSession=$_SESSION["session"];
// необхідні дії
$sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
$sAction=strtolower($sAction);
// можлива фаза дії
$sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";
// чи є поточна сесія?
if(! isset($dSession)){
// аутентифікація користувача
if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
// аномальна дія
authentifier_0($dConfig);
}//if — сеансу немає
// отримано сесію
$dSession=unserialize($dSession);
// чи є послідовність дій нормальною?
if( ! enchainementOK($dConfig,$dSession,$sAction,$sPhase)){
// послідовність ненормальна
authentifier_0($dConfig);
}//if
// обробка дій
if($sAction=="authentifier"){
if($sPhase=="0") authentifier_0($dConfig);
if($sPhase=="1") authentifier_1($dConfig);
if($sPhase=="2") authentifier_2($dConfig);
}//if
if($sAction=="addarticle"){
...
7.7.3. Зміна зовнішнього вигляду
Нагадаємо, що однією з умов, висунутих під час розробки цього додатка, була його розширюваність. Припустимо, що через кілька тижнів ми зрозуміємо, що ергономіку додатка потрібно покращити. Змініть додаток таким чином, щоб змінити структуру та оформлення типової сторінки. Зміни відбуватимуться у двох місцях:
- у скрипті main.php, який визначає структуру типової сторінки. Змініть її.
- у таблиці стилів, яка визначає «зовнішній вигляд» додатка. Змініть її.
7.7.4. Покращення продуктивності
Наразі ми обрали «легкий» клієнтський браузер: він виконує лише функції візуалізації. Можна змусити його виконувати обробку даних, включивши скрипти у веб-сторінки, які йому надсилаються. Ці скрипти можуть бути написані різними мовами, зокрема VBScript та JavaScript. Internet Explorer та Netscape домінують на ринку браузерів у співвідношенні приблизно 60/40. Крім того, IE існує лише в середовищі Windows, а не, наприклад, в Unix, де переважає Netscape. Netscape не виконує скрипти VBScript у нативному режимі, тоді як обидва браузери виконують скрипти JavaScript. Оскільки Netscape все ще займає значну частку ринку браузерів, слід уникати використання скриптів VBScript. Тому в клієнтських скриптах зазвичай використовується JavaScript.
На клієнтські скрипти делегуються операції, в яких сервер не повинен брати участь. У нашому додатку було б доцільно, щоб браузер клієнта надсилав запит на сервер лише після його перевірки. Таким чином, немає сенсу надсилати на сервер запит на автентифікацію, якщо користувач залишив поле [login] у формі автентифікації порожнім. Краще попередити користувача про те, що його запит містить помилку:

Слід зауважити, що це не завадить серверу перевірити, чи поле login не є порожнім, оскільки його клієнт не обов’язково є браузером, і тому попередня перевірка могла не відбутися. Припущення, що клієнт є браузером, становить серйозний ризик для безпеки додатка.
Проаналізуйте різні моменти, коли браузер надсилає інформацію на сервер, і, коли її можна перевірити, напишіть одну або кілька функцій JavaScript, які дозволять браузеру перевіряти правильність інформації перед її надсиланням на сервер.
Якщо повернутися до попереднього прикладу, скрипт login.php, який генерує сторінку автентифікації, виглядатиме так:
<script language="javascript">
function check(){
// перевіряємо, чи дійсно є логін
with(document.frmLogin){
champs=/^\s*$/.exec(txtLogin.value);
if(champs!=null){
// логін відсутній
alert("Vous n'avez pas indiqué de login");
txtLogin.focus();
return;
}//if
// дані є — надсилаємо їх на сервер
submit();
}//з
}//перевірка
</script>
<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
<table>
<tr>
<td>login</td>
<td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
</tr>
<tr>
<td>mot de passe</td>
<td><input type="password" value="" name="txtMdp" class="text"></td>
<td><input type="button" onclick="check()" value="Connexion" class="submit"></td>
</tr>
</table>
</form>
7.8. Додаткова інформація
На завершення наведемо кілька ідей для поглиблення цього кейсу:
- було б цікаво перевірити, чи не можна було б створити клас на основі типової сторінки цього додатка. Тоді його можна було б використовувати в інших додатках.
- наш додаток добре підходить для клієнтів типу «браузер», але менш придатний для клієнтів типу «автономний додаток». Останні повинні:
- встановити TCP-з’єднання з сервером
- «спілкуватися» з ним за протоколом HTTP
- аналізувати його відповіді HTML, щоб знайти потрібну інформацію, оскільки автономний клієнт, ймовірно, не буде зацікавлений у коді HTML, призначеному для відображення у браузерах.
Було б цікаво, щоб наш додаток генерував XML замість HTML. Тоді її клієнтами могли б бути як веб-браузери (хоча й досить нові), так і автономні додатки. Останні не матимуть жодних труднощів із пошуком потрібної інформації, оскільки відповідь XML від сервера не міститиме жодної інформації про оформлення, а лише сам вміст.
- Безумовно, слід звернути увагу на одночасний доступ до бази статей. Потрібно з’ясувати принаймні два моменти:
- чи правильно обробляє SGBD, який використовує додаток, одночасний доступ до однієї й тієї ж статті? Наприклад, що станеться, якщо двоє користувачів одночасно редагуватимуть одну й ту саму статтю (вони одночасно натиснуть кнопку [Modifier])? Це, ймовірно, залежить від базового SGBD.
- Наразі наш додаток не підтримує одночасний доступ. Однак база даних має залишатися в узгодженому стані, хоча й слід очікувати несподіванок. Розглянемо таку послідовність подій:
- користувач U1 переходить до редагування статті
- трохи пізніше користувач U2 переходить до видалення тієї самої статті
- кожна з цих двох дій вимагає обміну даними між клієнтом і сервером. Залежно від стилю роботи кожного з них, користувач U2 може завершити свою роботу раніше, ніж U1. Коли останній завершить свої зміни та підтвердить їх за допомогою [Modifier], у відповідь він отримає інформаційну сторінку, де SGBD повідомить йому, що [0 ligne(s) ont été modifiées], оскільки сторінка, яку він хотів редагувати, була тим часом видалена. Користувач, безсумнівно, буде здивований. З точки зору ергономіки, напевно, було б краще відображати сторінку, яка чіткіше вказує на помилку. Крім того, можна було б розглянути можливість надання користувачеві ексклюзивного доступу до статті, щойно він почне її редагувати. Інший користувач, який захоче редагувати ту саму статтю, отримає повідомлення про те, що інше редагування вже триває. Це створить проблему, якщо перший користувач затримається з підтвердженням своїх змін: інші користувачі будуть заблоковані. Тут потрібно знайти рішення, які значною мірою залежатимуть від можливостей використовуваного модуля SGBD. Наприклад, Oracle має більше можливостей у цій сфері, ніж MySQL.































