17. Веб-сервіси
Примітка: під веб-сервісом тут мається на увазі будь-який веб-додаток, що надає необроблені дані, які використовуються клієнтом (у наведених нижче прикладах — консольним скриптом). Ми не розглядаємо конкретні технології, наприклад REST (REpresentational State Transfer) або SOAP (Simple Object Access Protocol), які надають більш-менш необроблені дані у чітко визначеному форматі. REST надає jSON, тоді як для SOAP — це XML. Кожна з цих технологій точно описує, як клієнт повинен надсилати запит до сервера та який вигляд має мати відповідь сервера. У цьому курсі ми будемо набагато гнучкішими щодо характеру запиту клієнта та відповіді сервера. Проте написані скрипти та використовувані інструменти близькі до тих, що застосовуються в технології REST.
17.1. Introduction
Оскільки програми PHP можуть виконуватися сервером WEB, така програма стає серверною програмою, здатною обслуговувати декількох клієнтів. З точки зору клієнта виклик веб-сервісу зводиться до запиту на URL цього сервісу. Клієнт може бути написаний будь-якою мовою, зокрема на PHP. В останньому випадку використовуються мережеві функції, які ми щойно розглянули. Крім того, нам потрібно вміти «спілкуватися» з веб-сервісом, тобто розуміти протокол http, що забезпечує зв’язок між сервером WEB та його клієнтами. Саме цьому був присвячений попередній абзац.
Веб-клієнт, описаний у розділі «посилання», дав нам змогу ознайомитися з частиною протоколу HTTP.

У найпростішому варіанті обмін даними між клієнтом і сервером відбувається наступним чином:
- клієнт встановлює з’єднання з портом 80 веб-сервера;
- він надсилає запит щодо документа;
- веб-сервер надсилає запитуваний документ і закриває з’єднання;
- у свою чергу, клієнт закриває з’єднання;
Документ може бути різного типу: текст у форматі HTML, зображення, відео… Це може бути існуючий документ (статичний документ) або документ, що генерується на льоту за допомогою скрипта (динамічний документ). В останньому випадку йдеться про веб-програмування. Скрипт для динамічного формування документів може бути написаний на різних мовах: PHP, Python, Perl, Java, Ruby, C#, VB.net…
Далі ми будемо використовувати скрипти PHP для динамічного формування текстових документів.

