17. Usługi internetowe
Uwaga: termin „usługa internetowa” oznacza tutaj każdą aplikację internetową dostarczającą surowe dane wykorzystywane przez klienta – w poniższych przykładach będzie to skrypt konsolowy. Nie skupiamy się na konkretnej technologii, takiej jak na przykład REST (REpresentational State Transfer) czy SOAP (Simple Object Access Protocol), które dostarczają mniej lub bardziej surowe dane w ściśle zdefiniowanym formacie. REST dostarcza jSON, podczas gdy w przypadku SOAP jest to XML. Każda z tych technologii precyzyjnie określa sposób, w jaki klient powinien wysyłać zapytania do serwera, oraz formę, jaką powinna przybrać odpowiedź serwera. W ramach tego kursu będziemy znacznie bardziej elastyczni, jeśli chodzi o charakter zapytania klienta i odpowiedzi serwera. Jednak skrypty i narzędzia, z których korzystamy, są zbliżone do tych stosowanych w technologii REST.
17.1. Introduction
Ponieważ programy PHP mogą być uruchamiane przez serwer WEB, taki program staje się programem serwerowym, który może obsługiwać wielu klientów. Z punktu widzenia klienta wywołanie usługi internetowej sprowadza się do zażądania URL tej usługi. Klient może być napisany w dowolnym języku, w szczególności w PHP. W tym ostatnim przypadku wykorzystuje się wówczas funkcje sieciowe, które właśnie omówiliśmy. Musimy ponadto umieć „komunikować się” z usługą internetową, czyli rozumieć protokół http służący do komunikacji między serwerem WEB a jego klientami. Taki był cel akapitu link.
Klient internetowy opisany w akapicie „link” pozwolił nam poznać część protokołu HTTP.

W najprostszej wersji wymiana danych między klientem a serwerem przebiega następująco:
- klient nawiązuje połączenie z portem 80 serwera WWW;
- wysyła żądanie dotyczące dokumentu;
- serwer wysyła żądany dokument i zamyka połączenie;
- klient z kolei zamyka połączenie;
Dokument może mieć różny charakter: tekst w formacie HTML, obraz, film… Może to być dokument istniejący (dokument statyczny) lub dokument generowany na bieżąco przez skrypt (dokument dynamiczny). W tym ostatnim przypadku mówimy o programowaniu internetowym. Skrypt do dynamicznego generowania dokumentów może być napisany w różnych językach: PHP, Python, Perl, Java, Ruby, C#, VB.net…
W dalszej części będziemy używać skryptów PHP do dynamicznego generowania dokumentów tekstowych.