- у [1] клієнт встановлює з’єднання з сервером, запитує скрипт PHP, надсилає або не надсилає параметри для цього скрипта;
- у [2] веб-сервер запускає виконання скрипта PHP за допомогою інтерпретатора PHP. Скрипт генерує документ, який надсилається клієнту [3];
- сервер закриває з’єднання. Клієнт робить те саме;
Веб-сервер може обслуговувати декількох клієнтів одночасно.
У складі програмного пакету [Laragon] веб-сервером є Apache — сервер з відкритим кодом, розроблений Apache Foundation (http://www.apache.org/). У наведених нижче прикладах застосування необхідно запустити [Laragon]:

Це запускає веб-сервер Apache, а також SGBD та MySQL.
Скрипти, що виконуються веб-сервером, будуть написані за допомогою інструменту NetBeans. Наразі ми написали скрипти PHP, що виконуються в консольному режимі:

Користувач використовує консоль, щоб запустити скрипт PHP та отримати його результати.
У наступних клієнт-серверних додатках:
- скрипт клієнта виконується в консольному середовищі;
- скрипт сервера виконується у веб-контексті;

Скрипт PHP сервера не може знаходитися в довільному місці файлової системи. Адже веб-сервер шукає статичні та динамічні документи, які йому запитують, у місцях, визначених конфігурацією. За замовчуванням у налаштуваннях Laragon документи шукаються у папці <Laragon>/www, де <Laragon> — це папка інсталяції Laragon. Отже, якщо веб-клієнт запитує документ D із шляхом URL [http://localhost/D], веб-сервер надасть йому документ D із шляхом [<Laragon>/www/D].
У наведених нижче прикладах ми розмістимо серверні скрипти в папці [www/php7/scripts-web]. Якщо серверний скрипт має назву S.php, його буде запитуватися у веб-сервера за URL [http://localhost/php7/scripts-web/S.php]. У цьому випадку йому буде надано документ [<Laragon>/www/php7/scripts-web/S.php].

- у [1] — файл [<laragon>/www];
- у [2], папку [php7/scripts-web];
Щоб створити серверні скрипти за допомогою NetBeans, ми будемо діяти наступним чином:

- у [1-2] ми створюємо новий проєкт
- у [3-4] вибираємо категорію [PHP] та проект [PHP Application]

- в [5] — назва проєкту;
- в [6] — папку проекту у файловій системі. Зверніть увагу, що вона знаходиться в папці [<laragon>/www], де й має бути;
- у [7-8] прийміть запропоновані значення за замовчуванням;
- у [9-10] — прийміть запропоновані значення за замовчуванням. У [10] зверніть увагу, що URL скриптів, які ми розмістимо в цьому проєкті, починатимуться зі шляху [http://localhost/php7/scripts-web/];

- у [11] вам пропонуються веб-фреймворки, написані на PHP. Ці фреймворки стають незамінними, щойно веб-додаток набуває певних масштабів;
- у [12] можна додавати бібліотеки PHP за допомогою інструменту [Composer]. Ми двічі використовували цей інструмент у вікні [Terminal] Laragon:
- для встановлення бібліотеки [SwiftMailer], яка дозволяє надсилати електронні листи;
- для встановлення бібліотеки [php-mime-mail-parser], яка дозволяє читати електронні листи;
- у [13], після підтвердження роботи майстра створення проєкту, він з’являється у [13] на вкладці проєктів;
17.2. Створення статичної сторінки
Примітка: Для подальших дій необхідно запустити [Laragon].
Ми покажемо, як створити статичну сторінку HTML (HyperText Markup Language) за допомогою NetBeans:

- у [1-5] ми створюємо папку з назвою [01];


- з [6-12] ми створюємо файл HTML [exemple-01.html];
Файл [exemple-01.html] генерується з попередньо заповненими даними наступним чином (травень 2019 року):
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>TODO write content</div>
</body>
</html>
Змінимо його вміст таким чином:
<!DOCTYPE html>
<html>
<head>
<title>PHP7 par l'exemple</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div><b>Ceci est un exemple de page statique</b></div>
</body>
</html>
Ми змінили заголовок сторінки (рядок 4), а також її вміст (рядок 9).
Тепер давайте відобразимо цю сторінку HTML за допомогою сервера Apache у Laragon:

- у [1-2] ми виводимо сторінку за допомогою сервера Apache в Laragon;
- у [3] — URL відображеної сторінки;
- у [4] — заголовок, який ми змінили;
- у [5] — зміст, який ми змінили;
Відображена сторінка є статичною: її можна завантажувати в браузері скільки завгодно разів (F5), і завжди відображається той самий вміст.
Більшість браузерів надають доступ до даних, якими обмінюються клієнт і сервер, — тих, що були описані в розділі «Посилання». У браузері Firefox (травень 2019 року) для отримання доступу до цих даних потрібно виконати F12:

Як зазначено в [1], перезавантажимо сторінку (F5):

- у [2] — документ, завантажений браузером: ми його виділяємо;

- в [5] документ для аналізу виділено;
- у [3-4] ми запитуємо перегляд обміну даними між клієнтом і сервером;
- у [6] — ці обміни;

- у [7] вибирається вкладка заголовків;
- у [8] — запит URL, який надіслав браузер;
- у [9] команда, надіслана на сервер, — це [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1];
- у [10] — заголовки HTTP, надіслані потім браузером (клієнтом);
- у [11] — заголовки HTTP з відповіді сервера;

- у [12-14] — відповідь сервера, надіслана після заголовків HTTP;
- у [14] видно, що клієнтський браузер отримав сторінку HTML, яку ми створили. Потім він інтерпретував цей код, щоб відобразити наступне:

17.3. Створення динамічної сторінки у форматі PHP
Тепер ми напишемо динамічну сторінку у форматі PHP:


- у [1-8] ми створюємо сторінку [exemple-01.php];
Файл [exemple-01.php] генерується з попередньо заповненими даними наступним чином (травень 2019 року):
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
// вставте свій код тут
?>
</body>
</html>
Ми змінюємо наведений вище код наступним чином:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// час: кількість мілісекунд від поточного моменту до 01.01.1970
// формат відображення дати та часу
// d: день у форматі двох цифр
// m: місяць (2 цифри)
// y: рік у 2 цифри
// H: година 0,23
// I: хвилини
// s: секунди
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
Коментарі
- рядок 5: ми змінили заголовок сторінки;
- рядок 17: записує поточну дату та час;
В основному, наведений вище скрипт PHP виводить поточний час на консоль. Однак під час виконання на веб-сервері потік виводу команди [print], який зазвичай пов’язаний із консоллю виконання скрипта, перенаправляється до з’єднання між сервером і клієнтом. Отже, у веб-контексті наведений вище скрипт надсилає поточний час у вигляді тексту клієнту, у даному випадку — браузеру.
Запустимо скрипт [exemple-01.php]:

- у [3], URL, що запитується у веб-сервера Apache;
- у [4] — заголовок сторінки, який ми змінили;
- у [5] — вміст, згенерований командою [print];
Ми маємо тут динамічну сторінку, оскільки якщо кілька разів перезавантажити сторінку в браузері (F5), її вміст змінюється (змінюється час).
Браузер отримав потік HTML. Щоб дізнатися його, потрібно відобразити вихідний код сторінки в браузері:

- щоб отримати меню [1], клацніть правою кнопкою миші на сторінці в браузері;
- у [2] — це URL сторінки [exemple-01.php], але з префіксом [view-source :] [3];
- у [4] — вміст HTML, який відобразив браузер;
Отже, слід пам’ятати, що скрипт PHP, призначений для виконання веб-сервером, повинен генерувати потік HTML.
Тепер розглянемо (F12) заголовки HTTP, які сервер надсилає клієнтському браузеру:

- у [3] з’являється заголовок HTTP, якого не було під час запиту на статичну сторінку. Цей заголовок свідчить про те, що відповідь сервера була згенерована скриптом PHP;
Ми побачили, що відповідь (у даному випадку потік HTML) сервера може бути згенерована скриптом PHP. Цей скрипт також може генерувати заголовки HTTP та практично всі елементи відповіді сервера.
17.4. Основи мови HTML
У цьому розділі не буде детально розглядатися програмування на мові WEB. Веб-додаток на мові MVC розроблено у розділі за посиланням. Цей розділ присвячений скоріше веб-сервісам: PHP-сторінкам, які через веб-сервер надають дані іншим клієнтам PHP. Проте нам здалося корисним надати читачеві деякі основи HTML.
Веб-браузер може відображати різні документи, найпоширенішим з яких є документ HTML (HyperText Markup Language). Це текст, відформатований за допомогою тегів у формі <balise>texte</balise>. Так, тег <b>important</b> відображатиме текст important жирним шрифтом. Існують також окремі теги, такі як тег <hr/>, який відображає горизонтальну лінію. Ми не будемо розглядати теги, які можна знайти в тексті HTML. Існує чимало програм WYSIWYG, що дозволяють створити сторінку WEB без написання жодного рядка коду HTML. Ці інструменти автоматично генерують код HTML на основі макета, створеного за допомогою миші та попередньо визначених елементів управління. Таким чином, можна (за допомогою миші) вставити на сторінку таблицю, а потім переглянути код HTML, згенерований програмним забезпеченням, щоб дізнатися, які теги слід використовувати для визначення таблиці на сторінці WEB. Все дуже просто. Крім того, знання мови HTML є необхідним, оскільки динамічні веб-додатки повинні самостійно генерувати код HTML для надсилання клієнтам WEB. Цей код генерується програмно, і, звісно, потрібно знати, що саме слід згенерувати, щоб клієнт отримав бажану веб-сторінку.
Підсумовуючи, для початку веб-програмування зовсім не обов’язково знати всю мову HTML. Однак ці знання необхідні, і їх можна здобути завдяки використанню програмного забезпечення WYSIWYG для створення веб-сторінок WEB, таких як DreamWeaver та десятки інших. Інший спосіб ознайомитися з тонкощами мови HTML — це переглядати веб-сторінки та вивчати вихідний код тих сторінок, які мають цікаві та ще невідомі вам особливості.
Розглянемо наступний приклад, який демонструє деякі елементи, що можуть зустрічатися в документі WEB, такі як:
- таблиця;
- зображення;
- посилання.

Документ HTML має такий загальний вигляд:
<html> <head> <title>Заголовок</title> ... </head> <атрибути body> ... </body></html>
Весь документ обмежений тегами <html>…</html>. Він складається з двох частин:
- <head>…</head>: це невидима частина документа. Вона надає інформацію браузеру, який буде відображати документ. У ній часто зустрічається тег <title>…</title>, який визначає текст, що буде відображатися в рядку заголовка браузера. Також тут можуть бути інші теги, зокрема ті, що визначають ключові слова документа, які потім використовуються пошуковими системами. У цій частині також можуть міститися скрипти, найчастіше написані на JavaScript або VBScript, які будуть виконуватися браузером.
- <body атрибути>…</body>: це частина, яка відображатиметься браузером. Теги HTML, що містяться в цій частині, вказують браузеру «бажаний» візуальний вигляд документа. Кожен браузер інтерпретує ці теги по-своєму. Тому два браузери можуть по-різному відображати один і той самий веб-документ. Зазвичай це є однією з головних проблем веб-дизайнерів.
Код HTML нашого прикладу документа виглядає так:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Quelques balises HTML</title>
</head>
<body style="background-image: url(images/standard.jpg)">
<h1 style="text-align: left">Quelques balises HTML</h1>
<hr />
<table border="1">
<thead>
<tr>
<th>Colonne 1</th>
<th>Colonne 2</th>
<th>Colonne 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>cellule(1,1)</td>
<td style="text-align: center;">cellule(1,2)</td>
<td>cellule(1,3)</td>
</tr>
<tr>
<td>cellule(2,1)</td>
<td>cellule(2,2)</td>
<td>cellule(2,3</td>
</tr>
</tbody>
</table>
<br/><br/>
<table border="0">
<tr>
<td>Une image</td>
<td>
<img border="0" src="images/cerisier.jpg"/></td>
</tr>
<tr>
<td>Le site de Polytech'Angers</td>
<td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
</tr>
</table>
</body>
</html>
теги та приклади HTML | |
<title>Деякі теги HTML</title> (рядок 5) текст [Quelques balises HTML] з’явиться у рядку заголовка браузера, який відображатиме документ | |
<hr />: відображає горизонтальну лінію (рядок 10) | |
<атрибути таблиці>….</table>: для визначення таблиці (рядки 12, 32) <thead>…</thead>: для визначення заголовків стовпців (рядки 13, 19) <tbody>…</tbody>: для визначення вмісту таблиці (рядки 20, 31) <tr атрибути>…</tr>: для визначення рядка (рядки 21, 25) <td атрибути>…</td>: для визначення комірки (рядок 22) приклади: <table border="1">…</table>: атрибут border визначає товщину межі таблиці <td style="text-align: center;">комірка(1,2)</td> (рядок 23): визначає комірку, вміст якої буде комірка(1,2). Цей вміст буде вирівняно по горизонталі (text-align: center). | |
<img border="0" src="images/cerisier.jpg"/> (рядок 38): визначає зображення без рамки (border=0"), вихідний файл якого — [images/cerisier.jpg] на веб-сервері (src="images/cerisier.jpg"). Це посилання міститься у веб-документі, отриманому за допомогою URL http://localhost/php7/scripts-web/01/balises.html. Крім того, браузер завантажить файл URL http://localhost/php7/scripts-web/01/images/cerisier.jpg, щоб отримати зображення, на яке тут є посилання. | |
<a href="http://www.polytech-angers.fr/fr/index.html">тут</a> (рядок 42): робить текст ici посиланням на URL http://www.polytech-angers.fr/fr/index.html. | |
<body style="background-image: url(images/standard.jpg)"> (рядок 8): вказує, що зображення, яке має слугувати фоном сторінки, знаходиться за адресою URL [images/standard.jpg] на сервері WEB. У контексті нашого прикладу браузер надішле запит на URL http://localhost/php7/scripts-web/01/images/standard.jpg, щоб отримати це фонове зображення. |
З цього простого прикладу видно, що для побудови всього документа браузер повинен надіслати три запити до сервера:
- http://localhost/php7/scripts-web/01/images/balises.html, щоб отримати вихідний код документа HTML
- http://localhost/php7/scripts-web/01/images/cerisier.jpg — щоб отримати зображення cerisier.jpg
- http://localhost/php7/scripts-web/01/images/standard.jpg, щоб отримати фонове зображення standard.jpg
Про це свідчить обмін даними між клієнтом і сервером (F12 у браузері):

- у [3-5] видно три запити, зроблені браузером;
17.5. Як зробити статичну сторінку динамічною
Давайте покажемо, як можна зробити сторінку HTML [exemple-01.html] динамічною. Скопіюємо вміст

Ми скопіювали вміст [exemple-01.html] у файл [page-01.php]. Якщо запустити цей веб-скрипт [2], у браузері з’явиться таке:

- у [3] — запитуваний файл URL;
- у [4] — заголовок сторінки;
- [5] — вміст сторінки;
Якщо переглянути код, отриманий браузером, можна побачити таке:

- у [7] міститься код HTML, розміщений у скрипті [exemple-01.php]
Інтерпретатор PHP проінтерпретував скрипт [page-01.php] і згенерував той самий потік HTML, що й статична сторінка [exemple-01.html]. У скрипті [page-01.php] не було PHP, а був лише HTML. З цього можна зробити висновок: коли інтерпретатор PHP знаходить HTML у скрипті PHP, він не змінює його і надсилає клієнту без змін.
Тепер додамо кілька інструкцій PHP у скрипт [page-01.php], щоб інтерпретатор PHP мав що робити:
<!DOCTYPE html>
<html>
<head>
<title><?php print $page->title ?></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div><b><?php print $page->contents ?></b></div>
</body>
</html>
У рядках 4 і 9 ми розмістили код PHP для динамічного формування заголовка та вмісту сторінки. Тут ми виходимо з припущення, що змінна [$page] є об’єктом, який містить дані для відображення.
Якщо виконати цей новий код, у браузері з’явиться такий результат:

- у [1] — запитуваний URL;
- у [2] заголовок сторінки не вдалося відобразити, оскільки змінна [$page] не була визначена;
- у [3] те саме стосується вмісту;
Тепер напишемо такий веб-скрипт [exemple-02.php]:

Скрипт [exemple-02.php] матиме такий вигляд:
<?php
// визначаємо елементи сторінки, які потрібно відобразити
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// виводимо [page-01]
require_once "page-01.php";
- рядки 4–6: визначаємо об’єкт [$page];
- рядок 8: включається скрипт [page-01.php]. Код цього скрипта, у свою чергу, буде інтерпретований:
- змінна [$page] тепер визначена, і інтерпретатор PHP буде її використовувати;
- код HTML із [page-01.php] буде надіслано клієнту без змін;
- результати операцій PHP та [print] будуть включені до текстового потоку, що надсилається клієнту;
Тепер, якщо ми запустимо веб-скрипт [exemple-02.php], у браузері ми отримаємо таке:

Якщо переглянути текстовий вміст, отриманий браузером:

- коди PHP, які раніше були [2] та [3], були замінені результатами двох команд [print];
З цього прикладу слід винести два висновки:
- сторінки HTML, призначені для браузера, можна виділити у скрипти PHP, що містять лише цей код HTML та деякі динамічні частини, згенеровані кодом PHP. У цих сторінках має бути якомога менше PHP;
- уся логіка, що генерує динамічні дані, включені в сторінки HTML, має бути виокремлена в чисті скрипти PHP, що не містять жодного коду для оформлення сторінок (HTML, CSS, JavaScript тощо);
Це дозволяє розділити завдання:
- завдання створення веб-сторінок, що мають відображатися (HTML, CSS, Javascript…);
- завдання щодо логіки веб-додатку, який ми створюємо. Ця логіка може бути реалізована за допомогою трирівневої архітектури, точно так само, як ми це робили зі скриптами консолі;
Далі ми будемо створювати спеціальні веб-скрипти;
- вони надсилатимуть клієнту лише дані, без будь-якого оформлення (HTML, CSS, Javascript). Отже, це будуть скоріше сервери даних, а не веб-сторінки;
- клієнтами цих веб-скриптів будуть консольні скрипти, які будуть отримувати дані, надіслані сервером, і виконувати з ними певні дії;
17.6. Клієнтсько-серверний додаток для дати та часу
Тепер розглянемо таку конфігурацію:

Ми напишемо:
- веб-скрипт [1], який надсилає своєму клієнту поточну дату та час;
- консольний скрипт [2], який буде клієнтом веб-скрипта: він отримає дату та час, надіслані веб-скриптом, і відобразить їх у консолі;

- у [1] — веб-скрипт [date-time-server.php];
- у [2] — консольний скрипт [date-time-client], клієнтський скрипт веб-скрипта;
17.6.1. Серверний скрипт
Ми вже написали веб-скрипт, що генерує поточну дату та час, про що йдеться у розділі «Посилання». Це був такий скрипт [exemple-01.php]:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// time: кількість мілісекунд з 01.01.1970
// формат відображення дати та часу
// d: день у форматі 2 цифр
// m: місяць (2 цифри)
// y: рік у 2 цифри
// H: година 0,23
// i: хвилини
// s: секунди
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
Ми вже згадували, що будемо створювати сервери даних: необроблені дані без форматування HTML. Отже, серверний скрипт [date-time-server.php] матиме такий вигляд:
<?php
// задаємо заголовок HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// надсилаємо дату та час
// time: кількість мілісекунд з 01.01.1970
// формат відображення дати та часу
// d: день у 2 цифри
// m: місяць (2 цифри)
// y: рік у 2 цифри
// H: година 0,23
// i: хвилини
// s: секунди
print date("d/m/y H:i:s", time());
- рядок 4: ми встановлюємо заголовок HTTP [Content-Type], який повідомляє клієнту про характер документа, який він отримає. До цього моменту [Content-Type] був таким: [Content-Type: text/html; charset=UTF-8]. Тут ми вказуємо клієнту, що документ є текстом без форматування — HTML. Це не має значення для нашого клієнта-консолі, який не намагатиметься використовувати цей заголовок. Це важливіше для клієнтських браузерів, які використовують цей заголовок;
Запустимо цей серверний скрипт:

Якщо ми переглянемо у браузері відповідь сервера (F12), то в [5] бачимо заголовок HTTP, який встановив серверний скрипт, а в [8] — отриманий текстовий документ;

17.6.2. Клієнтський скрипт
У розділі «Посилання» ми розробили кілька клієнтів HTTP. Ми могли б використати їх для отримання текстового документа, надісланого серверним скриптом [date-time-server.php]. Але ми цього не робитимемо. Як і у випадку з протоколами SMTP та IMAP, ми будемо використовувати сторонню бібліотеку, а саме компонент [HttpClient] фреймворку Symfony [https://symfony.com/doc/master/components/http_client.html].
Як і у випадку з двома попередніми бібліотеками, для встановлення компонента [HttpClient] з Symfony використовуємо інструмент [Composer]. У вікні [Terminal] Laragon (див. параграф «посилання») вводимо таку команду:

- у [3] переконайтеся, що ви перебуваєте в папці [<laragon>/www/], де <laragon> — це папка встановлення Laragon;
- у [4] — команду [composer], яка встановлює бібліотеку [HttpClient] від Symfony;
- у [5] нічого не встановлюється, оскільки бібліотека [HttpClient] вже була встановлена на цьому комп’ютері;
- у [6-7] у [<laragon>/www/vendor/symfony] з’являються нові папки;
Замість [5] у вас має бути щось на зразок такого:
C:\myprograms\laragon-lite\www
? composer require symfony/http-client
Using version ^4.3 for symfony/http-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
- Installing symfony/polyfill-php73 (v1.11.0): Downloading (100%)
- Installing symfony/http-client-contracts (v1.1.1): Downloading (100%)
- Installing psr/log (1.1.0): Loading from cache
- Installing symfony/http-client (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files
Переконайтеся, що папка [<laragon>/www/vendor] входить до гілки [Include Path] вашого проєкту (див. параграф із посиланням):

Тепер ми можемо написати скрипт консолі [date-time-client.php]:

Консольний скрипт [date-time-client.php] буде обробляти наступні файли: jSON та [config-date-time-client.json]:
- рядок 2: URL серверного скрипта;
Клієнтський скрипт [date-time-client.php] матиме такий вигляд:
<?php
// клієнт служби дати/часу
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-date-time-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// надсилання запиту
$response = $httpClient->request('GET', $config['url']);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримано заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// отримуємо тіло відповіді
$content = $response->getContent();
// відображення
print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
// видається помилка
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
exit;
}
Коментарі
- рядок 10: як і в попередніх бібліотеках, ми завантажуємо файл [<laragon>/www/vendor/autoload.php];
- рядок 11: оголошуємо клас [HttpClient], який будемо використовувати;
- рядки 13–24: отримуємо конфігурацію скрипта зі словника [$config];
- рядок 27: створюємо об’єкт типу [HttpClient];
- рядок 31: запитуємо URL серверного скрипта за допомогою команди GET: [GET URL HTTTP/1.1]. Ця операція є асинхронною. Виконання продовжується у рядку 33, не чекаючи на отримання відповіді;
- рядок 33: запитується статус відповіді. Цей статус міститься в першому заголовку HTTP, надісланому сервером. Отже, якщо цей заголовок має значення [HTTP/1.1 200 OK], статус відповіді дорівнює 200. Ця операція є блокуючою: виконання продовжується лише після того, як клієнт отримав повну відповідь від сервера;
- рядок 37: запитуються заголовки HTTP відповіді;
- рядок 42: запитується документ, надісланий сервером: відомо, що цей документ є текстом.
- рядки 45–49: у разі помилки виводиться повідомлення про помилку;
Під час виконання клієнтського скрипта (для доступу до серверного скрипта необхідно запустити Laragon) на консолі з’являється такий результат:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Thu, 30 May 2019 14:42:03 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
content-length: 17
content-type: text/plain; charset=UTF-8
---Réponse du serveur : [30/05/19 14:42:03]
У рядку 8 ми успішно отримуємо поточну дату та час.
Може виникнути цікавість дізнатися, що саме клієнтський скрипт надіслав на сервер. Для цього ми використаємо наш загальний сервер TCP (див. параграф «посилання»):

- у [1] — папка з утилітами;
- у [2] сервер TCP запущено на порту 100;
- у [3] — очікування команди, введеної з клавіатури;
Ми змінюємо файл конфігурації скрипта [date-time-client.php]:
{
"url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}
Цього разу клієнт звертається до сервера [localhost] на порту 100. Отже, запит буде направлено до нашого загального сервера TCP. Коли ми запускаємо консольний скрипт [date-time-client.php], консоль загального сервера TCP змінюється наступним чином:

- на [3] — команда HTTP GET, сформована клієнтським скриптом;
- у [4] — підпис консольного скрипта;
- у [5] — відповідь сервера на клієнтський скрипт. Слід зауважити, що це не є дійсною відповіддю HTTP:
- тут повинні бути заголовки HTTP;
- потім порожній рядок;
- потім текстовий документ, надісланий клієнту;
- у [6] закривається зв’язок із клієнтським скриптом, щоб той визначив, що отримав повну відповідь;
У клієнтському скрипті відображається таке повідомлення в консолі:

- у [7] — те, що отримав клієнт Symfony;
17.6.3. Серверний скрипт — версія 2
За замовчуванням функції PHP для написання веб-скрипту не є об’єктно-орієнтованими. Тому на стороні сервера доводиться поєднувати класи та класичні функції PHP. Щоб забезпечити більш уніфікований стиль написання, ми будемо використовувати бібліотеку [HttpFoundation] фреймворку Symfony. Вона інкапсулювала всі класичні функції PHP для веб-сервісу в систему класів та інтерфейсів. Документація до бібліотеки доступна за посиланням URL [https://symfony.com/doc/current/components/http_foundation.html] (травень 2019 року).
Щоб встановити бібліотеку, у терміналі Laragon (див. параграф із посиланням) виконуємо такі дії:

- [2-3]: переконайтеся, що ви перебуваєте в папці [<laragon>/www];
- [4]: команда [composer], яка встановить бібліотеку [HttpFoundation];
- [5]: у цьому прикладі бібліотека вже була встановлена;
Під час першої інсталяції ви повинні побачити записи в консолі, схожі на такі:
C:\myprograms\laragon-lite\www
? composer require symfony/http-foundation
Using version ^4.3 for symfony/http-foundation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing symfony/mime (v4.3.0): Downloading (100%)
- Installing symfony/http-foundation (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files
Друга версія веб-сервера [date-time-server-2.php] виглядає так:
<?php
// використання бібліотек Symfony
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
// визначаємо заголовок Content-Type
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");
// визначаємо вміст відповіді
//
// надсилання дати та часу
// time: кількість мілісекунд з 01.01.1970
// формат відображення дати та часу
// d: день у форматі двох цифр
// m: місяць (2 цифри)
// y: рік у 2 цифри
// H: година 0,23
// i: хвилини
// s: секунди
$response->setContent(date("d/m/y H:i:s", time()));
// надсилається відповідь
$response->send();
Коментарі
- рядок 7: клас [Response] із бібліотеки [HttpFoundation] Symfony обробляє весь обсяг відповідей клієнтам веб-сервісу;
- рядок 10: створення екземпляра класу [Response];
- рядок 11: вказується, що відповідь має тип [text/plain];
- рядок 12: відповідь має вигляд тексту UTF-8;
- рядок 25: встановлюється документ відповіді, який запитував клієнт;
- рядок 28: відповідь надсилається клієнту;
17.6.4. Скрипт клієнта — версія 2
Скрипт клієнта не змінюється. Змінюється лише його файл конфігурації [config-date-time-client.json]:
Результати такі самі, як і у версії 1.
17.7. Сервер даних jSON
Відповідь веб-скрипта може складатися з декількох даних, які можна об’єднати в масиви та об’єкти. Тоді скрипт може надіслати ці різні елементи у складі рядка jSON, який клієнт розшифрує.

17.7.1. Серверний скрипт
Скрипт [json-server.php] використовує такий клас [Personne]:
<?php
namespace Modèles;
class Personne implements \JsonSerializable {
// атрибути
private $nom;
private $prénom;
private $âge;
// перетворення асоціативного масиву в об’єкт [Personne]
public function setFromArray(array $assoc): Personne {
// ініціалізуємо поточний об’єкт за допомогою асоціативного масиву
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// результат
return $this;
}
// гетери та сеттери
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function setNom($nom) {
$this->nom = $nom;
return $this;
}
public function setPrénom($prénom) {
$this->prénom = $prénom;
return $this;
}
public function getÂge() {
return $this->âge;
}
public function setÂge($âge) {
$this->âge = $âge;
return $this;
}
// toString
public function __toString(): string {
return "Personne [$this->prénom, $this->nom, $this->âge]";
}
// реалізує інтерфейс JsonSerializable
public function jsonSerialize(): array {
// повертає асоціативний масив, ключами якого є атрибути об’єкта
// цей масив потім можна буде закодувати у форматі jSON
return get_object_vars($this);
}
// перетворення jSON в об’єкт [Personne]
public static function jsonUnserialize(string $json): Personne {
// створюється особа на основі рядка jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
Коментарі
- рядок 5: клас реалізує інтерфейс PHP [JsonSerializable]. Це вимагає від нього реалізації методу [jsonSerialize] у рядках 55–59. Метод повинен повертати асоціативний масив, який має бути серіалізований у jSON. При використанні виразу [json_encode($personne)] функція [json_encode] перевіряє, чи клас [Personne] реалізує інтерфейс [JsonSerializable]. Якщо так, вираз перетворюється на [json_encode($personne→serialize())];
- рядки 12–19: клас не має конструктора, але має ініціалізатор. Тоді клас [Personne] можна інстанціювати за допомогою виразу [(new Personne())→setFromArray($array)]. Ініціалізаторів може бути кілька, тоді як конструктор може бути лише один. Ці ініціалізатори дозволяють використовувати різні способи інстанціювання типу [(new Personne())→initialiseuri(…)];
- рядки 62–65: статична функція [jsonUnserialize] дозволяє створити об’єкт [Personne] на основі його рядка jSON;
Скрипт [json-server.php] матиме такий вигляд:
<?php
// залежності
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
// встановлюємо заголовок Content-Type та використовувану бібліотеку символів
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// створюється об’єкт «Особа»
$personne = (new Personne())->setFromArray([
"nom" => "de la Hûche",
"prénom" => "jean-paul",
"âge" => 27]);
// асоціативний масив
$assoc = ["attr1" => "value1",
"attr2" => [
"prenom" => "Jean-Paul",
"nom" => "de la Hûche"
]
];
// вміст відповіді — це jSON
$response->setContent(json_encode([$personne, $assoc]));
// відправлення відповіді
$response->send();
Коментарі
- рядки 4–5: імпортується клас [Personne];
- рядок 11: вказується, що документ буде типу [application/json]. Отримавши цей заголовок, браузери відображатимуть форматування рядка jSON замість відображення простого тексту;
- рядок 12: рядок jSON міститиме символи UTF-8;
- рядки 15–18: створюється об’єкт [Personne];
- рядки 20–25: створюється дворівневий асоціативний масив;
- рядок 27: клієнту надсилається рядок jSON із масиву:
- елемент [$personne] буде серіалізовано в jSON за допомогою його методу [jsonSerialize];
- елемент [$assoc] буде серіалізовано в jSON;
Якщо виконати цей серверний скрипт (Laragon має бути запущений), у браузері з’явиться така відповідь:


Коментарі
- у [2] — відформатована відповідь jSON;
- у [4] — відповідь jSON у необробленому вигляді. Зверніть увагу на кодування символів з діакритичними знаками;
- у [6] — саме тип вмісту [application/json], надісланий сервером, змусив браузер виконати це форматування;
17.7.2. Клієнт

Клієнт [json-client.php] налаштовується за допомогою такого файлу: jSON [config-json-client.json]:
Скрипт [json-client.php] має такий вигляд:
<?php
// клієнт сервісу jSON
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-json-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// надсилання запиту
$response = $httpClient->request('GET', $config['url']);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримуємо заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// отримуємо тіло відповіді jSON
list($personne, $assoc) = json_decode($response->getContent(), true);
// створюється екземпляр особи на основі масиву її атрибутів
$personne = (new Personne())->setFromArray($personne);
// виводиться відповідь сервера
print "---Réponse du serveur\n";
print "$personne\n";
print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
// виводиться повідомлення про помилку
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядки 12–13: імпорт класу [Personne];
- рядок 30: створення клієнта HTTP;
- рядок 44: декодування рядка jSON, надісланого сервером. Відомо, що закодовано масив із двох елементів, що містить два асоціативні масиви;
- рядок 46: створюється об’єкт [Personne] для подальшого відображення в рядку 49;
- рядок 50: виводимо другий асоціативний масив. Команда [print] не вміє виводити масиви. Тому перетворюємо його на рядок jSON. Щоб правильно відобразити символи з діакритичними знаками, потрібно вказати другий параметр [JSON_UNESCAPED_UNICODE]. Ми бачили, що символи з діакритичними знаками дійсно кодуються у рядку jSON;
Виконання клієнтського скрипта дає такі результати:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 09:56:29 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 143
connection: close
content-type: application/json
---Réponse du serveur
Personne [jean-paul, de la Hûche, 27]
tableau={"attr1":"value1","attr2":{"prenom":"Jean-Paul","nom":"de la Hûche"}}
У рядках 11 і 12 символи з діакритичними знаками відтворено правильно.
17.8. Отримання змінних середовища веб-сервісу
Серверний скрипт виконується у веб-середовищі, яке йому може бути відоме. Це середовище зберігається у словнику $_SERVER, глобальній змінній PHP. Якщо ми використовуємо бібліотеку [HttpFoundation], це середовище буде знайдено в полі [Request→server], де [Request] — це запит HTTP, оброблений веб-скриптом.
17.8.1. Серверний скрипт
Ми пишемо серверний додаток, який надсилає своїм клієнтам своє середовище виконання.

Веб-скрипт [env-server.php] має такий вигляд:
<?php
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// отримуємо запит
$request = Request::createFromGlobals();
// формується відповідь
$response = new Response();
// вміст відповіді — JSON у кодуванні UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// встановлюємо вміст jSON відповіді
$response->setContent(json_encode($request->server->all()));
// відправляємо відповідь
$response->send();
- рядок 9: отримуємо об’єкт типу [Request], який інкапсулює всю доступну інформацію про запит HTTP, отриманий веб-скриптом, а також про його середовище виконання;
- рядки 13–14: клієнту надсилається текст у вигляді відкритого тексту з символами UTF-8;
- рядок 16: інформація, що надсилається клієнту, буде рядком символів, отриманим шляхом серіалізації jSON об’єкта [$request→server→all()]: [$request→server] представляє середовище виконання веб-скрипта. Це об’єкт типу [ServerBag], свого роду словник. [$request→server→all()] — це справжній словник, що містить вміст [ServerBag];
- рядок 18: надсилається інформація;
Якщо виконати цей скрипт із NetBeans, браузер відобразить таку сторінку:

- у [2] — різні ключі словника середовища;
- у файлі [3] — значення цих ключів;
17.8.2. Клієнтський скрипт

Клієнтський скрипт [env-client.php] налаштовується за допомогою наступного файлу jSON [config-env-client.json]:
Клієнтський скрипт [env-client.php] має такий вигляд:
<?php
// середовище серверного скрипта
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-env-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// надсилання запиту на сервер
$response = $httpClient->request('GET', $config['url']);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримано заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображається відповідь сервера
print "---Réponse du serveur\n";
$env = json_decode($response->getContent());
foreach ($env as $key => $value) {
print "[$key]=>$value\n";
}
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядок 42: десеріалізується відповідь jSON від сервера. Отримується асоціативний масив;
- рядки 43–45: виводимо всі значення цього асоціативного масиву;
Отримуємо такий результат у консолі:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 17:35:50 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 1505
connection: close
content-type: application/json
---Réponse du serveur
[HTTP_HOST]=>localhost
[HTTP_USER_AGENT]=>Symfony HttpClient/Curl
[HTTP_ACCEPT_ENCODING]=>deflate, gzip
[PATH]=>C:\Program Files (x86)\Mail Enable\BIN;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files (x86)\Mail Enable\BIN64;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\myprograms\Microsoft VS Code\bin
[SystemRoot]=>C:\windows
[COMSPEC]=>C:\windows\system32\cmd.exe
[PATHEXT]=>.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
[WINDIR]=>C:\windows
[SERVER_SIGNATURE]=>
[SERVER_SOFTWARE]=>Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
[SERVER_NAME]=>localhost
[SERVER_ADDR]=>::1
[SERVER_PORT]=>80
[REMOTE_ADDR]=>::1
[DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[REQUEST_SCHEME]=>http
[CONTEXT_PREFIX]=>
[CONTEXT_DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[SERVER_ADMIN]=>admin@example.com
[SCRIPT_FILENAME]=>C:/myprograms/laragon-lite/www/php7/scripts-web/04/env-server.php
[REMOTE_PORT]=>63744
[GATEWAY_INTERFACE]=>CGI/1.1
[SERVER_PROTOCOL]=>HTTP/1.1
[REQUEST_METHOD]=>GET
[QUERY_STRING]=>
[REQUEST_URI]=>/php7/scripts-web/04/env-server.php
[SCRIPT_NAME]=>/php7/scripts-web/04/env-server.php
[PHP_SELF]=>/php7/scripts-web/04/env-server.php
[REQUEST_TIME_FLOAT]=>1559496950.644
[REQUEST_TIME]=>1559496950
Ось значення деяких змінних (для Windows. У Linux вони будуть іншими):
значення xxx заголовка HTTP [Host: xxx], надісланого клієнтом | |
значення xxx заголовка HTTP [User_Agent: xxx], надісланого клієнтом | |
значення xxx заголовка HTTP [Accept-Encoding: xxx], надісланого клієнтом | |
шлях до виконуваних файлів на комп’ютері, на якому виконується серверний скрипт | |
шлях до командного інтерпретатора DOS | |
розширення виконуваних файлів | |
папка інсталяції Windows | |
підпис веб-сервера. Тут нічого немає. | |
тип веб-сервера | |
Інтернет-ім'я комп'ютера веб-сервера | |
порт, на якому працює веб-сервер | |
адреса IP комп'ютера, на якому працює веб-сервер, у даному випадку 127:0:0:1 | |
адреса IP клієнта. У цьому випадку клієнт знаходився на тому самому комп'ютері, що й сервер. | |
порт зв’язку клієнта | |
корінь дерева документів, що обслуговуються веб-сервером | |
протокол TCP запиту URL http://localhost/php7/… | |
електронна адреса адміністратора веб-сервера | |
повний шлях до серверного скрипта | |
порт, з якого клієнт надіслав запит | |
версія протоколу HTTP, що використовується веб-сервером | |
послідовність HTTP, яку використовує клієнт. Їх є чотири: GET, POST, PUT, DELETE | |
параметри, надіслані разом із запитом GET /url?параметри | |
URL, запитуваний клієнтом. Якщо браузер запитує URL http://machine[:port]/uri, отримаємо REQUEST_URI=uri | |
$_SERVER['SCRIPT_FILENAME']=$_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME'] |
17.9. Отримання сервером параметрів, надісланих клієнтом
17.9.1. Вступ
У протоколі HTTP клієнт має два способи передачі параметрів на сервер WEB:
- він запитує службу URL у формі
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
, де значення vali спочатку мають бути закодовані, щоб певні зарезервовані символи були замінені на їхні шістнадцяткові значення;
- він запитує URL сервісу у вигляді
POST url HTTP/1.0
потім серед заголовків HTTP, надісланих на сервер, розміщує такий заголовок:
Content-length=N
Наступні заголовки, надіслані клієнтом, закінчуються порожнім рядком. Після цього він може надіслати свої дані у форматі
val1¶m2=val2¶m3=val3…
, де значення vali, як і для методу GET, повинні бути попередньо закодовані. Кількість символів, що надсилаються на сервер, повинна дорівнювати N, де N — це значення, заявлене в заголовку
Content-length=N
Скрипт PHP веб-сервісу, який отримує попередні параметри parami, надіслані клієнтом, отримує їхні значення з масиву:
- $_GET["parami"] для запиту GET;
- $_POST["parami"] для команди POST;
це стосується основних функцій PHP. Якщо використовується бібліотека [HttpFoundation], ці параметри можна знайти в:
- [Request]->query->get(‘parami’) для команди GET;
- [Request]->request->get(‘parami’) для команди POST;
де [Request] представляє всю інформацію про запит, отриманий веб-скриптом;
17.9.2. Клієнт GET — версія 1

Клієнтські скрипти налаштовуються за допомогою такого файлу: jSON [config-parameters-client.json]:
- рядок 1: URL цільового веб-скрипта клієнтів GET;
- рядок 2: URL веб-скрипту, на який перенаправляє клієнт POST;
Клієнти GET надсилають на сервер три параметри [nom, prenom, age]. Клієнт [parameters-get-client.php] має такий вигляд:
<?php
// клієнт GET веб-сервера
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-parameters-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// підготовка параметрів
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// кодування інформації
$parameters = "prenom=" . urlencode($prenom) .
"&nom=" . urlencode($nom) .
"&age=$age”;
// надсилання запиту
$response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримано заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображення відповіді сервера
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// видається помилка
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядки 33–35: кодування параметрів, що надсилаються на сервер. Параметри [$prenom, $nom], які можуть містити символи UTF-8, кодуються за допомогою функції [urlencode]. Усі неалфавітно-цифрові символи (у значенні реляційних виразів) замінюються на %xx, де xx — шістнадцяткове значення символу. Пробіли замінюються на знак +;
- рядок 37: запитуване URL — це $URL?$parameters, де $parameters має вигляд nom=val1&prenom=val2&age=val3;
- рядок 48: клієнт просто відобразить відповідь сервера;
Може виникнути цікавість дізнатися, що отримує сервер під час надіслання запиту GET із заданими параметрами. Для цього ми запускаємо наш універсальний сервер [RawTcpServer] на порту 100 локальної машини з терміналу Laragon (див. параграф «посилання»):

Переконайтеся, що в [4] ви перебуваєте саме в папці з утилітами.
Ми редагуємо файл jSON [parameters-get-client.json], який налаштовує клієнти GET та POST:
{
"url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
- рядок 2: ми змінили порт веб-сервера. Отже, зв’язуватися буде з [RawTcpServer];
Запускаємо клієнт. У вікні [RawTcpServer] отримуємо таку інформацію:

- у [1] — надіслана клієнтом налаштована команда GET. Чітко видно кодування деяких символів;
17.9.3. Сервер GET / POST

Скрипт сервера [parameters-server.php] має такий вигляд:
<?php
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// отримується запит
$request = Request::createFromGlobals();
// отримуємо параметри запиту
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
// формується відповідь
$response = new Response();
// вміст відповіді — текст у форматі utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// вміст відповіді — масив, закодований у форматі jSON
$response->setContent(json_encode([
"method" => $request->getMethod(),
"uri" => $request->getRequestUri(),
"getParameters" => $getParameters,
"bodyParameters" => $bodyParameters
], JSON_UNESCAPED_UNICODE));
// відправлення відповіді
$response->send();
Коментарі
- рядок 9: створення об’єкта [Request] веб-скрипту. Цей об’єкт інкапсулює всю інформацію, яку веб-скрипт отримав від клієнта;
- рядок 11: об’єкт [Request→query] має тип [ParameterBag] і містить параметри можливої операції GET клієнта. Вираз [Request→query→get(«X»)] дозволяє отримати параметр з іменем X серед параметрів GET [nom=val1&prenom=val2&age=val3]. Вираз [Request→query→all()] дозволяє отримати словник параметрів операції GET;
- рядок 12: об’єкт [Request→request] має тип [ParameterBag] і об’єднує параметри, надіслані як документ від клієнта до сервера. Ці параметри також називають завантаженими, оскільки вони належать до документа, який клієнт надсилає на сервер. Вираз [Request→request→get(«X»)] дозволяє отримати параметр із назвою X із завантажених параметрів [nom=val1&prenom=val2&age=val3]. Вираз [Request→request→all()] дозволяє отримати словник завантажених параметрів;
- рядки 17–18: клієнту повідомляється, що йому буде надіслано jSON, закодований у форматі UTF-8;
- рядки 20–25: сервер повертає клієнту всі отримані параметри, а також тип операції [GET / POST / …], виконаної клієнтом, та запитуваний URI. Цей метод отримується за допомогою виразу [$request→getMethod()]. Документ, що надсилається клієнту, — це рядок jSON асоціативного масиву, деякі значення якого самі є асоціативними масивами. Параметр [JSON_UNESCAPED_UNICODE] вимагає, щоб символи Unicode (наприклад, символи з діакритичними знаками) надсилалися без змін, а не кодувалися;
- рядок 27: відповідь надсилається клієнту;
Виконання клієнтського скрипта дає такі результати:
- рядок 10:
- [method]: метод — GET;
- [uri]: у запиті GET, що надсилається, видно параметри запиту, закодовані за допомогою URL-кодування;
- [getParameters]: масив параметрів запиту GET;
- [bodyParameters]: масив завантажених параметрів: він порожній;
17.9.4. Клієнт GET — версія 2
У попередній версії клієнтського скрипта ми самостійно виконували URL-кодування параметрів, що надсилалися на сервер, з навчальною метою. Об’єкт [HttpClient] може виконувати цю роботу самостійно. Це наступний скрипт [parameters-get-client-2.php]:
<?php
// клієнт GET веб-сервера
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-parameters-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// підготовка параметрів
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// надсилання запиту на сервер
$response = $httpClient->request('GET', $config['url-get'],
["query" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримано заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображення відповіді сервера
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядки 33–37: додавання параметрів до запиту GET із рядка 32. Об’єкт [HttpClient] самостійно виконає кодування URL;
17.9.5. Клієнт POST
Клієнт HTTP надсилає веб-серверу таку послідовність тексту: заголовки HTTP, порожній рядок, документ. У попередньому клієнті ця послідовність була такою:
Документа не було. Існує інший спосіб передачі параметрів — метод, відомий як POST. У цьому випадку послідовність тексту, що надсилається на веб-сервер, є такою:
Цього разу параметри, які для клієнта GET були включені в заголовки HTTP, у клієнті POST є частиною документа, що надсилається після заголовків.
Скрипт клієнта POST [parameters-postclient.php] має такий вигляд:
<?php
// клієнт POST веб-сервера
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-parameters-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// підготовка параметрів
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// надсилання запиту на сервер
$response = $httpClient->request('POST', $config['url-post'],
["body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримано заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображення відповіді сервера
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
- рядок 32: тепер ми маємо запит HTTP типу POST;
- рядки 33–37: параметри POST називаються тілом (body) запиту POST — це документ, надісланий клієнтом на сервер. Тут надсилаються три параметри [nom, prenom, age];
- рядок 48: відображається відповідь jSON від сервера;
Результати виконання клієнтського скрипта такі:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 11:43:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 163
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
- рядок 10: метод — [Post], а параметри — типу [bodyParameters]. Параметрів типу [getParameters] немає, як показано в [uri];
Може виникнути цікавість дізнатися, що отримує сервер під час запиту POST. Для цього ми запускаємо наш універсальний сервер [RawTcpServer] на порту 100 локальної машини з терміналу Laragon (див. параграф «посилання»):

Переконайтеся, що в [4] ви перебуваєте саме в папці з утилітами.
Ми редагуємо файл jSON [config-parameters-client.json], який налаштовує клієнт POST:
{
"url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost:100/php7/scripts-web/05/parameters-server.php"
}
- рядок 3: ми змінили порт веб-сервера. Отже, зв’язуватися будемо з [RawTcpServer];
Запускаємо клієнт. У вікні [RawTcpServer] отримуємо таку інформацію:

- у [6] — команда POST;
- у [7]: заголовок HTTP [Content-Length] вказує кількість байтів документа, який клієнт надішле на сервер. Заголовок HTTP [Content-Type] вказує на тип цього документа. Тип [application/x-www-form-urlencoded] позначає текст, закодований за допомогою URL-кодування;
- у [8] — порожній рядок, що позначає кінець заголовків HTTP та початок документа розміром 44 байти. На знімку екрана не показано сам документ. Це URL-кодований рядок параметрів: [prenom=jean-paul&nom=de+la+h%C3%BBche&age=45]. Читач може переконатися, що він дійсно містить 44 символи;
17.9.6. Змішаний клієнт POST
У POST можна поєднувати параметри, закодовані в URL, та ті, що закодовані в документі, надісланому клієнтом після заголовків HTTP. Ось приклад [parameters-mixte-postclient.php]:
<?php
// клієнт POST веб-сервера
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-parameters-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// підготовка параметрів
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// надсилання запиту на сервер
$response = $httpClient->request('POST', $config['url-post'],
[
// параметри документа (body)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// параметри запиту URL
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримуємо заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображення відповіді сервера
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядок 32: запит POST;
- рядки 40–45: параметри, закодовані за допомогою URL-кодування, у URL;
- рядки 35–39: параметри, закодовані за допомогою URL-кодування, у тілі (body, document) запиту;
Під час виконання отримуємо такі результати у консолі:
- рядок 10: бачимо, що сервер зміг отримати обидва типи параметрів;
17.9.7. Змішаний клієнт GET
Спробуємо зробити те саме, що й раніше, із запитом GET. Скрипт [parameters-mixte-get-client.php] має такий вигляд:
<?php
// клієнт POST веб-сервера
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-parameters-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create();
try {
// підготовка параметрів
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// надсилання запиту на сервер
$response = $httpClient->request('GET', $config['url-post'],
[
// параметри документа (body)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// параметри запиту URL
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримуємо заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображення відповіді сервера
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядок 32: запит POST;
- рядки 40–45: параметри, закодовані за допомогою URL-кодування, у запиті URL;
- рядки 35–39: параметри, закодовані за допомогою URL-кодування, у тілі (body, document) запиту;
Під час виконання отримуємо такі результати у консолі:
- рядок 10: бачимо, що сервер не отримав параметрів, закодованих у форматі URL, у документі, надісланому клієнтом. Якщо подивитися на заголовки HTTP, надіслані клієнтом, то можна помітити, що він дійсно надіслав документ довжиною 44 символи, але сервер його не обробив;
Тож який метод обрати для надсилання інформації на сервер?
- метод [GET URL?param1=val1¶m2=val2&…] використовує налаштований URL, який може слугувати посиланням. Це його головна перевага: користувач може додавати такі посилання до своїх закладок;
- у інших додатках може бути небажано відображати в URL параметри, що надсилаються на сервер. Наприклад, з міркувань безпеки. Тоді слід використовувати метод [POST] і розміщувати URL-кодовані параметри в документі, що надсилається на сервер;
17.10. Управління веб-сесіями
У наведених вище прикладах «клієнт/сервер» ми мали такий алгоритм роботи:
- клієнт встановлює з’єднання з портом 80 сервера веб-служби;
- він надсилає текстову послідовність: заголовки HTTP, порожній рядок, [document];
- у відповідь сервер надсилає послідовність того самого типу;
- сервер закриває з'єднання з клієнтом;
- клієнт закриває з'єднання з сервером;
Якщо той самий клієнт незабаром надсилає новий запит до веб-сервера, між клієнтом і сервером встановлюється нове з’єднання. Сервер не може знати, чи клієнт, який підключається, вже звертався раніше, чи це перший запит. Між двома з’єднаннями сервер «забуває» свого клієнта. З цієї причини кажуть, що протокол HTTP є безстаномним протоколом. Проте серверу корисно запам’ятовувати своїх клієнтів. Так, якщо додаток є захищеним, клієнт надсилає серверу логін та пароль для автентифікації. Якщо сервер «забуває» свого клієнта між двома з’єднаннями, клієнт муситиме автентифікуватися під час кожного нового з’єднання, що є неприйнятним.
Щоб відстежувати клієнта, сервер діє наступним чином: під час першого запиту від клієнта він включає у свою відповідь ідентифікатор, який клієнт потім повинен надсилати йому з кожним новим запитом. Завдяки цьому ідентифікатору, який є унікальним для кожного клієнта, сервер може розпізнати клієнта. Тоді він може вести облік даних для цього клієнта у вигляді пам’яті, однозначно пов’язаної з ідентифікатором клієнта.
Технічно це відбувається так:
- у відповіді новому клієнту сервер включає заголовок HTTP Set-Cookie: MotClé=Ідентифікатор. Він робить це лише під час першого запиту;
- у своїх наступних запитах клієнт надсилатиме свій ідентифікатор через заголовок HTTP Cookie: MotClé=Ідентифікатор, щоб сервер міг його впізнати;
Можна запитати, як сервер дізнається, що має справу з новим клієнтом, а не з тим, хто вже відвідував сайт раніше. Про це свідчить наявність заголовка HTTP Cookie у заголовках HTTP клієнта. У нового клієнта цей заголовок відсутній.
Сукупність з’єднань певного клієнта називається сеансом.
17.10.1. Файл конфігурації [php.ini]
Щоб управління сесіями працювало коректно з PHP, необхідно переконатися, що він налаштований правильно. У Windows його файл конфігурації — php.ini. Залежно від середовища виконання (консоль, веб), файл конфігурації [php.ini] слід шукати в різних папках. Щоб їх визначити, скористайтеся таким скриптом:
У рядку 4 функція phpinfo надає інформацію про інтерпретатор PHP, який виконує скрипт. Зокрема, вона вказує шлях до використовуваного файлу конфігурації [php.ini].
Ми вже використовували цей скрипт у консольному середовищі (див. параграф «посилання»). У веб-середовищі отримуємо такий результат:

- у [1-2] — файл [php.ini], який налаштовує інтерпретатор веб-скриптів. У цьому файлі міститься розділ session:
- рядок 2: дані сеансу клієнта зберігаються у файлі;
- рядок 3: папка для збереження даних сесії. Якщо ця папка не існує, помилка не повідомляється, а управління сесіями не працює;
- рядки 4–6: вказують, що ідентифікатор сесії керується заголовками HTTP, Set-Cookie та Cookie;
- рядок 7: заголовок Set-Cookie матиме вигляд Set-Cookie: PHPSESSID=identifiant_de_session;
- рядок 8: сесія клієнта не запускається автоматично. Серверний скрипт повинен явно запросити її за допомогою команди session_start();
- рядок 9: сесійний файл cookie залишається дійсним, доки браузер клієнта не буде закрито;
- рядок 10: шлях, для якого має повертатися сесійний файл cookie. Якщо [session.cookie_path = /xxx], то щоразу, коли браузер запитує URL типу [/xxx/yyy/zzz], він повинен повертати цей файл cookie. Тут шлях [/] вказує, що файл cookie має повертатися для будь-якого запиту URL на сайті;
- рядок 13: деякі об’єкти сесії повинні бути серіалізовані, щоб їх можна було зберегти у файлі. Саме PHP забезпечує цю серіалізацію/десеріалізацію за допомогою функцій [serialize / unserialize];
- рядок 16: термін дії, після закінчення якого об’єкти сесії, збережені у файлі резервної копії, вважаються застарілими;
- рядок 19: тривалість сеансу. Після закінчення цього часу створюється новий сеанс, а об’єкти, збережені в попередньому сеансі, втрачаються;
17.10.2. Приклад 1
17.10.2.1. Сервер

Управління ідентифікатором сеансу є прозорим для веб-сервісу. Цей ідентифікатор управляється веб-сервером. Веб-сервіс отримує доступ до сеансу клієнта за допомогою команди session_start(). Відтепер веб-сервіс може читати/записувати дані в сесії клієнта за допомогою словника $_SESSION. Якщо використовується бібліотека [HttpFoundation], сесія доступна через вираз [Request→getSession].
Наступний код [session-server.php] демонструє управління трьома лічильниками в сесії. При кожному новому запиті веб-скрипт збільшує значення цих лічильників і зберігає їх у сесії, щоб їх можна було отримати під час наступного запиту.
<?php
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
//
// отримується запит
$request = Request::createFromGlobals();
// сесія
$session = new Session();
$session->start();
// отримано три лічильники в сесії
if ($session->has("N1")) {
// інкрементування лічильника N1
$session->set("N1", (int) $session->get("N1") + 1);
} else {
// лічильник N1 відсутній у сесії — його створюють
$session->set("N1", 0);
}
if ($session->has("N2")) {
// інкремент лічильника N2
$session->set("N2", (int) $session->get("N2") + 1);
} else {
// лічильник N2 не перебуває в сесії — його створюють
$session->set("N2", 10);
}
if ($session->has("N3")) {
// інкремент лічильника N3
$session->set("N3", (int) $session->get("N3") + 1);
} else {
// лічильник N3 не перебуває в сесії — його створюють
$session->set("N3", 100);
}
// складається відповідь
$response = new Response();
// вміст відповіді — текст у форматі UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// відповіддю буде jSON — масив, що містить три лічильники
$response->setContent(json_encode([
"N1" => $session->get("N1"),
"N2" => $session->get("N2"),
"N3" => $session->get("N3")]));
// відправлення відповіді
$response->send();
- рядок 10: об’єкт [$request] інкапсулює всю інформацію про запит, отриманий веб-скриптом;
- рядки 12–13: створюється сесія та активується. Об’єкт [Session] містить дані сесії, що відповідають сесійному файлу cookie, надісланому клієнтом. Якщо клієнт не надіслав такого файлу cookie, то в [Session] не зберігаються жодні дані. Веб-скрипт включить у свою першу відповідь заголовок HTTP [Set-Cookie : PHPSESSID=xxx]. У своїх наступних запитах клієнт надсилатиме заголовок HTTP [Cookie : PHPSESSID=xxx], щоб вказати сесію, вміст якої він хоче використовувати. Сесія — це пам’ять клієнта;
- рядок 15: перевіряємо, чи є в сесії ключ із назвою [N1]. Це буде ім’я нашого першого лічильника. Якщо його немає (рядок 20), ми присвоюємо йому значення 0 і додаємо до сесії. Якщо він є (рядок 23), ми:
- витягуємо його з сесії;
- збільшуємо його значення на 1;
- повертаємо його в сесію;
- рядки 22–35: робимо те саме для двох інших лічильників N2 та N3;
- рядки 36–40: готуємо відповідь типу [application/json];
- рядки 42–45: відповіддю буде рядок jSON із масиву, що містить три лічильники;
- рядок 48: відповідь надсилається клієнту;
У взаємодії «клієнт — сервер» управління сеансом клієнта на сервері залежить від обох учасників — клієнта та сервера:
- сервер відповідає за надсилання ідентифікатора своєму клієнту під час його першого запиту
- клієнт зобов’язаний надсилати цей ідентифікатор із кожним новим запитом. Якщо він цього не зробить, сервер вважатиме, що це новий клієнт, і згенерує новий ідентифікатор для нової сесії.
Результати
Як клієнт ми використовуємо веб-браузер. За замовчуванням (а точніше, згідно з налаштуваннями) він надсилає серверу ідентифікатори сеансу, які той йому надсилає. У міру надходження запитів браузер отримуватиме три лічильники, надіслані сервером, і бачитиме, як їхні значення збільшуються.

- У [2] — перший запит до веб-сервісу;
- у [4] четвертий запит показує, що лічильники дійсно збільшуються. Значення лічильників дійсно запам’ятовуються у міру надходження запитів;
Скористаємося режимом розробки, щоб переглянути заголовки HTTP, якими обмінюються сервер і клієнт. Ми закриваємо Firefox, щоб завершити поточну сесію з сервером, знову відкриваємо його та вмикаємо режим розробки (F12). Це призведе до видалення поточної сесії браузера, який, відповідно, розпочне нову. Ми надсилаємо запит до сервісу [session-server.php]:

У [5] ми бачимо ідентифікатор сесії, надісланий сервером у відповіді на перший запит клієнта. Він використовує заголовок HTTP Set-Cookie.
Здійснимо новий запит, оновивши (F5) сторінку у веб-браузері:

У наведеному вище прикладі можна помітити дві речі:
- у [11] веб-браузер повертає ідентифікатор сесії разом із заголовком HTTP Cookie.
- У випадку [12] у своїй відповіді веб-сервіс більше не включає цей ідентифікатор. Тепер саме клієнт відповідає за його надсилання у кожному своєму запиті.
17.10.2.2. Клієнт
Тепер ми пишемо клієнтський скрипт на основі попереднього серверного скрипта. У процесі управління сесією він повинен поводитися як веб-браузер:
- у відповіді сервера на свій перший запит він повинен знайти ідентифікатор сеансу, який сервер йому надсилає. Він знає, що знайде його в заголовку HTTP Set-Cookie.
- У кожному з наступних запитів він повинен надсилати серверу отриманий ідентифікатор. Він зробить це за допомогою заголовка HTTP Cookie.

Клієнт [session-client] налаштовується за допомогою такого файлу: jSON [config-session-client.json]:
Код клієнта [session-client] такий:
<?php
// управління сеансом
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-session-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створюємо клієнта HTTP
$httpClient = HttpClient::create();
try {
// буде виконано 10 запитів
for ($i = 0; $i < 10; $i++) {
// надсилаємо запит на сервер
if (!isset($sessionCookie)) {
// без сесії
$response = $httpClient->request('GET', $config['url']);
} else {
// із сесією
$response = $httpClient->request('GET', $config['url'],
["headers" => ["Cookie" => $sessionCookie]]);
}
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримуємо заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// отримуємо сесійний файл cookie, якщо він існує
if (isset($headers["set-cookie"])) {
// файл cookie сеансу?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// відображаємо відповідь jSON від сервера
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// виводиться повідомлення про помилку
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядок 27: створення клієнта HTTP;
- рядок 30: буде виконано 10 разів один і той самий запит до сервера [session-server.php];
- рядок 32: змінна [$sessionCookie] матиме значення заголовка HTTP [Set-Cookie], отриманого клієнтом;
- рядки 32–34: якщо ця змінна не існує, це означає, що сесія ще не розпочалася. Надсилається команда [GET] без заголовка [Cookie];
- рядки 35–38: в іншому випадку сесія розпочалася, і надсилається команда [GET] із заголовком [Cookie]. Значенням цього заголовка буде [$sessionCookie];
- рядок 50: якщо заголовок [Set-Cookie] входить до складу отриманих заголовків HTTP, то виконується пошук сесійного файлу cookie;
- рядок 52: веб-сервер може надсилати кілька заголовків [Set-Cookie]. Сесійний файл cookie — лише один із них. У нашому прикладі він має особливість: його форма — [PHPSESSID=xxx;];
- рядки 53–57: використовується регулярний вираз для пошуку сесійного файлу cookie;
- рядок 62: після надсилання 10 запитів виводиться остання відповідь jSON від сервера;
Результати
Виконання клієнтського скрипта призводить до відображення такого виводу в консолі NetBeans:
"C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\clients web\06\session-client.php"
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=1cerjgsgdlc35e1mkenvtltmh8; path=/
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
…………………………………………………………
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse du serveur : {"N1":9,"N2":19,"N3":109}
- рядок 8: у першій відповіді сервер надсилає ідентифікатор сесії. У наступних відповідях він його більше не надсилає;
- рядок 41: три лічильники [N1, N2, N3] були збільшені на 9. Під час запиту № 1 їх було обнулено;
Наступний приклад показує, що в сесії також можна зберігати значення масиву або об’єкта.
17.10.3. Приклад 2
17.10.3.1. Сервер

Ми помістимо об’єкт [Personne] у сесію. Визначення цього класу таке:
<?php
namespace Modèles;
class Personne implements \JsonSerializable {
// атрибути
private $nom;
private $prénom;
private $âge;
// перетворення асоціативного масиву в об’єкт [Personne]
public function setFromArray(array $assoc): Personne {
// поточний об’єкт ініціалізується асоціативним масивом
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// результат
return $this;
}
// гетери та сеттери
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function setNom($nom) {
$this->nom = $nom;
return $this;
}
public function setPrénom($prénom) {
$this->prénom = $prénom;
return $this;
}
public function getÂge() {
return $this->âge;
}
public function setÂge($âge) {
$this->âge = $âge;
return $this;
}
// toString
public function __toString(): string {
return "Personne [$this->prénom, $this->nom, $this->âge]";
}
// реалізує інтерфейс JsonSerializable
public function jsonSerialize(): array {
// повертає асоціативний масив, ключами якого є атрибути об’єкта
// цей масив потім можна буде закодувати у форматі jSON
return get_object_vars($this);
}
// перетворення jSON в об’єкт [Personne]
public static function jsonUnserialize(string $json): Personne {
// створюється особа на основі рядка jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
Скрипт сервера матиме такий вигляд:
<?php
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
//
// отримуємо поточний запит
$request = Request::createFromGlobals();
// сесія
$session = new Session();
$session->start();
// отримуємо різні дані з сесії
// масив
if ($session->has("tableau")) {
// масив знаходиться в сесії — збільшуємо всі його значення
$tableau = $session->get("tableau");
for ($i = 0; $i < count($tableau); $i++) {
$tableau[$i] += 1;
}
// масив повертається в сесію
$session->set("tableau", $tableau);
} else {
// масив відсутній у сесії — його створюють
$tableau = [0, 10, 100];
// поміщаємо його в сесію
$session->set("tableau", $tableau);
}
// словник
if ($session->has("assoc")) {
// [assoc] є в сесії — збільшуємо значення всіх його елементів
$assoc = $session->get("assoc");
foreach ($assoc as $key => $value) {
$assoc[$key] = $value + 1;
}
// додаємо $assoc до сесії
$session->set("assoc", $assoc);
} else {
// [assoc] відсутній у сесії — його створюють
$assoc = ["un" => 0, "deux" => 10, "trois" => 100];
// додаємо $assoc до сесії
$session->set("assoc", $assoc);
}
// об'єкт «Особа»
if ($session->has("personne")) {
// [personne] є в сесії — збільшуємо його вік
$personne = $session->get("personne");
$personne->setÂge($personne->getÂge() + 1);
} else {
// [personne] відсутній у сесії — його створюють
$personne = (new Personne())->setFromArray(
["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
// додаємо $personne до сесії
$session->set("personne", $personne);
}
// формується відповідь
$response = new Response();
// вміст відповіді — jSON у форматі utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
"tableau" => $tableau,
"assoc" => $assoc,
"personne" => $personne], JSON_UNESCAPED_UNICODE));
// відправлення відповіді
$response->send();
Коментарі
- рядки 16–17: отримуємо поточну сесію та активуємо її;
- рядки 21–34: обробляється масив [tableau], прив’язаний до сесії. З кожним новим запитом значення його елементів збільшуються на 1;
- рядки 36–49: обробляється асоціативний масив [assoc], занесений у сесію. З кожним новим запитом значення його елементів збільшуються на 1;
- рядки 51–61: обробляється об’єкт [Personne], збережений у сесії. При кожному новому запиті вік цієї особи збільшується на 1;
- рядки 62–73: надсилається відповідь jSON клієнту: рядок jSON з асоціативного масиву;
Запустимо цей скрипт із NetBeans. Перші два запити дають такі результати (F5 у браузері для другого):

- бачимо, що в [6-8] усі лічильники були збільшені;
17.10.3.2. Клієнт

Клієнт той самий, що й у прикладі 1 (абзац із посиланням). Змінюємо лише його файл конфігурації [config-session-client]:
{
"url": "http://localhost/php7/scripts-web/07/session-server.php"
}
Виконання дає такі результати:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=qbfrj8clr20mod3eriur71mao6; path=/
content-length: 119
connection: close
content-type: application/json
---Réponse avec statut : 200
………….……………………………………………………….
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 119
connection: close
content-type: application/json
---Réponse du serveur : {"tableau":[9,19,109],"assoc":{"un":9,"deux":19,"trois":109},"personne":{"nom":"Hûche","prénom":"Léonard","âge":9}}
- у рядку [22] видно, що всі лічильники були збільшені;
17.11. Authentification
Тепер розглянемо веб-сервіси, призначені лише для певних користувачів. У цьому випадку клієнт повинен пройти аутентифікацію у веб-сервісі, перш ніж отримати відповідь.
17.11.1. Клієнт

Код клієнта [auth-client.php] має такий вигляд:
<?php
// управління сесією
//
// обробка помилок
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// конфігурація клієнта
const CONFIG_FILE_NAME = "config-auth-client.json";
// отримуємо конфігурацію
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// створення клієнта HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
// "verify_peer" => false,
// «verify_host» => false
]);
try {
// надсилається запит на сервер
$response = $httpClient->request('GET', $config['url']);
// статус відповіді
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// отримуємо заголовки
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// відображається відповідь jSON від сервера
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// відображення помилки
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Коментарі
- рядки 27–31: до статичного методу [HttpClient::create] передано параметр — асоціативний масив;
- рядок 28: значенням ключа [auth_basic] є масив із двома елементами [user, password]. Саме за допомогою цих елементів клієнт буде проходити автентифікацію у веб-сервісі. Ключ [auth_basic] позначає тип автентифікації під назвою [Autorization Basic], що походить від імені заголовка HTTP, який буде надіслано клієнтом. Існують й інші типи автентифікації;
- за винятком цього коду клієнт ідентичний попереднім;
Щоб переглянути заголовки HTTP, надіслані клієнтом, ми підключимо його до сервера TCP загального типу [RawTcpServer], як ми це вже робили багато разів:

Запускаємо клієнт із такою конфігурацією:
{
"url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}
Сервер [RawTcpServer] отримує такі рядки:

- у [5] видно заголовок [Autorization : Basic XXX], надісланий клієнтом. Рядок XXX — це рядок [user:password], закодований у Base64;
Щоб переконатися в цьому, ви можете декодувати отриманий рядок на сайті [https://www.base64decode.org/]:

17.11.2. Сервер

Сервер [auth-server.php] виглядає так:
<?php
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// авторизовані користувачі
$users = ["admin" => "admin"];
//
// отримується поточний запит
$request = Request::createFromGlobals();
// аутентифікація
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// чи існує користувач?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// підготовка відповіді
$response = new Response();
// встановлення коду статусу відповіді
if (!$trouvé) {
// не знайдено — код 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
// знайдено — код 200
$response->setStatusCode(Response::HTTP_OK);
}
// відповідь не містить вмісту, лише заголовки HTTP
$response->send();
Коментарі
- рядок 9: авторизовані користувачі, у даному випадку лише один із логіном [admin] та паролем [admin];
- рядок 14: ідентифікатор користувача отримується із заголовка [PHP-AUTH-USER]. Це не заголовок, надісланий клієнтом, а заголовок, сформований сервером PHP;
- рядок 15: пароль користувача отримується з заголовка [PHP-AUTH-PW], який був сформований заголовком PHP;
- рядок 17: у списку авторизованих користувачів шукається користувач, який хоче увійти;
- рядки 23–24: якщо користувача не впізнано, клієнту надсилається
- рядок 23: код [401 Unauthorized];
- рядок 24: заголовок [WWW-Authenticate: Basic realm=”quelque chose”]. Більшість браузерів розпізнають цей заголовок і відкриють вікно автентифікації, в якому користувачеві буде запропоновано пройти автентифікацію. Заголовки HTTP повинні бути закодовані у форматі ISO 8859-1. Тексти NetBeans кодуються у форматі UTF-8. Функція [utf8_decode] забезпечує перетворення UTF-8 у ISO 8859-1. У цьому випадку вона не була потрібна, оскільки символи у рядку [PHP7 par l’exemple] є однаковими у UTF-8 та ISO 8859-1. Ця функція призначена лише для нагадування про кодування, яке використовується в заголовках HTTP;
- рядок 25: якщо користувача було розпізнано, клієнту надсилається код [200 OK];
Запросімо URL [auth-server.php] за допомогою браузера:

Бачимо, що браузер відображає вікно автентифікації. У [2] ми бачимо значення заголовка [WWW-Authenticate], надісланого сервером. Якщо подивитися на заголовки HTTP, отримані браузером, то можна побачити таке:
- рядок 1: код відповіді [401 Unauthorized];
- рядок 6: заголовок HTTP [WWW-Authenticate];
- рядок 7: тіло відповіді порожнє;
Якщо в полі [3-4] двічі ввести [admin], відповідь сервера буде такою:
- рядок 1: код відповіді 200 OK;
- рядок 6: тіло відповіді порожнє;
Якщо в [3-4] ввести неправильні облікові дані, браузер [Firefox], який використовується для тестування, безкінечно відображатиме вікно автентифікації, доки не будуть введені правильні облікові дані. Кожного разу відбувається обмін даними з сервером, і щоразу надходить одна й та сама відповідь, яка викликає вікно автентифікації в браузері.
Запустимо клієнт [auth-client.php] з неавторизованим користувачем. Відповідь сервера така:
---Réponse avec statut : 401
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
- У [1] клієнт дійсно отримав код 401;
- у [3] у клієнті було згенеровано виняток. Виняток був згенерований клієнтом Symfony [HttpClient]: він генерує виняток, коли код статусу відповіді HTTP вказує на те, що сталася помилка на стороні сервера, а клієнт намагається прочитати заголовки або вміст відповіді сервера. З повідомлення у рядку 3 видно, що сервер відповів кодом [HTTP/1.0 401 Unauthorized], щоб вказати, що користувач не був розпізнаний;
Тепер запустимо клієнт [auth-client.php] з авторизованим користувачем [‘admin’,’admin’]. Відповідь сервера буде такою:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:11:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :
- рядок 1: сервер відповів [HTTP/1. 200 OK];
- рядок 7: відповідь не містить даних (0 байт);
17.11.3. Захист з’єднання «клієнт–сервер»
Ми бачили, що для автентифікації на сервері клієнт надсилає заголовок:
Якщо цей рядок буде перехоплено шпигунською програмою, вона зможе легко відновити ідентифікатори [login, mot de passe], закодовані в базі 64 у рядку [YWRtaW46YWRtaW4=]. З цієї причини аутентифікація повинна відбуватися в захищеному з’єднанні між клієнтом і сервером. Захищені URL використовують протокол [HTTPS] замість протоколу HTTP. Протокол [HTTPS] — це протокол HTTP у рамках захищеного з’єднання «клієнт-сервер». Захищені URL мають вигляд [https://chemin_document].
Не всі веб-сервери підтримують URL у такому форматі. Їх потрібно змінити, щоб вони стали безпечними. Сервер Apache від Laragon є безпечним, але протокол HTTPS за замовчуванням неактивний. Його потрібно активувати в меню Laragon:

- на [4], потрібно увімкнути шифрування SSL сервера Apache;
Після цього сервер Apache автоматично перезапускається:

- на [1] з’являється зелений замок: це ознака того, що протокол HTTPS було активовано;
- у [2] з’являється новий порт служби — у даному випадку порт 443. Це порт служби безпечного протоколу HTTPS;
Тепер, коли ми маємо захищений сервер, змінимо файл конфігурації клієнта [config-auth-client.json] наступним чином:
{
"url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}
У [2] протокол змінився на [https], а порт — на [443].
Тепер запустимо клієнт [auth-client.php] з авторизованим користувачем [admin, admin]. Результати у консолі такі:
Клієнт Symfony [HttpClient] викликав виняток, оскільки сервер надіслав йому сертифікат довіри, який [HttpClient] не прийняв. Зв’язок SSL здійснюється за допомогою сертифікатів довіри, завірених офіційними органами. Під час увімкнення протоколу HTTPS на сервері Apache в Laragon було згенеровано самопідписаний сертифікат для сервера Apache. Самопідписаний сертифікат — це сертифікат, який не пройшов перевірку офіційним органом. Клієнт Symfony [HttpClient] відхилив цей самопідписаний сертифікат.
Можна налаштувати [HttpClient] так, щоб він не перевіряв дійсність сертифіката, надісланого сервером. Це робиться за допомогою параметрів у методі [HttpClient::create]:
// створюється клієнт HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
"verify_peer" => false
]);
У рядку 4 вказано, що сертифікат сервера не повинен перевірятися. Ми вже стикалися з цією проблемою у скрипті [http-02.php], про який йшлося у розділі «Посилання». Цей скрипт використовував бібліотеку [libcurl] для підключення до сайтів HTTP та HTTPS. Тоді для цієї бібліотеки використовувалася така конфігурація:
// Ініціалізація сеансу cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// сталася помилка
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// параметри curl
$options = [
// режим verbose
CURLOPT_VERBOSE => true,
// нове з'єднання — кеш відсутній
CURLOPT_FRESH_CONNECT => true,
// час очікування запиту (у секундах)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// не перевіряти дійсність сертифікатів SSL
CURLOPT_SSL_VERIFYPEER => false,
// дотримуватися перенаправлень
CURLOPT_FOLLOWLOCATION => true,
// отримати запитуваний документ у вигляді символьного рядка
CURLOPT_RETURNTRANSFER => true
];
// налаштування curl
curl_setopt_array($curl, $options);
У рядку 17 константа [CURLOPT_SSL_VERIFYPEER] визначає, чи перевіряти сертифікат, надісланий сервером. Клієнт [HttpClient] фактично є клієнтом [curl], коли розширення [curl] увімкнено в конфігурації PHP, як це відбувається в даному випадку. Отже, клас, інстанційований [HttpClient::create], є класом [CurlHttpClient]. Константи [curl] доступні в цьому класі, але під іншими іменами:
$curlopts = [
CURLOPT_URL => $url,
CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
CURLOPT_TCP_NODELAY => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
CURLOPT_COOKIEFILE => '', // Відстеження файлів cookie під час перенаправлень
CURLOPT_CONNECTTIMEOUT_MS => 1000 * $options['timeout'],
CURLOPT_PROXY => $options['proxy'],
CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
CURLOPT_CAINFO => $options['cafile'],
CURLOPT_CAPATH => $options['capath'],
CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
CURLOPT_SSLCERT => $options['local_cert'],
CURLOPT_SSLKEY => $options['local_pk'],
CURLOPT_KEYPASSWD => $options['passphrase'],
CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
];
Ми виділили жовтим кольором константи, які використовує [CurlHttpClient].
Якщо тепер запустити клієнт [auth-client] під користувачем [admin, admin], отримаємо такий результат:
---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:44:37 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :
Користувач було успішно розпізнано. Якщо запустити клієнт [auth-client] під ім’ям користувача, відмінним від [admin, admin], отримаємо такий результат:
Тепер ми знаємо, як пройти автентифікацію на захищеному сервері.