- w [1] klient nawiązuje połączenie z serwerem, żąda skryptu PHP, wysyła lub nie parametry do tego skryptu;
- w [2] serwer WWW uruchamia skrypt PHP za pomocą interpretera PHP. Skrypt generuje dokument, który jest wysyłany do klienta [3];
- serwer zamyka połączenie. Klient robi to samo;
Serwer WWW może obsługiwać wielu klientów jednocześnie.
W pakiecie oprogramowania [Laragon] serwerem WWW jest serwer Apache, serwer typu open source opracowany przez Apache Foundation (http://www.apache.org/). W poniższych aplikacjach należy uruchomić [Laragon]:

Spowoduje to uruchomienie serwera WWW Apache oraz skryptów SGBD i MySQL.
Skrypty wykonywane przez serwer WWW zostaną napisane za pomocą narzędzia NetBeans. Do tej pory napisaliśmy skrypty PHP wykonywane w środowisku konsoli:

Użytkownik korzysta z konsoli, aby zlecić wykonanie skryptu PHP i otrzymać jego wyniki.
W kolejnych aplikacjach typu klient/serwer:
- skrypt klienta jest uruchamiany w środowisku konsoli;
- skrypt serwera jest uruchamiany w środowisku internetowym;

Skrypt serwerowy PHP nie może znajdować się w dowolnym miejscu w systemie plików. Serwer WWW przeszukuje bowiem w lokalizacjach określonych w konfiguracji, szukając żądanych dokumentów statycznych i dynamicznych. Domyślna konfiguracja Laragon sprawia, że dokumenty są wyszukiwane w folderze <Laragon>/www, gdzie <Laragon> to folder instalacyjny Laragon. Jeśli więc klient sieciowy zażąda dokumentu D znajdującego się w ścieżce URL lub [http://localhost/D], serwer WWW dostarczy mu dokument D znajdujący się w ścieżce [<Laragon>/www/D].
W poniższych przykładach umieścimy skrypty serwerowe w folderze [www/php7/scripts-web]. Jeśli skrypt serwerowy nosi nazwę S.php, zostanie on zażądany od serwera WWW z adresem URL [http://localhost/php7/scripts-web/S.php]. Wówczas zostanie mu dostarczony dokument o nazwie [<Laragon>/www/php7/scripts-web/S.php].

- w [1], folder [<laragon>/www];
- w [2], folder [php7/scripts-web];
Aby utworzyć skrypty serwerowe w programie NetBeans, postępujemy w następujący sposób:

- z [1-2] tworzymy nowy projekt
- w [3-4] wybieramy kategorię [PHP] oraz projekt [PHP Application]

- w [5] – nazwa projektu;
- w [6] – folder projektu w systemie plików. Należy zauważyć, że znajduje się on w folderze [<laragon>/www], gdzie powinien być;
- w [7-8] należy zaakceptować proponowane wartości domyślne;
- w polu [9-10] zaakceptuj proponowane wartości domyślne. W polu [10] zwróć uwagę, że skrypty o nazwie URL, które umieścimy w tym projekcie, będą zaczynać się od ścieżki [http://localhost/php7/scripts-web/];

- w [11] dostępne są frameworki internetowe napisane w PHP. Frameworki te są niezbędne, gdy aplikacja internetowa nieco się rozrasta;
- w [12] można dodawać biblioteki PHP za pomocą narzędzia [Composer]. Wykorzystaliśmy to narzędzie dwukrotnie w oknie [Terminal] programu Laragon:
- w celu zainstalowania biblioteki [SwiftMailer], która umożliwia wysyłanie wiadomości e-mail;
- w celu zainstalowania biblioteki [php-mime-mail-parser], która umożliwia odczytywanie wiadomości e-mail;
- w [13], po zatwierdzeniu kreatora tworzenia projektu, pojawia się on w [13] w zakładce projektów;
17.2. Tworzenie strony statycznej
Uwaga: Aby kontynuować, należy uruchomić [Laragon].
Pokażemy, jak utworzyć stronę statyczną HTML (HyperText Markup Language) za pomocą programu NetBeans:

- w [1-5] tworzymy folder o nazwie [01];


- z [6-12] tworzymy plik o nazwie HTML [exemple-01.html];
Plik [exemple-01.html] jest generowany z następującymi wstępnie wypełnionymi danymi (maj 2019 r.):
<!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>
Zmodyfikujmy jego zawartość w następujący sposób:
<!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>
Zmieniliśmy tytuł strony (wiersz 4) oraz jej treść (wiersz 9).
Teraz wyświetlmy tę stronę HTML za pomocą serwera Apache w Laragonie:

- w [1-2], wyświetlamy stronę za pomocą serwera Apache w Laragonie;
- w [3], URL wyświetlonej strony;
- w pliku [4] – tytuł, który zmieniliśmy;
- w [5] – treść, którą zmodyfikowaliśmy;
Wyświetlana strona jest stroną statyczną: można ją ładować w przeglądarce tyle razy, ile się chce (F5), a wyświetlana jest zawsze ta sama treść.
Większość przeglądarek zapewnia dostęp do danych wymienianych między klientem a serwerem, opisanych w akapicie dotyczącym linków. W przeglądarce Firefox (maj 2019 r.) należy wykonać operację F12, aby uzyskać dostęp do tych danych:

Jak wskazano w [1], odświeżmy stronę (F5):

- w pliku [2] dokument załadowany przez przeglądarkę: zaznaczamy go;

- w [5], dokument do analizy jest zaznaczony;
- w [3-4] prosimy o wyświetlenie wymiany danych między klientem a serwerem;
- w [6] – te komunikaty;

- w [7] wybieramy zakładkę nagłówków;
- w [8], żądanie URL wysłane przez przeglądarkę;
- w [9] polecenie wysłane do serwera to [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1];
- w [10] nagłówki HTTP wysłane następnie przez przeglądarkę (klienta);
- w [11] nagłówki HTTP z odpowiedzi serwera;

- w pliku [12-14] – odpowiedź serwera wysłana po nagłówkach HTTP;
- w [14] widać, że przeglądarka klienta otrzymała stronę HTML, którą stworzyliśmy. Następnie zinterpretowała ten kod, aby wyświetlić następującą treść:

17.3. Tworzenie strony dynamicznej w formacie PHP
Teraz tworzymy stronę dynamiczną w formacie PHP:


- w [1-8] tworzymy stronę [exemple-01.php];
Plik [exemple-01.php] jest generowany z następującymi wstępnie wypełnionymi danymi (maj 2019 r.):
<!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
// wpisz tutaj swój kod
?>
</body>
</html>
Modyfikujemy powyższy kod w następujący sposób:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// czas: liczba milisekund od chwili obecnej do 01.01.1970
// format wyświetlania daty i godziny
// d: dzień w formacie dwucyfrowym
// m: miesiąc (2 cyfry)
// y: rok (2 cyfry)
// H: godzina 0,23
// I: minuty
// s: sekundy
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
Uwagi
- wiersz 5: zmieniliśmy tytuł strony;
- wiersz 17: zapisuje aktualną datę i godzinę;
Zasadniczo powyższy skrypt PHP wyświetla aktualny czas na konsoli. Jednak gdy jest on uruchamiany przez serwer WWW, strumień wyjściowy instrukcji [print], który zazwyczaj jest kierowany do konsoli wykonującej skrypt, jest tutaj przekierowywany do połączenia łączącego serwer z klientem. Tak więc w kontekście sieci WWW powyższy skrypt wysyła aktualną godzinę w postaci tekstu do klienta, w tym przypadku przeglądarki.
Uruchommy skrypt [exemple-01.php]:

- w [3], URL zażądany przez serwer WWW Apache;
- w [4] – tytuł strony, który zmieniliśmy;
- w [5] – treść wygenerowana przez instrukcję [print];
Mamy tu do czynienia ze stroną dynamiczną, ponieważ jeśli kilkakrotnie odświeżymy stronę w przeglądarce (F5), jej zawartość ulega zmianie (zmienia się godzina).
Przeglądarka otrzymała strumień HTML. Aby go sprawdzić, należy wyświetlić kod źródłowy strony w przeglądarce:

- aby wyświetlić menu [1], należy kliknąć prawym przyciskiem myszy na stronie w przeglądarce;
- w [2], URL strony [exemple-01.php], ale z prefiksem [view-source :] [3];
- w [4] – zawartość HTML, którą wyświetliła przeglądarka;
Należy zatem pamiętać, że skrypt PHP przeznaczony do wykonania przez serwer WWW musi generować strumień HTML.
Przyjrzyjmy się teraz (F12) nagłówkom HTTP wysyłanym przez serwer do przeglądarki klienta:

- w [3] pojawia się nagłówek HTTP, którego nie było podczas żądania strony statycznej. Nagłówek ten wskazuje, że odpowiedź serwera została wygenerowana przez skrypt PHP;
Zauważyliśmy, że odpowiedź serwera (w tym przypadku strumień HTML) mogła zostać wygenerowana przez skrypt PHP. Skrypt ten może również generować nagłówki HTTP oraz niemal wszystkie elementy odpowiedzi serwera.
17.4. Podstawy języka HTML
W niniejszym rozdziale nie będziemy się szczegółowo zajmować programowaniem w języku WEB. Aplikacja internetowa MVC została omówiona w akapicie [link]. Niniejszy rozdział skupia się raczej na usługach internetowych: stronach PHP, które za pośrednictwem serwera internetowego dostarczają dane do innych klientów PHP. Niemniej jednak uznaliśmy za przydatne przedstawienie czytelnikowi kilku podstawowych informacji na temat HTML.
Przeglądarka internetowa może wyświetlać różne dokumenty, z których najpopularniejszym jest dokument HTML (HyperText Markup Language). Jest to tekst sformatowany za pomocą znaczników o postaci <balise>texte</balise>. W ten sposób tekst <b>important</b> wyświetli tekst important pogrubioną czcionką. Istnieją również pojedyncze znaczniki, takie jak znacznik <hr/>, który wyświetla poziomą linię. Nie będziemy omawiać znaczników, które można znaleźć w tekście HTML. Istnieje wiele programów WYSIWYG, które pozwalają stworzyć stronę WEB bez pisania ani jednej linii kodu HTML. Narzędzia te automatycznie generują kod HTML na podstawie układu strony utworzonego za pomocą myszki i gotowych elementów sterujących. Można więc wstawić (za pomocą myszki) do strony tabelę, a następnie przejrzeć kod HTML wygenerowany przez oprogramowanie, aby dowiedzieć się, jakich tagów należy użyć do zdefiniowania tabeli na stronie WEB. To naprawdę nie jest skomplikowane. Ponadto znajomość języka HTML jest niezbędna, ponieważ dynamiczne aplikacje internetowe muszą samodzielnie generować kod HTML, który ma być wysyłany do klientów WEB. Kod ten jest generowany programowo i oczywiście trzeba wiedzieć, co należy wygenerować, aby klient otrzymał pożądaną stronę internetową.
Podsumowując, nie ma potrzeby znajomości całego języka HTML, aby rozpocząć programowanie stron internetowych. Jednak ta wiedza jest niezbędna i można ją zdobyć, korzystając z oprogramowania do tworzenia stron internetowych, takiego jak WYSIWYG, na przykład WEB, DreamWeaver oraz dziesiątek innych. Innym sposobem na poznanie subtelności języka HTML jest przeglądanie sieci i wyświetlanie kodu źródłowego stron, które zawierają interesujące i jeszcze nieznane Ci elementy.
Rozważmy poniższy przykład, który przedstawia kilka elementów, jakie można znaleźć w dokumencie WEB, takich jak:
- tabela;
- obraz;
- link.

Dokument HTML ma następującą ogólną postać:
<html> <head> <title>Tytuł</title> ... </head> <atrybuty body> ... </body></html>
Cały dokument jest ujęty w tagi <html>…</html>. Składa się on z dwóch części:
- <head>…</head>: jest to niewidoczna część dokumentu. Zawiera informacje dla przeglądarki, która wyświetli dokument. Często znajduje się w niej tag <title>…</title>, który określa tekst wyświetlany w pasku tytułu przeglądarki. Można tam również znaleźć inne tagi, w szczególności tagi definiujące słowa kluczowe dokumentu, wykorzystywane następnie przez wyszukiwarki. W tej części można również znaleźć skrypty, najczęściej napisane w języku JavaScript lub VBScript, które zostaną wykonane przez przeglądarkę.
- <body atrybuty>…</body>: jest to część, która zostanie wyświetlona przez przeglądarkę. Tagi zawarte w tej części wskazują przeglądarce „pożądany” wygląd dokumentu. Każda przeglądarka interpretuje te tagi na swój sposób. W związku z tym dwie przeglądarki mogą wyświetlać ten sam dokument internetowy w różny sposób. Jest to zazwyczaj jeden z problemów, z którymi borykają się projektanci stron internetowych.
Kod HTML naszego przykładowego dokumentu wygląda następująco:
<!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>
tagi i przykłady HTML | |
<title>Kilka znaczników HTML</title> (wiersz 5) tekst [Quelques balises HTML] pojawi się w pasku tytułu przeglądarki wyświetlającej dokument | |
<hr />: wyświetla poziomą linię (wiersz 10) | |
<atrybuty tabeli>….</table>: w celu zdefiniowania tabeli (wiersze 12, 32) <thead>…</thead>: w celu zdefiniowania nagłówków kolumn (wiersze 13, 19) <tbody>…</tbody>: w celu zdefiniowania zawartości tabeli (wiersze 20, 31) <tr atrybuty>…</tr>: w celu zdefiniowania wiersza (wiersze 21, 25) <td atrybuty>…</td>: w celu zdefiniowania komórki (wiersz 22) przykłady: <table border="1">…</table>: atrybut border określa grubość obramowania tabeli <td style="text-align: center;">komórka(1,2)</td> (wiersz 23): definiuje komórkę, której zawartością będzie komórka(1,2). Zawartość ta zostanie wyśrodkowana w poziomie (text-align: center). | |
<img border="0" src="images/cerisier.jpg"/> (wiersz 38): definiuje obraz bez obramowania (border=0"), którego plik źródłowy to [images/cerisier.jpg] na serwerze internetowym (src="images/cerisier.jpg"). Link ten znajduje się w dokumencie internetowym wygenerowanym za pomocą URL http://localhost/php7/scripts-web/01/balises.html. W związku z tym przeglądarka zażąda pliku URL http://localhost/php7/scripts-web/01/images/cerisier.jpg, aby pobrać obraz, do którego odwołuje się ten link. | |
<a href="http://www.polytech-angers.fr/fr/index.html">tutaj</a> (wiersz 42): sprawia, że tekst ici służy jako link do strony http://www.polytech-angers.fr/fr/index.html. | |
<body style="background-image: url(images/standard.jpg)"> (wiersz 8): wskazuje, że obraz, który ma służyć jako tło strony, znajduje się pod adresem URL [images/standard.jpg] na serwerze WEB. W kontekście naszego przykładu przeglądarka wyśle żądanie do serwera URL http://localhost/php7/scripts-web/01/images/standard.jpg, aby pobrać ten obraz tła. |
Na tym prostym przykładzie widać, że aby zbudować cały dokument, przeglądarka musi wysłać trzy żądania do serwera:
- http://localhost/php7/scripts-web/01/images/balises.html, aby pobrać kod źródłowy HTML dokumentu
- http://localhost/php7/scripts-web/01/images/cerisier.jpg, aby pobrać obraz cerisier.jpg
- http://localhost/php7/scripts-web/01/images/standard.jpg, aby uzyskać obraz tła standard.jpg
Widać to na podstawie komunikacji sieciowej między klientem a serwerem (F12 w przeglądarce):

- w pliku [3-5] widać trzy żądania wysłane przez przeglądarkę;
17.5. Dynamizacja strony statycznej
Pokażmy, jak możemy uczynić stronę HTML [exemple-01.html] dynamiczną. Skopiujmy zawartość

Skopiowaliśmy zawartość pliku [exemple-01.html] do pliku [page-01.php]. Jeśli uruchomimy skrypt internetowy [2], w przeglądarce pojawi się następujący wynik:

- w pliku [3] znajduje się żądany plik URL;
- w [4] – tytuł strony;
- w [5] – treść strony;
Jeśli wyświetlimy kod otrzymany przez przeglądarkę, zobaczymy następujące informacje:

- w [7] znajduje się kod HTML umieszczony w skrypcie [exemple-01.php]
Interpreter PHP przetworzył skrypt [page-01.php] i wygenerował ten sam strumień HTML, co statyczna strona [exemple-01.html]. W skrypcie [page-01.php] nie było PHP, a jedynie HTML. W ten sposób dowiadujemy się jednej rzeczy: gdy interpreter PHP znajdzie kod HTML w skrypcie PHP, nie modyfikuje go i wysyła go w niezmienionej postaci do klienta.
Teraz umieśćmy kilka instrukcji PHP w skrypcie [page-01.php], aby interpreter PHP miał coś do zrobienia:
<!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>
W wierszach 4 i 9 umieściliśmy kod PHP, aby dynamicznie wygenerować tytuł i treść strony. Zakładamy tutaj, że zmienna [$page] jest obiektem zawierającym dane do wyświetlenia.
Jeśli uruchomimy ten nowy kod, w przeglądarce otrzymamy następujący wynik:

- w [1], żądany URL;
- w [2] nie udało się wyświetlić tytułu strony, ponieważ zmienna [$page] nie została zdefiniowana;
- w [3] to samo dotyczy treści;
Teraz napiszmy następujący skrypt internetowy [exemple-02.php]:

Skrypt [exemple-02.php] będzie wyglądał następująco:
<?php
// określa się elementy strony do wyświetlenia
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// wyświetla się [page-01]
require_once "page-01.php";
- wiersze 4–6: definiujemy obiekt [$page];
- wiersz 8: dołączamy skrypt [page-01.php]. Kod tego skryptu zostanie z kolei zinterpretowany:
- zmienna [$page] jest teraz zdefiniowana, a interpreter PHP będzie z niej korzystał;
- kod HTML ze skryptu [page-01.php] zostanie wysłany w niezmienionej postaci do klienta;
- wyniki operacji PHP i [print] zostaną uwzględnione w strumieniu tekstowym wysyłanym do klienta;
Teraz, jeśli uruchomimy skrypt internetowy [exemple-02.php], w przeglądarce otrzymamy następujący wynik:

Jeśli wyświetlimy treść tekstową odebraną przez przeglądarkę:

- kody PHP, które wcześniej miały postać [2] i [3], zostały zastąpione wynikami dwóch poleceń [print];
Z tego przykładu można wyciągnąć dwa wnioski:
- strony HTML przeznaczone dla przeglądarki można wyodrębnić do skryptów PHP zawierających wyłącznie ten kod HTML oraz kilka dynamicznych fragmentów generowanych przez kod PHP. W stronach tych powinno znajdować się jak najmniej kodu PHP;
- cała logika generująca dane dynamiczne zawarte na stronach HTML musi być wyodrębniona do czystych skryptów PHP, niezawierających żadnego kodu odpowiedzialnego za prezentację stron (HTML, CSS, JavaScript…);
Pozwala to na rozdzielenie zadań:
- zadanie tworzenia stron internetowych do wyświetlenia (HTML, CSS, JavaScript…);
- zadanie związane z logiką tworzonej aplikacji internetowej. Logikę tę można zaimplementować w architekturze trójwarstwowej, dokładnie tak, jak zrobiliśmy to w przypadku skryptów konsolowych;
Następnie stworzymy specjalne skrypty internetowe;
- będą one wysyłać do klienta wyłącznie dane, bez żadnych elementów dekoracyjnych (HTML, CSS, JavaScript). Będą to zatem raczej serwery danych niż strony internetowe;
- klientami tych skryptów internetowych będą skrypty konsolowe, które będą pobierać dane wysyłane przez serwer i przetwarzać je;
17.6. Aplikacja klient-serwer do obsługi daty i godziny
Przechodzimy teraz do następującej konfiguracji:

Napiszemy:
- skrypt internetowy [1], który wysyła do swojego klienta aktualną datę i godzinę;
- skrypt konsolowy [2], który będzie pełnił rolę klienta skryptu internetowego: pobierze on datę i godzinę przesłane przez skrypt internetowy i wyświetli je na konsoli;

- w [1], skrypt internetowy [date-time-server.php];
- w [2] – skrypt konsolowy [date-time-client], będący klientem skryptu internetowego;
17.6.1. Skrypt serwerowy
W akapicie z linkiem napisaliśmy już skrypt internetowy generujący aktualną datę i godzinę. Był to następujący skrypt [exemple-01.php]:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// time: liczba milisekund od 01.01.1970
// format wyświetlania daty i godziny
// d: dzień w formacie dwucyfrowym
// m: miesiąc (2 cyfry)
// y: rok (2 cyfry)
// H: godzina 0,23
// i: minuty
// s: sekundy
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
Zapowiedzieliśmy, że zamierzamy stworzyć skrypty serwerowe dostarczające surowe dane bez formatowania – HTML. Skrypt serwerowy [date-time-server.php] będzie zatem wyglądał następująco:
<?php
// ustalamy nagłówek HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// wysyła się datę i godzinę
// czas: liczba milisekund od 01.01.1970
// format wyświetlania daty i godziny
// d: dzień w formacie dwucyfrowym
// m: miesiąc (2 cyfry)
// y: rok (2 cyfry)
// H: godzina 0,23
// i: minuty
// s: sekundy
print date("d/m/y H:i:s", time());
- wiersz 4: ustalamy nagłówek HTTP [Content-Type], który informuje klienta o charakterze dokumentu, który otrzyma. Do tej pory [Content-Type] brzmiało: [Content-Type: text/html; charset=UTF-8]. W tym miejscu informujemy klienta, że dokument jest tekstem bez formatowania – HTML. Nie ma to znaczenia dla naszego klienta konsolowego, który nie będzie próbował wykorzystać tego nagłówka. Jest to ważniejsze dla przeglądarek klienckich, które wykorzystują ten nagłówek;
Uruchommy ten skrypt serwerowy:

Jeśli w przeglądarce sprawdzimy odpowiedź serwera (F12), widzimy w [5] nagłówek HTTP, który ustawił skrypt serwerowy, oraz w [8] otrzymany dokument tekstowy;

17.6.2. Skrypt kliencki
W sekcji poświęconej linkom opracowaliśmy kilka klientów HTTP. Moglibyśmy ich użyć do pobrania dokumentu tekstowego wysłanego przez skrypt serwerowy [date-time-server.php]. Nie będziemy tego jednak robić. Podobnie jak w przypadku protokołów SMTP i IMAP, wykorzystamy bibliotekę zewnętrzną, a mianowicie komponent [HttpClient] z frameworka Symfony [https://symfony.com/doc/master/components/http_client.html].
Podobnie jak w przypadku dwóch poprzednich bibliotek, do zainstalowania komponentu [HttpClient] z Symfony używamy narzędzia [Composer]. W oknie [Terminal] programu Laragon (patrz akapit „link”) wpisujemy następujące polecenie:

- w [3] upewnij się, że znajdujesz się w folderze [<laragon>/www/], gdzie <laragon> to folder instalacyjny Laragona;
- w [4], polecenie [composer], które instaluje bibliotekę [HttpClient] z Symfony;
- w [5] nic nie jest instalowane, ponieważ biblioteka [HttpClient] została już wcześniej zainstalowana na tym komputerze;
- w przypadku [6-7] w [<laragon>/www/vendor/symfony] pojawiają się nowe foldery;
Zamiast [5] powinno pojawić się coś takiego:
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
Upewnij się, że folder [<laragon>/www/vendor] znajduje się w gałęzi [Include Path] Twojego projektu (zobacz akapit dotyczący linku):

Po wykonaniu tych czynności możemy napisać skrypt konsolowy [date-time-client.php]:

Skrypt konsolowy [date-time-client.php] będzie przetwarzał następujące pliki: jSON i [config-date-time-client.json]:
- wiersz 2: URL ze skryptu serwera;
Skrypt kliencki [date-time-client.php] będzie wyglądał następująco:
<?php
// klient usługi daty/godziny
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-date-time-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// wysyłanie zapytania
$response = $httpClient->request('GET', $config['url']);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// pobieramy treść odpowiedzi
$content = $response->getContent();
// wyświetlanie
print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się błąd
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
exit;
}
Komentarze
- wiersz 10: podobnie jak w przypadku poprzednich bibliotek, ładujemy plik [<laragon>/www/vendor/autoload.php];
- wiersz 11: deklarujemy klasę [HttpClient], której będziemy używać;
- wiersze 13–24: pobieramy konfigurację skryptu ze słownika [$config];
- wiersz 27: tworzymy obiekt typu [HttpClient];
- wiersz 31: wysyłamy żądanie do skryptu serwera o wartości URL za pomocą polecenia GET: [GET URL HTTTP/1.1]. Operacja ta jest asynchroniczna. Wykonanie jest kontynuowane w wierszu 33 bez oczekiwania na otrzymanie odpowiedzi;
- wiersz 33: wysyłane jest zapytanie o status odpowiedzi. Status ten znajduje się w pierwszym nagłówku HTTP zwróconym przez serwer. Jeśli więc nagłówek ten ma wartość [HTTP/1.1 200 OK], status odpowiedzi wynosi 200. Operacja ta jest blokująca: powrót następuje dopiero po otrzymaniu przez klienta całej odpowiedzi z serwera;
- wiersz 37: żądamy nagłówków HTTP z odpowiedzi;
- wiersz 42: pobierany jest dokument zwrócony przez serwer: wiadomo, że w tym przypadku jest to tekst;
- wiersze 45–49: w przypadku błędu wyświetlany jest komunikat o błędzie;
Po uruchomieniu skryptu klienckiego (aby skrypt serwerowy był dostępny, musi być uruchomiony Laragon) na konsoli pojawia się następujący wynik:
---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]
W wierszu 8 prawidłowo pobieramy aktualną datę i godzinę.
Można być ciekawym, co skrypt kliencki wysłał do serwera. W tym celu wykorzystamy nasz ogólny serwer TCP (patrz akapit „link”):

- w [1] znajduje się folder narzędzi;
- w [2] serwer TCP uruchomiony jest na porcie 100;
- w [3] – oczekiwanie na polecenie wpisane z klawiatury;
Modyfikujemy plik konfiguracyjny skryptu [date-time-client.php]:
{
"url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}
Tym razem klient łączy się z serwerem [localhost] na porcie 100. Zatem zapytanie zostanie skierowane do naszego serwera ogólnego TCP. Po uruchomieniu skryptu konsolowego [date-time-client.php] konsola serwera generycznego TCP zmienia się w następujący sposób:

- na [3], polecenie HTTP GET utworzone przez skrypt kliencki;
- w [4] – podpis skryptu konsoli;
- w [5] – odpowiedź serwera na skrypt kliencki. Należy zauważyć, że nie jest to prawidłowa odpowiedź HTTP:
- powinny znajdować się nagłówki o wartości HTTP;
- następnie pusty wiersz;
- następnie dokument tekstowy wysłany do klienta;
- w [6] zamykamy komunikację ze skryptem klienckim, aby ten wykrył, że otrzymał całą odpowiedź;
Po stronie skryptu klienckiego wyświetla się następujący komunikat konsoli:

- w [7] – to, co otrzymał klient Symfony;
17.6.3. Skrypt serwerowy – wersja 2
Zasadniczo funkcje PHP służące do pisania skryptów internetowych nie są zorientowane obiektowo. Po stronie serwera musimy zatem łączyć klasy z klasycznymi funkcjami PHP. Aby uzyskać bardziej spójny styl pisania kodu, wykorzystamy bibliotekę [HttpFoundation] frameworku Symfony. Zawiera ona wszystkie klasyczne funkcje PHP przeznaczone dla serwisu internetowego, zamknięte w systemie klas i interfejsów. Dokumentacja biblioteki jest dostępna pod adresem URL [https://symfony.com/doc/current/components/http_foundation.html] (maj 2019 r.).
Aby zainstalować bibliotekę, należy wykonać następujące czynności w terminalu Laragon (patrz link w akapicie):

- [2-3]: upewnij się, że znajdujesz się w folderze [<laragon>/www];
- [4]: polecenie [composer], które zainstaluje bibliotekę [HttpFoundation];
- [5]: w tym przykładzie biblioteka była już zainstalowana;
Podczas pierwszej instalacji powinny pojawić się logi konsoli wyglądające mniej więcej tak:
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
Druga wersja serwera WWW [date-time-server-2.php] wygląda następująco:
<?php
// korzystanie z bibliotek Symfony
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
// ustawiamy nagłówek Content-Type
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");
// ustawiamy treść odpowiedzi
//
// wysyłanie daty i godziny
// czas: liczba milisekund od 01.01.1970
// format wyświetlania daty i godziny
// d: dzień w formacie dwucyfrowym
// m: miesiąc (2 cyfry)
// y: rok (2 cyfry)
// H: godzina 0,23
// i: minuty
// s: sekundy
$response->setContent(date("d/m/y H:i:s", time()));
// wysyłamy odpowiedź
$response->send();
Komentarze
- wiersz 7: klasa [Response] z biblioteki [HttpFoundation] w Symfony obsługuje całą odpowiedź dla klientów serwisu internetowego;
- wiersz 10: utworzenie instancji klasy [Response];
- wiersz 11: wskazano, że odpowiedź jest typu [text/plain];
- wiersz 12: treść odpowiedzi to tekst UTF-8;
- wiersz 25: ustalanie dokumentu odpowiedzi zgodnie z żądaniem klienta;
- wiersz 28: wysyłamy odpowiedź do klienta;
17.6.4. Skrypt klienta – wersja 2
Skrypt klienta pozostaje bez zmian. Zmienia się jedynie jego plik konfiguracyjny [config-date-time-client.json]:
Wyniki są takie same jak w wersji 1.
17.7. Serwer danych jSON
Odpowiedź skryptu internetowego może składać się z wielu danych, które można zebrać w tabelach i obiektach. Skrypt może następnie wysłać te różne elementy w ramach ciągu znaków jSON, który klient zdekoduje.

17.7.1. Skrypt serwerowy
Skrypt [json-server.php] wykorzystuje następującą klasę [Personne]:
<?php
namespace Modèles;
class Personne implements \JsonSerializable {
// atrybuty
private $nom;
private $prénom;
private $âge;
// konwersja tablicy asocjacyjnej na obiekt [Personne]
public function setFromArray(array $assoc): Personne {
// inicjujemy bieżący obiekt za pomocą tablicy asocjacyjnej
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// wynik
return $this;
}
// metody pobierające i ustawiające
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]";
}
// implementuje interfejs JsonSerializable
public function jsonSerialize(): array {
// zwraca tablicę asocjacyjną, której kluczami są atrybuty obiektu
// tablica ta może następnie zostać zakodowana w formacie jSON
return get_object_vars($this);
}
// konwersja obiektu jSON na obiekt [Personne]
public static function jsonUnserialize(string $json): Personne {
// tworzymy osobę na podstawie ciągu znaków jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
Komentarze
- wiersz 5: klasa implementuje interfejs PHP [JsonSerializable]. Wymaga to od niej zaimplementowania metody [jsonSerialize] w wierszach 55–59. Metoda musi zwracać tablicę asocjacyjną, która powinna zostać zserializowana w formacie jSON. Gdy używa się wyrażenia [json_encode($personne)], funkcja [json_encode] sprawdza, czy klasa [Personne] implementuje interfejs [JsonSerializable]. Jeśli tak, wyrażenie zmienia się na [json_encode($personne→serialize())];
- wiersze 12–19: klasa nie posiada konstruktora, ale posiada inicjalizator. Klasę [Personne] można zatem instancjonować za pomocą wyrażenia [(new Personne())→setFromArray($array)]. Można mieć różne typy inicjalizatorów, podczas gdy konstruktor może występować tylko jeden. Inicjalizatory te umożliwiają różne sposoby instancjonowania typu [(new Personne())→initialiseuri(…)];
- wiersze 62–65: funkcja statyczna [jsonUnserialize] pozwala utworzyć obiekt [Personne] na podstawie jego ciągu znaków jSON;
Skrypt [json-server.php] będzie wyglądał następująco:
<?php
// zależności
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
// ustalamy nagłówek Content-Type oraz używaną bibliotekę znaków
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// tworzy się obiekt Osoba
$personne = (new Personne())->setFromArray([
"nom" => "de la Hûche",
"prénom" => "jean-paul",
"âge" => 27]);
// tablica asocjacyjna
$assoc = ["attr1" => "value1",
"attr2" => [
"prenom" => "Jean-Paul",
"nom" => "de la Hûche"
]
];
// treść odpowiedzi to jSON
$response->setContent(json_encode([$personne, $assoc]));
// wysyłanie odpowiedzi
$response->send();
Komentarze
- wiersze 4–5: importujemy klasę [Personne];
- wiersz 11: określa się, że dokument będzie typu [application/json]. Po otrzymaniu tego nagłówka przeglądarki wyświetlą sformatowaną wersję ciągu znaków jSON zamiast zwykłego tekstu;
- wiersz 12: ciąg znaków jSON będzie zawierał znaki UTF-8;
- wiersze 15–18: tworzy się obiekt o nazwie [Personne];
- wiersze 20–25: tworzy się dwupoziomową tablicę asocjacyjną;
- wiersz 27: wysyłamy do klienta ciąg znaków jSON z tablicy:
- element [$personne] zostanie zserializowany do postaci jSON za pomocą swojej metody [jsonSerialize];
- element [$assoc] zostanie natywnie zserializowany do postaci jSON;
Po uruchomieniu tego skryptu serwerowego (Laragon musi być uruchomiony) w przeglądarce otrzymujemy następującą odpowiedź:


Komentarze
- w [2] – sformatowana odpowiedź jSON;
- w przypadku [4] – sformatowana odpowiedź jSON; należy zwrócić uwagę na kodowanie znaków z akcentami;
- w [6] to typ zawartości [application/json] wysłany przez serwer, który spowodował, że przeglądarka zastosowała to formatowanie;
17.7.2. Klient

Klient [json-client.php] jest skonfigurowany przez następujący plik jSON [config-json-client.json]:
Skrypt [json-client.php] ma następującą treść:
<?php
// klient usługi jSON
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-json-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// wysyłanie zapytania
$response = $httpClient->request('GET', $config['url']);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// pobieramy treść odpowiedzi jSON
list($personne, $assoc) = json_decode($response->getContent(), true);
// tworzymy instancję osoby na podstawie tablicy jej atrybutów
$personne = (new Personne())->setFromArray($personne);
// wyświetla się odpowiedź serwera
print "---Réponse du serveur\n";
print "$personne\n";
print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się błąd
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersze 12–13: import klasy [Personne];
- wiersz 30: utworzenie klienta HTTP;
- wiersz 44: dekodowanie ciągu jSON przesłanego przez serwer. Wiemy, że zakodowano tablicę zawierającą dwa elementy, z których każdy jest tablicą asocjacyjną;
- wiersz 46: tworzymy obiekt [Personne], aby następnie wyświetlić go w wierszu 49;
- wiersz 50: wyświetla się drugą tablicę asocjacyjną. Instrukcja [print] nie potrafi wyświetlać tablic. Dlatego przekształca się ją w ciąg znaków jSON. Aby poprawnie wyświetlić znaki z akcentami, należy ustawić drugi parametr na [JSON_UNESCAPED_UNICODE]. Widzieliśmy, że znaki z akcentami są rzeczywiście zakodowane w ciągu znaków jSON;
Wykonanie skryptu po stronie klienta daje następujące wyniki:
---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"}}
W wierszach 11 i 12 znaki z akcentami zostały poprawnie odtworzone.
17.8. Pobieranie zmiennych środowiskowych serwisu internetowego
Skrypt serwerowy działa w środowisku internetowym, które może znać. Środowisko to jest przechowywane w słowniku $_SERVER, który jest zmienną globalną o nazwie PHP. Jeśli korzystamy z biblioteki [HttpFoundation], środowisko to zostanie znalezione w polu [Request→server], gdzie [Request] to żądanie HTTP przetwarzane przez skrypt internetowy.
17.8.1. Skrypt serwerowy
Tworzymy aplikację serwerową, która wysyła do swoich klientów swoje środowisko wykonawcze.

Skrypt internetowy [env-server.php] wygląda następująco:
<?php
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// pobieramy żądanie
$request = Request::createFromGlobals();
// tworzy się odpowiedź
$response = new Response();
// treść odpowiedzi jest w formacie JSON UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// ustalanie treści odpowiedzi jSON
$response->setContent(json_encode($request->server->all()));
// wysyłamy odpowiedź
$response->send();
- wiersz 9: pobieramy obiekt typu [Request], który zawiera wszystkie dostępne informacje dotyczące żądania HTTP otrzymanego przez skrypt internetowy, a także dotyczące jego środowiska wykonawczego;
- wiersze 13–14: wysyłamy do klienta tekst zwykły zawierający znaki UTF-8;
- wiersz 16: informacją wysyłaną do klienta będzie ciąg znaków uzyskany poprzez serializację jSON obiektu [$request→server→all()]: [$request→server] reprezentuje środowisko wykonawcze skryptu internetowego. Jest to obiekt typu [ServerBag], rodzaj słownika. [$request→server→all()] jest natomiast prawdziwym słownikiem, zawierającym zawartość obiektu [ServerBag];
- wiersz 18: wysyłamy informację;
Jeśli uruchomimy ten skrypt z poziomu NetBeans, przeglądarka wyświetli następującą stronę:

- w [2] – różne klucze słownika środowiska;
- w pliku [3] – wartości tych kluczy;
17.8.2. Skrypt kliencki

Skrypt kliencki [env-client.php] jest konfigurowany przez następujący plik jSON [config-env-client.json]:
Skrypt kliencki [env-client.php] ma następującą postać:
<?php
// środowisko skryptu serwerowego
//
// obsługa błędów
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-env-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// wysyłanie zapytania do serwera
$response = $httpClient->request('GET', $config['url']);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetla się odpowiedź serwera
print "---Réponse du serveur\n";
$env = json_decode($response->getContent());
foreach ($env as $key => $value) {
print "[$key]=>$value\n";
}
} catch (TypeError | RuntimeException $ex) {
// wyświetlanie błędu
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersz 42: deserializujemy odpowiedź jSON z serwera. Otrzymujemy tablicę asocjacyjną;
- wiersze 43–45: wyświetlane są wszystkie wartości tej tablicy asocjacyjnej;
Otrzymujemy następujący wynik na konsoli:
---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
Oto znaczenie niektórych zmiennych (dla systemu Windows. W systemie Linux byłyby one inne):
wartość xxx z nagłówka HTTP [Host: xxx] przesłanego przez klienta | |
wartość xxx nagłówka HTTP [User_Agent: xxx] wysłanego przez klienta | |
wartość xxx nagłówka HTTP [Accept-Encoding: xxx] wysłanego przez klienta | |
ścieżka do plików wykonywalnych na komputerze, na którym uruchomiony jest skrypt serwera | |
ścieżka do interpretera poleceń DOS | |
rozszerzenia plików wykonywalnych | |
katalog instalacyjny systemu Windows | |
podpis serwera WWW. Tutaj nic. | |
typ serwera WWW | |
nazwa internetowa komputera serwera WWW | |
port nasłuchowy serwera WWW | |
adres IP serwera WWW, w tym przypadku 127:0:0:1 | |
adres IP klienta. W tym przypadku klient znajdował się na tym samym komputerze co serwer. | |
port komunikacyjny klienta | |
katalog główny drzewa dokumentów obsługiwanych przez serwer WWW | |
protokół TCP żądania URL http://localhost/php7/… | |
adres e-mail administratora serwera WWW | |
pełna ścieżka do skryptu serwera | |
port, z którego klient wysłał żądanie | |
wersja protokołu HTTP używanego przez serwer WWW | |
polecenie HTTP używane przez klienta. Są cztery: GET, POST, PUT, DELETE | |
parametry wysyłane wraz z poleceniem GET /url?parametry | |
URL żądane przez klienta. Jeśli przeglądarka zażąda URL http://machine[:port]/uri, otrzymamy REQUEST_URI=uri | |
$_SERVER['SCRIPT_FILENAME']=$_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME'] |
17.9. Pobieranie przez serwer parametrów przesłanych przez klienta
17.9.1. Wprowadzenie
W protokole HTTP klient ma dwie metody przekazywania parametrów do serwera WEB:
- wysyła żądanie do serwisu URL w postaci
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
, gdzie wartości vali muszą zostać wcześniej zakodowane, tak aby niektóre znaki zarezerwowane zostały zastąpione ich wartościami szesnastkowymi;
- żąda kodu URL usługi w postaci
POST url HTTP/1.0
następnie wśród nagłówków HTTP wysyłanych do serwera umieszcza następujący nagłówek:
Content-length=N
Kolejne nagłówki wysyłane przez klienta kończą się pustym wierszem. Następnie może on wysłać swoje dane w postaci
val1¶m2=val2¶m3=val3…
gdzie wartości vali muszą, podobnie jak w przypadku metody GET, zostać wcześniej zakodowane. Liczba znaków wysyłanych do serwera musi wynosić N, gdzie N jest wartością zadeklarowaną w nagłówku
Content-length=N
Skrypt PHP serwisu internetowego, który pobiera poprzednie parametry parami wysłane przez klienta, pobiera ich wartości z tablicy:
- $_GET["parami"] dla polecenia GET;
- $_POST["parami"] dla polecenia POST;
dotyczy to podstawowych funkcji biblioteki PHP. W przypadku korzystania z biblioteki [HttpFoundation] parametry te znajdują się w:
- [Request]->query->get(‘parami’) dla polecenia GET;
- [Request]->request->get(‘parami’) dla polecenia POST;
gdzie [Request] oznacza całość informacji dotyczących żądania otrzymanego przez skrypt internetowy;
17.9.2. Klient GET – wersja 1

Skrypty klienckie są konfigurowane za pomocą następującego pliku jSON [config-parameters-client.json]:
- wiersz 1: URL docelowego skryptu internetowego klientów GET;
- wiersz 2: skrypt internetowy URL jest przeznaczony dla klienta POST;
Klienci o identyfikatorach GET wysyłają do serwera trzy parametry o identyfikatorach [nom, prenom, age]. Klient o identyfikatorze [parameters-get-client.php] jest następujący:
<?php
// klient serwera WWW GET
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-parameters-client.json";
// pobieramy konfigurację
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// przygotowuje się parametry
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// kodowanie informacji
$parameters = "prenom=" . urlencode($prenom) .
"&nom=" . urlencode($nom) .
"&age=$age”;
// wysyłamy żądanie
$response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi serwera
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się błąd
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersze 33–35: kodowanie parametrów wysyłanych do serwera. Parametry [$prenom, $nom], które mogą zawierać znaki UTF-8, są kodowane za pomocą funkcji [urlencode]. Wszystkie znaki niealfanumeryczne (w rozumieniu wyrażeń relacyjnych) są zastępowane przez %xx, gdzie xx to wartość szesnastkowa danego znaku. Spacje są natomiast zastępowane znakiem +;
- wiersz 37: żądana funkcja URL to $URL?$parameters, gdzie $parameters ma postać nom=val1&prenom=val2&age=val3;
- wiersz 48: klient po prostu wyświetli odpowiedź serwera;
Można być ciekawym, co otrzymuje serwer podczas wysłania skonfigurowanego żądania GET. W tym celu uruchamiamy nasz serwer generyczny [RawTcpServer] na porcie 100 lokalnej maszyny z terminala Laragon (patrz akapit „link”):

Sprawdź, czy w [4] znajdujesz się rzeczywiście w folderze narzędzi.
Modyfikujemy plik jSON [parameters-get-client.json], który konfiguruje klientów GET i POST:
{
"url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
- wiersz 2: zmieniliśmy port serwera WWW. Kontakt nawiązywany będzie zatem z plikiem [RawTcpServer];
Uruchamiamy klienta. W oknie [RawTcpServer] otrzymujemy następujące informacje:

- w [1] widoczne jest skonfigurowane polecenie GET wysłane przez klienta. Wyraźnie widać kodowanie niektórych znaków;
17.9.3. Serwer GET / POST

Skrypt serwera [parameters-server.php] wygląda następująco:
<?php
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// pobierane jest zapytanie
$request = Request::createFromGlobals();
// pobierane są parametry żądania
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
// tworzy się odpowiedź
$response = new Response();
// treść odpowiedzi to tekst w formacie UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// treść odpowiedzi – tablica zakodowana w formacie jSON
$response->setContent(json_encode([
"method" => $request->getMethod(),
"uri" => $request->getRequestUri(),
"getParameters" => $getParameters,
"bodyParameters" => $bodyParameters
], JSON_UNESCAPED_UNICODE));
// wysyłanie odpowiedzi
$response->send();
Komentarze
- wiersz 9: utworzenie obiektu [Request] skryptu internetowego. Obiekt ten zawiera wszystkie informacje, które skrypt internetowy otrzymał od klienta;
- wiersz 11: obiekt [Request→query] jest typu [ParameterBag] i gromadzi parametry ewentualnej operacji GET klienta. Wyrażenie [Request→query→get(«X»)] pozwala uzyskać parametr o nazwie X spośród parametrów GET i [nom=val1&prenom=val2&age=val3]. Wyrażenie [Request→query→all()] pozwala uzyskać słownik parametrów operacji GET;
- wiersz 12: obiekt [Request→request] jest typu [ParameterBag] i gromadzi parametry wysłane jako dokument z klienta do serwera. Mówi się również, że parametry te są przesyłane, ponieważ należą do dokumentu, który klient wysyła do serwera. Wyrażenie [Request→request→get(«X»)] pozwala uzyskać parametr o nazwie X spośród przesłanych parametrów [nom=val1&prenom=val2&age=val3]. Wyrażenie [Request→request→all()] pozwala uzyskać słownik przesłanych parametrów;
- wiersze 17–18: informuje się klienta, że zostanie mu wysłany jSON zakodowany w formacie UTF-8;
- wiersze 20–25: serwer zwraca klientowi wszystkie otrzymane parametry, a także typ operacji [GET / POST / …] wykonanej przez klienta oraz żądany URI. Metoda ta jest określona przez wyrażenie [$request→getMethod()]. Dokument wysyłany do klienta to ciąg jSON z tablicy asocjacyjnej, której niektóre wartości same w sobie są tablicami asocjacyjnymi. Parametr [JSON_UNESCAPED_UNICODE] wymaga, aby znaki Unicode (takie jak na przykład znaki z akcentami) były wysyłane w niezmienionej postaci, a nie kodowane;
- wiersz 27: odpowiedź jest wysyłana do klienta;
Wykonanie skryptu klienckiego daje następujące wyniki:
- wiersz 10:
- [method]: metodą jest GET;
- [uri]: widoczne są zakodowane w formacie URL parametry żądania GET w żądaniu URI;
- [getParameters]: tablica parametrów z GET;
- [bodyParameters]: tablica przesłanych parametrów: jest pusta;
17.9.4. Klient GET – wersja 2
W poprzedniej wersji skryptu klienckiego, w celach edukacyjnych, sami kodowaliśmy parametry wysyłane do serwera przy użyciu kodowania URL. Obiekt [HttpClient] potrafi wykonać tę pracę samodzielnie. Jest to następujący skrypt [parameters-get-client-2.php]:
<?php
// klient serwera WWW GET
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-parameters-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// przygotowuje się parametry
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// wysyłanie zapytania do serwera
$response = $httpClient->request('GET', $config['url-get'],
["query" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi serwera
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się błąd
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersze 33–37: dodanie parametrów do żądania GET z wiersza 32. Obiekt [HttpClient] sam zajmie się kodowaniem URL;
17.9.5. Klient POST
Klient HTTP wysyła do serwera WWW następującą sekwencję tekstową: nagłówki HTTP, pusty wiersz, dokument. W poprzednim kliencie sekwencja ta wyglądała następująco:
Nie było dokumentu. Istnieje inny sposób przekazywania parametrów, tzw. metoda POST. W tym przypadku sekwencja tekstowa wysyłana do serwera WWW wygląda następująco:
Tym razem parametry, które w kliencie GET były zawarte w nagłówkach HTTP, w kliencie POST stanowią część dokumentu wysyłanego po nagłówkach.
Skrypt klienta POST [parameters-postclient.php] wygląda następująco:
<?php
// klient POST serwera WWW
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-parameters-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// przygotowuje się parametry
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// wysyłanie zapytania do serwera
$response = $httpClient->request('POST', $config['url-post'],
["body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieranie nagłówków
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi serwera
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się błąd
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
- wiersz 32: mamy teraz żądanie HTTP typu POST;
- wiersze 33–37: parametry POST nazywane są treścią (body) żądania POST: jest to dokument wysłany przez klienta do serwera. W tym przypadku wysyłane są trzy parametry [nom, prenom, age];
- wiersz 48: wyświetlana jest odpowiedź serwera o numerze jSON;
Wyniki wykonania skryptu klienckiego są następujące:
---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"}}]
- wiersz 10: metoda to [Post], a parametry są typu [bodyParameters]. Nie ma parametrów typu [getParameters], jak pokazuje [uri];
Można być ciekawym, co otrzymuje serwer podczas żądania POST. W tym celu uruchamiamy nasz serwer generyczny [RawTcpServer] na porcie 100 lokalnej maszyny z terminala Laragon (patrz akapit „link”):

Sprawdź, czy w [4] znajdujesz się rzeczywiście w folderze narzędzi.
Modyfikujemy plik jSON [config-parameters-client.json], który konfiguruje klienta 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"
}
- wiersz 3: zmieniliśmy port serwera WWW. Kontakt nawiązywany będzie zatem z plikiem [RawTcpServer];
Uruchamiamy klienta. W oknie [RawTcpServer] otrzymujemy następujące informacje:

- w [6] polecenie POST;
- w [7]: nagłówek HTTP [Content-Length] podaje liczbę bajtów dokumentu, który klient wyśle na serwer. Nagłówek HTTP [Content-Type] określa rodzaj tego dokumentu. Typ [application/x-www-form-urlencoded] oznacza tekst zakodowany w formacie URL;
- w przypadku [8] – pusty wiersz, który sygnalizuje koniec nagłówków HTTP i początek dokumentu o długości 44 bajtów. To, czego nie widać na zrzucie ekranu, to sam dokument. Jest to zakodowany w formacie URL ciąg parametrów: [prenom=jean-paul&nom=de+la+h%C3%BBche&age=45]. Czytelnik może sprawdzić, czy rzeczywiście ma on 44 znaki;
17.9.6. Klient mieszany POST
W POST można połączyć parametry zakodowane w URL z tymi zakodowanymi w dokumencie wysłanym przez klienta po nagłówkach HTTP. Oto przykład [parameters-mixte-postclient.php]:
<?php
// klient POST serwera WWW
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-parameters-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// przygotowuje się parametry
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// wysyłanie zapytania do serwera
$response = $httpClient->request('POST', $config['url-post'],
[
// parametry dokumentu (treść)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// parametry zapytania URL
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieramy nagłówki
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi serwera
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetlanie błędu
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersz 32: żądanie POST;
- wiersze 40–45: parametry zakodowane w formacie URL w URL;
- wiersze 35–39: parametry zakodowane w formacie URL w treści (body, dokument) żądania;
Po uruchomieniu otrzymujemy następujące wyniki w konsoli:
- wiersz 10: widać, że serwer był w stanie pobrać oba rodzaje parametrów;
17.9.7. Klient mieszany GET
Próbujemy wykonać to samo, co poprzednio, używając żądania GET. Skrypt [parameters-mixte-get-client.php] wygląda następująco:
<?php
// klient POST serwera WWW
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-parameters-client.json";
// pobieranie konfiguracji
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create();
try {
// przygotowuje się parametry
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// wysyłanie zapytania do serwera
$response = $httpClient->request('GET', $config['url-post'],
[
// parametry dokumentu (treść)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// parametry zapytania URL
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieramy nagłówki
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi serwera
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetlanie błędu
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersz 32: żądanie POST;
- wiersze 40–45: parametry zakodowane w formacie URL w żądaniu URL;
- wiersze 35–39: parametry zakodowane w formacie URL w treści (body, dokument) żądania;
Po uruchomieniu otrzymujemy następujące wyniki w konsoli:
- wiersz 10: widać, że serwer nie otrzymał parametrów zakodowanych w formacie URL w dokumencie wysłanym przez klienta. Analizując nagłówki HTTP wysłane przez klienta, zauważamy, że rzeczywiście wysłał on dokument o długości 44 znaków, ale serwer go nie przetworzył;
Jaką metodę należy ostatecznie wybrać do wysyłania informacji do serwera?
- Metoda [GET URL?param1=val1¶m2=val2&…] wykorzystuje skonfigurowany link URL, który może służyć jako odnośnik. To jej główna zaleta: użytkownik może dodać takie linki do swoich zakładek;
- w innych aplikacjach może nie być pożądane wyświetlanie w URL parametrów wysyłanych do serwera. Na przykład ze względów bezpieczeństwa. W takim przypadku należy użyć metody [POST] i umieścić parametry zakodowane w formacie URL w dokumencie wysyłanym do serwera;
17.10. Zarządzanie sesjami internetowymi
W poprzednich przykładach klient/serwer mieliśmy następujący sposób działania:
- klient nawiązuje połączenie z portem 80 serwera;
- wysyła sekwencję tekstową: nagłówki HTTP, pusty wiersz, [document];
- w odpowiedzi serwer wysyła sekwencję tego samego typu;
- serwer zamyka połączenie z klientem;
- klient zamyka połączenie z serwerem;
Jeśli ten sam klient wkrótce potem wyśle nowe żądanie do serwera internetowego, tworzone jest nowe połączenie między klientem a serwerem. Serwer nie jest w stanie stwierdzić, czy łączący się klient już wcześniej korzystał z serwisu, czy też jest to pierwsze żądanie. Pomiędzy dwoma połączeniami serwer „zapomina” o swoim kliencie. Z tego powodu mówi się, że protokół HTTP jest protokołem bezstanowym. Jednakże przydatne jest, aby serwer zapamiętywał swoich klientów. Jeśli więc aplikacja jest zabezpieczona, klient wysyła do serwera nazwę użytkownika i hasło w celu uwierzytelnienia się. Jeśli serwer „zapomni” o swoim kliencie między dwoma połączeniami, klient będzie musiał uwierzytelniać się przy każdym nowym połączeniu, co jest nie do pomyślenia.
Aby śledzić klienta, serwer postępuje w następujący sposób: przy pierwszym żądaniu od klienta dołącza do odpowiedzi identyfikator, który klient musi następnie odsyłać przy każdym kolejnym żądaniu. Dzięki temu identyfikatorowi, różnemu dla każdego klienta, serwer może rozpoznać klienta. Może wówczas zarządzać pamięcią dla tego klienta w postaci pamięci jednoznacznie powiązanej z identyfikatorem klienta.
Z technicznego punktu widzenia wygląda to następująco:
- w odpowiedzi dla nowego klienta serwer dołącza nagłówek HTTP Set-Cookie: MotClé=Identyfikator. Czyni to wyłącznie przy pierwszym żądaniu;
- w kolejnych żądaniach klient będzie odsyłał swój identyfikator za pomocą nagłówka HTTP Cookie: MotClé=Identyfikator, aby serwer mógł go rozpoznać;
Można się zastanawiać, w jaki sposób serwer rozpoznaje, że ma do czynienia z nowym klientem, a nie z klientem, który już wcześniej odwiedził stronę. Wskazuje na to obecność nagłówka HTTP Cookie w nagłówkach HTTP klienta. W przypadku nowego klienta ten nagłówek nie występuje.
Zbiór wszystkich połączeń danego klienta nazywany jest sesją.
17.10.1. Plik konfiguracyjny [php.ini]
Aby zarządzanie sesjami działało poprawnie z PHP, należy sprawdzić, czy jest on prawidłowo skonfigurowany. W systemie Windows jego plik konfiguracyjny to php.ini. W zależności od środowiska uruchomieniowego (konsola, internet) plik konfiguracyjny [php.ini] należy szukać w różnych folderach. Aby je zidentyfikować, należy użyć następującego skryptu:
W wierszu 4 funkcja phpinfo podaje informacje o interpreterze PHP, który wykonuje skrypt. Podaje ona w szczególności ścieżkę do używanego pliku konfiguracyjnego [php.ini].
Skrypt ten wykorzystaliśmy już wcześniej w środowisku konsoli (patrz akapit „link”). W środowisku internetowym otrzymujemy następujący wynik:

- w [1-2], pliku [php.ini], który konfiguruje interpreter skryptów internetowych. W pliku tym znajduje się sekcja session:
- wiersz 2: dane sesji klienta są zapisywane w pliku;
- wiersz 3: folder, w którym zapisywane są dane sesji. Jeśli folder ten nie istnieje, nie zgłaszany jest żaden błąd, a zarządzanie sesjami nie działa;
- wiersze 4–6: wskazują, że identyfikator sesji jest zarządzany przez nagłówki HTTP, Set-Cookie oraz Cookie;
- wiersz 7: nagłówek Set-Cookie będzie miał postać Set-Cookie: PHPSESSID=identifiant_de_session;
- wiersz 8: sesja klienta nie jest uruchamiana automatycznie. Skrypt serwera musi ją wyraźnie zażądać za pomocą instrukcji session_start();
- wiersz 9: plik cookie sesji jest ważny do momentu zamknięcia przeglądarki klienta;
- wiersz 10: ścieżka, dla której plik cookie sesji ma być odsyłany. Jeśli [session.cookie_path = /xxx], to za każdym razem, gdy przeglądarka żąda URL typu [/xxx/yyy/zzz], musi odesłać plik cookie. W tym przypadku ścieżka [/] oznacza, że plik cookie musi być odsyłany dla każdego żądania typu URL z tej witryny;
- wiersz 13: niektóre obiekty sesji muszą zostać zserializowane, aby można je było zapisać w pliku. Funkcja PHP zapewnia tę serializację/deserializację za pomocą funkcji [serialize / unserialize];
- wiersz 16: czas trwania, po upływie którego obiekty sesji zapisane w pliku kopii zapasowej są uznawane za nieaktualne;
- wiersz 19: czas trwania sesji. Po upływie tego czasu tworzona jest nowa sesja, a obiekty zapisane w poprzedniej sesji zostają utracone;
17.10.2. Przykład 1
17.10.2.1. Serwer

Zarządzanie identyfikatorem sesji jest dla serwisu internetowego procesem przezroczystym. Identyfikator ten jest zarządzany przez serwer internetowy. Serwis internetowy uzyskuje dostęp do sesji klienta za pomocą instrukcji session_start(). Od tego momentu usługa internetowa może odczytywać i zapisywać dane w sesji klienta za pośrednictwem słownika $_SESSION. W przypadku korzystania z biblioteki [HttpFoundation] sesja jest dostępna za pośrednictwem wyrażenia [Request→getSession].
Poniższy kod [session-server.php] ilustruje zarządzanie trzema licznikami w sesji. Przy każdym nowym żądaniu skrypt internetowy zwiększa wartości tych liczników i zapisuje je w sesji, aby można je było odzyskać podczas następnego żądania.
<?php
// zależności
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;
//
// pobierane jest zapytanie
$request = Request::createFromGlobals();
// sesja
$session = new Session();
$session->start();
// pobieramy trzy liczniki z sesji
if ($session->has("N1")) {
// inkrementacja licznika N1
$session->set("N1", (int) $session->get("N1") + 1);
} else {
// licznik N1 nie znajduje się w sesji – tworzymy go
$session->set("N1", 0);
}
if ($session->has("N2")) {
// inkrementacja licznika N2
$session->set("N2", (int) $session->get("N2") + 1);
} else {
// licznik N2 nie jest aktywny – tworzymy go
$session->set("N2", 10);
}
if ($session->has("N3")) {
// inkrementacja licznika N3
$session->set("N3", (int) $session->get("N3") + 1);
} else {
// licznik N3 nie jest aktywny – tworzymy go
$session->set("N3", 100);
}
// tworzymy odpowiedź
$response = new Response();
// treść odpowiedzi to tekst w formacie UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// odpowiedzią będzie jSON – tabela zawierająca trzy liczniki
$response->setContent(json_encode([
"N1" => $session->get("N1"),
"N2" => $session->get("N2"),
"N3" => $session->get("N3")]));
// wysyłanie odpowiedzi
$response->send();
- wiersz 10: obiekt [$request] zawiera wszystkie informacje dotyczące żądania otrzymanego przez skrypt internetowy;
- wiersze 12–13: tworzona jest sesja i jest ona aktywowana. Obiekt [Session] zawiera dane sesji odpowiadające plikowi cookie sesji wysłanemu przez klienta. Jeśli klient nie wysłał takiego pliku cookie, wówczas w obiekcie [Session] nie są zapisane żadne dane. Skrypt internetowy dołączy do swojej pierwszej odpowiedzi nagłówek HTTP [Set-Cookie : PHPSESSID=xxx]. W kolejnych żądaniach klient wyśle nagłówek HTTP [Cookie : PHPSESSID=xxx], aby wskazać sesję, z której treści chce skorzystać. Sesja to pamięć klienta;
- wiersz 15: sprawdzamy, czy sesja zawiera klucz o nazwie [N1]. Będzie to nazwa naszego pierwszego licznika. Jeśli tak nie jest (wiersz 20), nadajemy mu wartość 0 i zapisujemy go w sesji. Jeśli tak jest (wiersz 23), to:
- pobieramy go z sesji;
- zwiększamy jego wartość o 1;
- umieszczamy go ponownie w sesji;
- wiersze 22–35: wykonujemy to samo dla dwóch pozostałych liczników N2 i N3;
- wiersze 36–40: przygotowujemy odpowiedź typu [application/json];
- wiersze 42–45: odpowiedzią będzie ciąg znaków jSON z tablicy zawierającej trzy liczniki;
- wiersz 48: wysyłamy odpowiedź do klienta;
W relacji klient–serwer zarządzanie sesją klienta na serwerze zależy od obu stron, czyli klienta i serwera:
- serwer ma za zadanie wysłać identyfikator do klienta przy jego pierwszym żądaniu
- klient ma obowiązek odesłać ten identyfikator przy każdym nowym żądaniu. Jeśli tego nie zrobi, serwer uzna, że jest to nowy klient i wygeneruje nowy identyfikator dla nowej sesji.
Wyniki
Jako klienta wykorzystujemy przeglądarkę internetową. Domyślnie (a właściwie zgodnie z konfiguracją) przeglądarka ta prawidłowo odsyła do serwera identyfikatory sesji, które ten jej przesyła. W miarę wysyłania kolejnych żądań przeglądarka będzie odbierać trzy liczniki wysyłane przez serwer i obserwować, jak ich wartości rosną.

- W przypadku [2] – pierwsze żądanie skierowane do serwisu internetowego;
- w [4] czwarte żądanie pokazuje, że liczniki są prawidłowo zwiększane. Wartości liczników są rzeczywiście zapamiętywane w miarę wysyłania kolejnych żądań;
Skorzystajmy z trybu programistycznego, aby zobaczyć nagłówki HTTP wymieniane między serwerem a klientem. Zamykamy przeglądarkę Firefox, aby zakończyć bieżącą sesję z serwerem, ponownie ją otwieramy i włączamy tryb programistyczny (F12). Spowoduje to usunięcie bieżącej sesji przeglądarki, która w związku z tym rozpocznie nową. Wysyłamy żądanie do serwisu [session-server.php]:

W [5] widoczny jest identyfikator sesji wysłany przez serwer w odpowiedzi na pierwsze żądanie klienta. Wykorzystuje on nagłówek HTTP Set-Cookie.
Wyślijmy nowe żądanie, odświeżając (F5) stronę w przeglądarce internetowej:

W powyższym przykładzie można zauważyć dwie rzeczy:
- w [11] przeglądarka internetowa zwraca identyfikator sesji wraz z nagłówkiem HTTP Cookie.
- W przypadku [12] serwis internetowy nie dołącza już tego identyfikatora w swojej odpowiedzi. Teraz to klient jest odpowiedzialny za wysyłanie go w każdym swoim żądaniu.
17.10.2.2. Klient
Teraz piszemy skrypt kliencki na podstawie poprzedniego skryptu serwerowego. W zakresie zarządzania sesją musi on zachowywać się tak samo jak przeglądarka internetowa:
- W odpowiedzi serwera na swoje pierwsze żądanie musi znaleźć identyfikator sesji, który serwer mu przesyła. Wie, że znajdzie go w nagłówku HTTP Set-Cookie.
- W każdym kolejnym żądaniu musi przesłać serwerowi otrzymany identyfikator. Zrobi to za pomocą nagłówka HTTP Cookie.

Klient [session-client] jest skonfigurowany za pomocą następującego pliku: jSON [config-session-client.json]:
Kod klienta [session-client] jest następujący:
<?php
// zarządzanie sesją
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-session-client.json";
// pobieramy konfigurację
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;
}
// tworzymy klienta HTTP
$httpClient = HttpClient::create();
try {
// wykonamy 10 zapytań
for ($i = 0; $i < 10; $i++) {
// wysyłamy żądanie do serwera
if (!isset($sessionCookie)) {
// bez sesji
$response = $httpClient->request('GET', $config['url']);
} else {
// z sesją
$response = $httpClient->request('GET', $config['url'],
["headers" => ["Cookie" => $sessionCookie]]);
}
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieramy nagłówki
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// pobieramy plik cookie sesji, jeśli istnieje
if (isset($headers["set-cookie"])) {
// plik cookie sesji?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// wyświetlanie odpowiedzi jSON z serwera
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetla się komunikat o błędzie
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Uwagi
- wiersz 27: utworzenie klienta HTTP;
- wiersz 30: 10-krotne wysłanie tego samego zapytania do serwera [session-server.php];
- wiersz 32: zmienna [$sessionCookie] przyjmie wartość nagłówka HTTP [Set-Cookie] otrzymanego przez klienta;
- wiersze 32–34: jeśli ta zmienna nie istnieje, oznacza to, że sesja jeszcze się nie rozpoczęła. Wysyłamy polecenie [GET] bez nagłówka [Cookie];
- wiersze 35–38: w przeciwnym razie sesja została uruchomiona i wysyłamy polecenie [GET] wraz z nagłówkiem [Cookie]. Wartością tego nagłówka będzie [$sessionCookie];
- wiersz 50: jeśli nagłówek [Set-Cookie] znajduje się wśród otrzymanych nagłówków HTTP, wówczas wyszukiwany jest plik cookie sesji;
- wiersz 52: serwer WWW może wysłać kilka nagłówków [Set-Cookie]. Plik cookie sesji jest tylko jednym z nich. W naszym przykładzie ma on specyficzną postać [PHPSESSID=xxx;];
- wiersze 53–57: używamy wyrażenia regularnego, aby znaleźć plik cookie sesji;
- wiersz 62: po wysłaniu 10 żądań wyświetlana jest ostatnia odpowiedź serwera o treści jSON;
Wyniki
Uruchomienie skryptu klienckiego powoduje wyświetlenie następującego komunikatu w konsoli 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}
- wiersz 8: w pierwszej odpowiedzi serwer wysyła identyfikator sesji. W kolejnych odpowiedziach już go nie wysyła;
- wiersz 41: trzy liczniki [N1, N2, N3] zostały prawidłowo zwiększone o 9. Podczas żądania nr 1 zostały one zresetowane do zera;
Poniższy przykład pokazuje, że w sesji można również zapisać wartości tablicy lub obiektu.
17.10.3. Przykład 2
17.10.3.1. Serwer

Umieścimy obiekt [Personne] w sesji. Definicja tej klasy jest następująca:
<?php
namespace Modèles;
class Personne implements \JsonSerializable {
// atrybuty
private $nom;
private $prénom;
private $âge;
// konwersja tablicy asocjacyjnej na obiekt [Personne]
public function setFromArray(array $assoc): Personne {
// inicjuje się bieżący obiekt za pomocą tablicy asocjacyjnej
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// wynik
return $this;
}
// metody pobierające i ustawiające
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]";
}
// implementuje interfejs JsonSerializable
public function jsonSerialize(): array {
// zwraca tablicę asocjacyjną, której kluczami są atrybuty obiektu
// tablica ta może następnie zostać zakodowana w formacie jSON
return get_object_vars($this);
}
// konwersja obiektu jSON na obiekt [Personne]
public static function jsonUnserialize(string $json): Personne {
// tworzymy osobę na podstawie ciągu znaków jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
Skrypt serwera będzie wyglądał następująco:
<?php
// zależności
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;
//
// pobieramy bieżące zapytanie
$request = Request::createFromGlobals();
// sesja
$session = new Session();
$session->start();
// pobieranie różnych danych z sesji
// tablica
if ($session->has("tableau")) {
// tabela znajduje się w sesji – zwiększamy wszystkie jej wartości
$tableau = $session->get("tableau");
for ($i = 0; $i < count($tableau); $i++) {
$tableau[$i] += 1;
}
// tablica jest ponownie umieszczana w sesji
$session->set("tableau", $tableau);
} else {
// tablica nie znajduje się w sesji – tworzymy ją
$tableau = [0, 10, 100];
// umieszczamy go w sesji
$session->set("tableau", $tableau);
}
// słownik
if ($session->has("assoc")) {
// [assoc] znajduje się w sesji – zwiększamy wartości wszystkich jego elementów
$assoc = $session->get("assoc");
foreach ($assoc as $key => $value) {
$assoc[$key] = $value + 1;
}
// umieszczamy $assoc w sesji
$session->set("assoc", $assoc);
} else {
// [assoc] nie znajduje się w sesji – tworzymy go
$assoc = ["un" => 0, "deux" => 10, "trois" => 100];
// dodajemy $assoc do sesji
$session->set("assoc", $assoc);
}
// obiekt Osoba
if ($session->has("personne")) {
// [personne] znajduje się w sesji – zwiększamy jego wiek
$personne = $session->get("personne");
$personne->setÂge($personne->getÂge() + 1);
} else {
// [personne] nie znajduje się w sesji – tworzymy go
$personne = (new Personne())->setFromArray(
["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
// dodajemy $personne do sesji
$session->set("personne", $personne);
}
// tworzymy odpowiedź
$response = new Response();
// treść odpowiedzi jest w formacie 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));
// wysyłanie odpowiedzi
$response->send();
Komentarze
- wiersze 16–17: pobieramy bieżącą sesję i aktywujemy ją;
- wiersze 21–34: obsługujemy tablicę [tableau] przypisaną do sesji. Przy każdym nowym żądaniu wartości jej elementów są zwiększane o 1;
- wiersze 36–49: obsługujemy tablicę asocjacyjną [assoc] umieszczoną w sesji. Przy każdym nowym żądaniu wartości jej elementów są zwiększane o 1;
- wiersze 51–61: obsługujemy obiekt [Personne] zapisany w sesji. Przy każdym nowym żądaniu wiek tej osoby jest zwiększany o 1;
- wiersze 62–73: wysyłamy odpowiedź jSON do klienta: ciąg znaków jSON z tablicy asocjacyjnej;
Uruchommy ten skrypt w programie NetBeans. Dwa pierwsze żądania dają następujące wyniki (F5 w przeglądarce dla drugiego):

- widać, że w [6-8] wszystkie liczniki zostały zwiększone;
17.10.3.2. Klient

Klient jest taki sam jak w przykładzie 1 (akapit dotyczący linku). Zmienia się jedynie jego plik konfiguracyjny [config-session-client]:
{
"url": "http://localhost/php7/scripts-web/07/session-server.php"
}
Wykonanie daje następujące wyniki:
---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}}
- w wierszu [22] widać, że wszystkie liczniki zostały zwiększone;
17.11. Authentification
Zajmiemy się teraz usługami internetowymi przeznaczonymi wyłącznie dla niektórych użytkowników. Klient musi wówczas uwierzytelnić się w usłudze internetowej, zanim otrzyma odpowiedź.
17.11.1. Klient

Kod klienta [auth-client.php] wygląda następująco:
<?php
// zarządzanie sesją
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// konfiguracja klienta
const CONFIG_FILE_NAME = "config-auth-client.json";
// pobieramy konfigurację
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;
}
// tworzy się klienta HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
// „verify_peer” => false,
// „verify_host” => false
]);
try {
// wysyłamy zapytanie do serwera
$response = $httpClient->request('GET', $config['url']);
// status odpowiedzi
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// pobieramy nagłówki
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// wyświetlanie odpowiedzi jSON z serwera
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// wyświetlanie błędu
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersze 27–31: do metody statycznej [HttpClient::create] przekazano parametr w postaci tablicy asocjacyjnej;
- wiersz 28: klucz [auth_basic] ma za wartość tablicę zawierającą dwa elementy [user, password]. To właśnie za pomocą tych elementów klient uwierzytelni się w serwisie internetowym. Klucz [auth_basic] określa typ uwierzytelniania o nazwie [Autorization Basic], pochodzącej od nazwy nagłówka HTTP, który wygeneruje klient. Istnieją inne typy uwierzytelniania;
- poza tym kodem klient jest identyczny jak poprzednie;
Aby zobaczyć nagłówki HTTP wysyłane przez klienta, połączymy go z serwerem TCP o nazwie ogólnej [RawTcpServer], tak jak robiliśmy to już wielokrotnie:

Uruchamiamy klienta z następującą konfiguracją:
{
"url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}
Serwer [RawTcpServer] otrzymuje wówczas następujące wiersze:

- w [5] widać nagłówek [Autorization : Basic XXX] wysłany przez klienta. Ciąg XXX to ciąg [user:password] zakodowany w Base64;
Aby się o tym przekonać, można zdekodować otrzymany ciąg znaków na stronie [https://www.base64decode.org/]:

17.11.2. Serwer

Serwer [auth-server.php] wygląda następująco:
<?php
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// uprawnieni użytkownicy
$users = ["admin" => "admin"];
//
// pobierane jest bieżące żądanie
$request = Request::createFromGlobals();
// uwierzytelnianie
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// czy użytkownik istnieje?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// przygotowanie odpowiedzi
$response = new Response();
// ustalanie kodu statusu odpowiedzi
if (!$trouvé) {
// nie znaleziono – kod 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
// znaleziono – kod 200
$response->setStatusCode(Response::HTTP_OK);
}
// odpowiedź nie zawiera treści, tylko nagłówki HTTP
$response->send();
Uwagi
- wiersz 9: uprawnieni użytkownicy, w tym przypadku tylko jeden o nazwie użytkownika [admin] i haśle [admin];
- wiersz 14: identyfikator użytkownika jest pobierany z nagłówka [PHP-AUTH-USER]. Nie jest to nagłówek wysłany przez klienta, lecz nagłówek utworzony przez serwer PHP;
- wiersz 15: hasło użytkownika jest pobierane z nagłówka [PHP-AUTH-PW], nagłówka utworzonego przez PHP;
- wiersz 17: na liście uprawnionych użytkowników szuka się użytkownika, który chce się zalogować;
- wiersze 23–24: jeśli użytkownik nie został rozpoznany, wysyła się do klienta
- wiersz 23: kod [401 Unauthorized];
- wiersz 24: nagłówek [WWW-Authenticate: Basic realm=”quelque chose”]. Większość przeglądarek rozpoznaje ten nagłówek i wyświetla okno uwierzytelniania, w którym użytkownik jest proszony o uwierzytelnienie się. Nagłówki HTTP muszą być zakodowane w formacie ISO 8859-1. Teksty NetBeans są zakodowane w formacie UTF-8. Funkcja [utf8_decode] zapewnia konwersję z UTF-8 do ISO 8859-1. W tym przypadku nie była ona konieczna, ponieważ znaki w ciągu [PHP7 par l’exemple] są takie same w UTF-8 i ISO 8859-1. Funkcja ta służy jedynie jako przypomnienie o kodowaniu używanym w nagłówkach HTTP;
- wiersz 25: jeśli użytkownik został rozpoznany, wysyłamy do klienta kod [200 OK];
Wprowadźmy URL [auth-server.php] w przeglądarce:

Widać, że przeglądarka wyświetla okno uwierzytelniania. W [2] widoczna jest wartość nagłówka [WWW-Authenticate] wysłanego przez serwer. Jeśli przyjrzymy się nagłówkom HTTP otrzymanym przez przeglądarkę, znajdziemy następujące informacje:
- wiersz 1: kod odpowiedzi [401 Unauthorized];
- wiersz 6: nagłówek HTTP [WWW-Authenticate];
- wiersz 7: treść odpowiedzi jest pusta;
Jeśli w polu [3-4] dwukrotnie wpiszemy [admin], odpowiedź serwera będzie następująca:
- wiersz 1: kod odpowiedzi 200 OK;
- wiersz 6: treść odpowiedzi jest pusta;
Jeśli w [3-4] wpiszemy błędne dane uwierzytelniające, przeglądarka [Firefox] używana do testów wyświetla okno uwierzytelniania w nieskończoność, dopóki nie zostaną wprowadzone prawidłowe dane uwierzytelniające. Za każdym razem następuje wymiana danych z serwerem, przy czym zawsze otrzymujemy tę samą odpowiedź, która powoduje wyświetlenie okna uwierzytelniania w przeglądarce.
Uruchommy klienta [auth-client.php] z nieuprawnionym użytkownikiem. Odpowiedź serwera jest następująca:
---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".
- W przypadku [1] klient rzeczywiście otrzymał kod 401;
- w przypadku [3] w kliencie zgłoszono wyjątek. Wyjątek ten został wygenerowany przez klienta Symfony o numerze [HttpClient]: generuje on wyjątek, gdy kod statusu odpowiedzi HTTP wskazuje, że wystąpił błąd po stronie serwera, a klient próbuje odczytać nagłówki lub treść odpowiedzi serwera. Komunikat w wierszu 3 pozwala nam stwierdzić, że serwer odpowiedział kodem [HTTP/1.0 401 Unauthorized], wskazując, że użytkownik nie został rozpoznany;
Uruchommy teraz klienta [auth-client.php] z autoryzowanym użytkownikiem [‘admin’,’admin’]. Odpowiedź serwera jest wówczas następująca:
---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 :
- wiersz 1: serwer odpowiedział [HTTP/1. 200 OK];
- wiersz 7: odpowiedź nie zawiera treści (0 bajtów);
17.11.3. Zabezpieczenie połączenia klient–serwer
Widzieliśmy, że w celu uwierzytelnienia się na serwerze klient wysyła nagłówek:
Jeśli ten wiersz zostanie przechwycony przez program szpiegowski, będzie on mógł z łatwością odczytać identyfikatory [login, mot de passe] zakodowane w systemie base64 w ciągu znaków [YWRtaW46YWRtaW4=]. Z tego powodu uwierzytelnianie musi odbywać się w ramach bezpiecznego połączenia między klientem a serwerem. Zabezpieczone połączenia URL wykorzystują protokół [HTTPS] zamiast protokołu HTTP. Protokół [HTTPS] to protokół HTTP w ramach bezpiecznego połączenia klient–serwer. Bezpieczne URL mają postać [https://chemin_document].
Nie wszystkie serwery WWW akceptują protokoły URL w tej postaci. Należy je zmodyfikować, aby były bezpieczne. Serwer Apache w Laragonie jest serwerem bezpiecznym, ale protokół HTTPS nie jest domyślnie aktywny. Należy go włączyć w menu Laragonu:

- na [4], należy włączyć szyfrowanie SSL serwera Apache;
Po wykonaniu tej czynności serwer Apache zostanie automatycznie ponownie uruchomiony:

- na [1], pojawia się zielona kłódka: oznacza to, że protokół HTTPS został włączony;
- W [2] pojawia się nowy port usługowy, w tym przypadku port 443. Jest to port usługowy bezpiecznego protokołu HTTPS;
Teraz, gdy mamy już bezpieczny serwer, zmieńmy plik konfiguracyjny klienta [config-auth-client.json] w następujący sposób:
{
"url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}
W pliku [2] protokół zmienił się na [https], a port na [443].
Teraz uruchommy klienta [auth-client.php] przy użyciu uprawnionego użytkownika [admin, admin]. Wyniki wyświetlone w konsoli są następujące:
Klient Symfony o nazwie [HttpClient] zgłosił wyjątek, ponieważ serwer przesłał mu certyfikat zaufania, którego [HttpClient] nie zaakceptował. Komunikacja SSL odbywa się przy użyciu certyfikatów zaufania poświadczonych przez oficjalne organy. Po włączeniu protokołu HTTPS na serwerze Apache w Laragon wygenerowano certyfikat z podpisem własnym dla serwera Apache. Certyfikat z podpisem własnym to certyfikat, który nie został zweryfikowany przez oficjalną instytucję. Klient Symfony [HttpClient] odrzucił ten certyfikat z podpisem własnym.
Można nakazać [HttpClient], aby nie sprawdzał ważności certyfikatu przesłanego przez serwer. Odbywa się to za pomocą opcji w metodzie [HttpClient::create]:
// tworzymy klienta HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
"verify_peer" => false
]);
Wiersz 4 nakazuje, aby certyfikat serwera nie był sprawdzany. Z tym problemem mieliśmy już do czynienia w skrypcie [http-02.php], o którym mowa w akapicie „link”. Skrypt ten korzystał z biblioteki [libcurl] w celu nawiązania połączenia ze stronami HTTP i HTTPS. Wówczas zastosowano następującą konfigurację dla tej biblioteki:
// Inicjowanie sesji cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// wystąpił błąd
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// opcje curl
$options = [
// tryb szczegółowy
CURLOPT_VERBOSE => true,
// nowe połączenie – brak pamięci podręcznej
CURLOPT_FRESH_CONNECT => true,
// limit czasu żądania (w sekundach)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// nie sprawdzaj ważności certyfikatów SSL
CURLOPT_SSL_VERIFYPEER => false,
// śledź przekierowania
CURLOPT_FOLLOWLOCATION => true,
// pobieranie żądanego dokumentu w postaci ciągu znaków
CURLOPT_RETURNTRANSFER => true
];
// konfiguracja curl
curl_setopt_array($curl, $options);
W wierszu 17 stała [CURLOPT_SSL_VERIFYPEER] określa, czy ma być sprawdzany certyfikat wysłany przez serwer. Klient [HttpClient] jest w rzeczywistości klientem [curl], gdy rozszerzenie [curl] jest włączone w konfiguracji PHP, tak jak ma to miejsce w tym przypadku. Klasą instancjonowaną przez [HttpClient::create] jest wówczas klasa [CurlHttpClient]. Stałe z klasy [curl] są dostępne w tej klasie, ale pod innymi nazwami:
$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 => '', // Śledzenie plików cookie podczas przekierowań
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'],
];
Podkreśliliśmy na żółto stałe używane przez klasę [CurlHttpClient].
Jeśli teraz uruchomimy klienta [auth-client] z użytkownikiem [admin, admin], otrzymamy następujący wynik:
---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 :
Użytkownik został poprawnie rozpoznany. Jeśli uruchomimy klienta [auth-client] z użytkownikiem innym niż [admin, admin], otrzymamy następujący wynik:
Teraz wiemy już, jak uwierzytelnić się na bezpiecznym serwerze.