17. Web Services
Note: By "web service," we mean here any web application that delivers raw data consumed by a client—a console script in the examples that follow. We are not concerned with any particular technology— , REST (REpresentational State Transfer), or SOAP (Simple Object Access Protocol), for example—which deliver more or less raw data in a well-defined format. REST returns JSON, whereas SOAP returns XML. Each of these technologies precisely defines how the client must query the server and the format the server’s response must take. In this course, we will be much more flexible regarding the nature of the client request and the server response. However, the scripts written and the tools used are similar to those of REST technology.
17.1. Introduction
Since PHP programs can be executed by a web server, such a program becomes a server-side program capable of serving multiple clients. From the client’s perspective, calling a web service amounts to requesting the URL of that service. The client can be written in any language, including PHP. In the latter case, we use the network functions we just covered. We also need to know how to “communicate” with a web service, that is, understand the HTTP protocol for communication between a web server and its clients. That was the purpose of the “link” section.
The web client described in the "link" section allowed us to explore part of the HTTP protocol.

In their simplest form, client/server exchanges proceed as follows:
- the client opens a connection to port 80 on the web server;
- it makes a request for a document;
- the web server sends the requested document and closes the connection;
- the client then closes the connection;
The document can be of various types: text in HTML format, an image, a video… It can be an existing document (static document) or a document generated on the fly by a script (dynamic document). In the latter case, we refer to web programming. The script for dynamically generating documents can be written in various languages: PHP, Python, Perl, Java, Ruby, C#, VB.NET…
In the following, we will use PHP scripts to dynamically generate text documents.

- In [1], the client establishes a connection with the server, requests a PHP script, and may or may not send parameters to that script;
- In [2], the web server executes the PHP script using the PHP interpreter. The script generates a document that is sent to the client [3];
- The server closes the connection. The client does the same;
The web server can handle multiple clients at once.
With the [Laragon] software package, the web server is an Apache server, an open-source server from the Apache Foundation (http://www.apache.org/). In the following applications, [Laragon] must be launched:

This starts the Apache web server as well as the MySQL DBMS.
The scripts executed by the web server will be written using the NetBeans tool. So far, we have written PHP scripts executed in a console environment:

The user uses the console to request the execution of a PHP script and receive the results.
In the client/server applications that follow:
- the client script is executed in a console environment;
- the server script is executed in a web context;

The server-side PHP script cannot be located just anywhere in the file system. In fact, the web server searches for the static and dynamic documents requested of it in locations specified by configuration. Laragon’s default configuration causes documents to be searched for in the <Laragon>/www folder, where <Laragon> is the Laragon installation folder. Thus, if a web client requests a document D with the URL [http://localhost/D], the web server will serve the document D located at the path [<Laragon>/www/D].
In the following examples, we will place the server scripts in the [www/php7/scripts-web] folder. If a server script is named S.php, it will be requested from the web server using the URL [http://localhost/php7/scripts-web/S.php]. The document [<Laragon>/www/php7/scripts-web/S.php] will then be served.

- in [1], the [<laragon>/www] folder;
- in [2], the [php7/scripts-web] folder;
To create server scripts with NetBeans, we will proceed as follows:

- in [1-2], we create a new project
- in [3-4], we select the [PHP] category and the [PHP Application] project

- in [5], the project name;
- in [6], the project folder in the file system. Note that this is in the [<laragon>/www] folder, where it should be;
- In [7-8], accept the default values;
- In [9-10], accept the default values provided. In [10], note that the URL of the scripts we will place in this project will start with the path [http://localhost/php7/scripts-web/];

- In [11], web frameworks written in PHP are offered to you. These frameworks are essential as soon as the web application grows in scale;
- In [12], you can add PHP libraries using the [Composer] tool. We used this tool twice in a Laragon [Terminal] window:
- to install the [SwiftMailer] library, which allows you to send emails;
- to install the [php-mime-mail-parser] library, which allows you to read emails;
- in [13], once the project creation wizard has been confirmed, the project appears in [13] in the Projects tab;
17.2. Writing a static page
Note: For the rest of this guide, [Laragon] must be running.
We will show how to create a static HTML (HyperText Markup Language) page using NetBeans:

- In [1-5], we create a folder named [01];


- In [6-12], we create an HTML file named [example-01.html];
The [example-01.html] file is generated with the following pre-filled content (May 2019):
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<title>TODO: Add 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>
Let's update its content as follows:
<!DOCTYPE html>
<html>
<head>
<title>PHP7 by example</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div><b>This is an example of a static page</b></div>
</body>
</html>
We have changed the page title (line 4) and its content (line 9).
Now let’s have Laragon’s Apache server display this HTML page:

- in [1-2], we have the Laragon Apache server display the page;
- in [3], the URL of the displayed page;
- in [4], the title we modified;
- in [5], the content we modified;
The displayed page is a static page: you can reload it as many times as you want in the browser (F5), and the same content is always displayed.
Most browsers provide access to the data exchanged between the client and the server, as described in the “Link” section. In Firefox (as of May 2019), press F12 to access this data:

As indicated in [1], let’s reload the page (F5):

- in [2], the document loaded by the browser: we select it;

- in [5], the document to be analyzed is selected;
- in [3-4], we request to view the client/server exchanges;
- In [6], these exchanges;

- in [7], select the headers tab;
- in [8], the URL requested by the browser;
- in [9], the command sent to the server is [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1];
- in [10], the HTTP headers subsequently sent by the browser (the client);
- in [11], the HTTP headers of the server’s response;

- in [12-14], the server’s response sent after the HTTP headers;
- in [14], we see that the client browser has received the HTML page we built. It then interpreted this code to display the following:

17.3. Creating a Dynamic Page in PHP
We will now write a dynamic page in PHP:


- in [1-8], we create a page [example-01.php];
The [example-01.php] file is generated pre-filled as follows (May 2019):
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
// put your code here
?>
</body>
</html>
We modify the code above as follows:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Example of a dynamic page</title>
</head>
<body>
<?php
// time: number of milliseconds between the present moment and January 1, 1970
// date and time display format
// d: 2-digit day
// m: 2-digit month
// y: 2-digit year
// H: hour 0,23
// I: minutes
// s: seconds
print "<b>Today's date and time: </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
Comments
- line 5: we changed the page title;
- line 17: prints the current date and time;
Basically, the PHP script above prints the current time to the console. However, when executed by a web server, the output of the [print] statement—which is usually directed to the script’s execution console—is redirected here to the connection linking the server to its client. Therefore, in a web context, the script above sends the current time as text to the client, in this case a browser.
Let’s run the [example-01.php] script:

- in [3], the URL requested from the Apache web server;
- in [4], the page title that we changed;
- in [5], the content generated by the [print] statement;
This is a dynamic page because if you reload the page several times in the browser (F5), its content changes (the time changes).
The browser has received an HTML stream. To view it, you need to display the page’s source code in the browser:

- to access the menu [1], right-click on the page in the browser;
- in [2], the page’s URL [example-01.php] but prefixed with [view-source:] [3];
- in [4], the HTML content that the browser displayed;
It is therefore important to remember that a PHP script intended to be executed by a web server must produce an HTML stream.
Let’s now look (F12) at the HTTP headers sent by the server to the client browser:

- in [3], an HTTP header that was not present when the static page was requested. This header indicates that the server’s response was generated by a PHP script;
We have seen that the server’s response (the HTML output here) can be generated by a PHP script. The script can also generate the HTTP headers and virtually all elements of the server’s response.
17.4. Basics of HTML
This chapter will not delve into web programming in PHP. An MVC web application is developed in the linked section. This chapter focuses instead on web services: PHP pages that deliver data, via a web server, to other PHP clients. Nevertheless, we felt it would be useful to provide the reader with some basics of HTML.
A web browser can display various documents, the most common being HTML (HyperText Markup Language) documents. These consist of text formatted with tags in the form <tag>text</tag>. Thus, the text <b>important</b> will display the text "important" in bold. There are standalone tags, such as the <hr/> tag, which displays a horizontal line. We will not go over all the tags that can be found in an HTML text. There are many WYSIWYG software programs that allow you to build a web page without writing a single line of HTML code. These tools automatically generate the HTML code for a layout created using the mouse and predefined controls. You can thus insert (using the mouse) a table into the page and then view the HTML code generated by the software to discover the tags to use for defining a table on a web page. It’s as simple as that. Furthermore, knowledge of HTML is essential since dynamic web applications must generate the HTML code themselves to send to web clients. This code is generated programmatically, and you must, of course, know what to generate so that the client receives the web page they want.
In short, you don’t need to know the entire HTML language to get started with web programming. However, this knowledge is necessary and can be acquired by using WYSIWYG web page builders such as DreamWeaver and dozens of others. Another way to discover the intricacies of HTML is to browse the web and view the source code of pages that feature interesting elements you haven’t encountered yet.
Consider the following example, which highlights some elements commonly found in a web document, such as:
- a table;
- an image;
- a link.

An HTML document generally has the following form:
<html> <head> <title>A title</title> ... </head> <body attributes> ... </body></html>
The entire document is enclosed by the <html>…</html> tags. It consists of two parts:
- <head>…</head>: this is the non-displayable part of the document. It provides information to the browser that will display the document. It often contains the <title>…</title> tag, which sets the text to be displayed in the browser’s title bar. It may also contain other tags, notably those defining the document’s keywords, which are then used by search engines. This section may also contain scripts, usually written in JavaScript or VBScript, which will be executed by the browser.
- <body attributes>…</body>: This is the section that will be displayed by the browser. The HTML tags contained in this section tell the browser the "desired" visual layout for the document. Each browser interprets these tags in its own way. As a result, two browsers may display the same web document differently. This is generally one of the challenges faced by web designers.
The HTML code for our example document is as follows:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Some HTML tags</title>
</head>
<body style="background-image: url(images/standard.jpg)">
<h1 style="text-align: left">Some HTML tags</h1>
<hr />
<table border="1">
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>cell(1,1)</td>
<td style="text-align: center;">cell(1,2)</td>
<td>cell(1,3)</td>
</tr>
<tr>
<td>cell(2,1)</td>
<td>cell(2,2)</td>
<td>cell(2,3</td>
</tr>
</tbody>
</table>
<br/><br/>
<table border="0">
<tr>
<td>An image</td>
<td>
<img border="0" src="images/cherry-tree.jpg"/></td>
</tr>
<tr>
<td>The Polytech'Angers website</td>
<td><a href="http://www.polytech-angers.fr/fr/index.html">here</a></td>
</tr>
</table>
</body>
</html>
| HTML tags and examples |
| <title>Some HTML tags</title> (line 5) The text [Some HTML tags] will appear in the browser's title bar when the document is displayed |
| <hr />: displays a horizontal line (line 10) |
| <table attributes>….</table>: to define the table (lines 12, 32) <thead>…</thead>: to define the column headers (lines 13, 19) <tbody>…</tbody>: to define the table content (lines 20, 31) <tr attributes>…</tr>: to define a row (lines 21, 25) <td attributes>…</td>: to define a cell (line 22) examples: <table border="1">…</table>: the border attribute defines the thickness of the table border <td style="text-align: center;">cell(1,2)</td> (line 23): defines a cell whose content will be cell(1,2). This content will be centered horizontally (text-align: center). |
| <img border="0" src="images/cherrytree.jpg"/> (line 38): defines an image with no border (border="0") whose source file is [images/cherrytree.jpg] on the web server (src="images/cherrytree.jpg"). This link is located on a web document accessible via the URL http://localhost/php7/scripts-web/01/balises.html. Therefore, the browser will request the URL http://localhost/php7/scripts-web/01/images/cerisier.jpg to retrieve the image referenced here. |
| <a href="http://www.polytech-angers.fr/fr/index.html">here</a> (line 42): makes the text "here" serve as a link to the URL http://www.polytech-angers.fr/fr/index.html. |
| <body style="background-image: url(images/standard.jpg)"> (line 8): indicates that the image to be used as the page background is located at the URL [images/standard.jpg] on the web server. In the context of our example, the browser will request the URL http://localhost/php7/scripts-web/01/images/standard.jpg to retrieve this background image. |
We can see in this simple example that to build the entire document, the browser must make three requests to the server:
- http://localhost/php7/scripts-web/01/images/balises.html to retrieve the document’s HTML source
- http://localhost/php7/scripts-web/01/images/cerisier.jpg to retrieve the image cerisier.jpg
- http://localhost/php7/scripts-web/01/images/standard.jpg to retrieve the background image standard.jpg
This is shown by the network traffic between the client and the server (F12 in the browser):

- In [3-5], we see the three requests made by the browser;
17.5. Making a Static Page Dynamic
Let’s show how we can make the HTML page [example-01.html] dynamic. Copy the content

We copied the content of [example-01.html] into the file [page-01.php]. If we run [2] this web script, we see the following in the browser:

- in [3], the requested URL;
- in [4], the page title;
- in [5], the page content;
If we view the code received by the browser, we find this:

- in [7], the HTML code placed in the [example-01.php] script
The PHP interpreter interpreted the script [page-01.php] and produced the same HTML output as the static page [example-01.html]. In the script [page-01.php], there was no PHP, only HTML. This teaches us something: when the PHP interpreter finds HTML in a PHP script, it leaves it alone and sends it as-is to the client.
Now let’s add some PHP instructions to the [page-01.php] script so that the PHP interpreter has something to do:
<!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>
In lines 4 and 9, we’ve added PHP code to dynamically generate the page title and content. Here, we assume that the variable [$page] is an object containing the data to be displayed.
If we run this new code, we get the following result in the browser:

- in [1], the requested URL;
- in [2], the page title could not be displayed because the variable [$page] was not defined;
- in [3], the same applies to the content;
Now, let’s write the following web script [example-02.php]:

The script [example-02.php] will be as follows:
<?php
// we define the elements of the page to be displayed
$page = new \stdclass();
$page->title="A new title";
$page->contents="New dynamically generated content";
// display [page-01]
require_once "page-01.php";
- lines 4-6: we define the object [$page];
- line 8: include the script [page-01.php]. The code in this script will then be interpreted:
- the variable [$page] is now defined and the PHP interpreter will use it;
- the HTML code from [page-01.php] will be sent as-is to the client;
- the results of the PHP [print] operations will be included in the text stream sent to the client;
Now, if we run the web script [example-02.php], we get the following in the browser:

If we view the text content received by the browser:

- the PHP code that was in [2] and [3] has been replaced by the results of the two [print] commands;
From this example, we can take away two key points:
- HTML pages intended for the browser can be isolated in PHP scripts containing only HTML code and a few dynamic parts generated by PHP code. There should be as little PHP as possible in these pages;
- all logic that generates the dynamic data included in HTML pages must be isolated in pure PHP scripts, containing no page presentation code (HTML, CSS, JavaScript, etc.);
This allows for a separation of tasks:
- the task of creating the web pages to be displayed (HTML, CSS, JavaScript, etc.);
- the task of the web application logic we are building. This logic can be implemented using a three-tier architecture, exactly as we did with the console scripts;
Next, we will build specific web scripts;
- they will send only data to the client and no presentation elements (HTML, CSS, JavaScript). They will therefore be data servers rather than web pages;
- The clients for these web scripts will be console scripts that will retrieve the data sent by the server and process it;
17.6. Client/server date/time application
We are now in the following configuration:

We will write:
- a web script [1] that sends the current date and time to its client;
- a console script [2] that will act as the client for the web script: it will retrieve the date and time sent by the web script and display them on the console;

- in [1], the web script [date-time-server.php];
- in [2], the console script [date-time-client], which is the client of the web script;
17.6.1. The server script
We have already written a web script that generates the current date and time in the linked section. It was the following script [example-01.php]:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Example of a dynamic page</title>
</head>
<body>
<?php
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: day (2 digits)
// m: 2-digit month
// y: 2-digit year
// H: hour 0,23
// i: minutes
// s: seconds
print "<b>Today's date and time: </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
We said we were going to write data servers: raw data without HTML markup. The server script [date-time-server.php] will then be as follows:
<?php
// Set the HTTP header [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// send date and time
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: day (2 digits)
// m: 2-digit month
// y: 2-digit year
// H: hour 0,23
// i: minutes
// s: seconds
print date("d/m/y H:i:s", time());
- Line 4: We set the HTTP header [Content-Type], which tells the client the nature of the document it will receive. Until now, the [Content-Type] was: [Content-Type: text/html; charset=UTF-8]. Here, we tell the client that the document is plain text without HTML markup. This isn’t important for our console client, which won’t use this header. It’s more important for browser clients, which do use this header;
Let’s run this server-side script:

If we examine the server’s response in the browser (F12), we see in [5] the HTTP header that the server script set and in [8], the received text document;

17.6.2. The client-side script
In the previous section, we developed several HTTP clients. We could use them to retrieve the text document sent by the server script [date-time-server.php]. We won’t do that. As we did for the SMTP and IMAP protocols, we’ll use a third-party library, namely the [HttpClient] component of the Symfony framework [https://symfony.com/doc/master/components/http_client.html].
As with the two previous libraries, we use the [Composer] tool to install the Symfony [HttpClient] component. In a Laragon [Terminal] window (see the linked section), enter the following command:

- in [3], verify that you are in the [<laragon>/www/] directory, where <laragon> is the Laragon installation directory;
- in [4], the [composer] command that installs the Symfony [HttpClient] library;
- in [5], nothing is installed because the [HttpClient] library had already been installed on this machine;
- In [6-7], new folders appear in [<laragon>/www/vendor/symfony];
Instead of [5], you should have something like the following:
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
Make sure the [<laragon>/www/vendor] folder is included in your project’s [Include Path] (see linked section):

Once this is done, we can write the console script [date-time-client.php]:

The console script [date-time-client.php] will use the following JSON file [config-date-time-client.json]:
- line 2: the server script URL;
The client script [date-time-client.php] will be as follows:
<?php
// date/time service client
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-date-time-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// send the request
$response = $httpClient->request('GET', $config['url']);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// retrieve the response body
$content = $response->getContent();
// display it
print "---Server response: [$content]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
exit;
}
Comments
- line 10: as we did for the previous libraries, we load the file [<laragon>/www/vendor/autoload.php];
- line 11: we declare the [HttpClient] class that we will use;
- lines 13–24: we retrieve the script’s configuration from the [$config] dictionary;
- line 27: we create an object of type [HttpClient];
- line 31: we request the server script’s URL using a GET request: [GET URL HTTP/1.1]. This operation is asynchronous. Execution continues on line 33 without waiting for the response;
- line 33: the response status is retrieved. This status is found in the first HTTP header returned by the server. Thus, if this header is [HTTP/1.1 200 OK], the response status is 200. This operation is blocking: execution only resumes once the client has received the entire response from the server;
- line 37: the HTTP headers of the response are requested;
- line 42: we retrieve the document returned by the server: we know that this document is text.
- lines 45–49: if an error occurs, the error message is displayed;
When the client script is executed (Laragon must be running for the server script to be accessible), the following result is displayed on the console:
---Response with status: 200
---Response headers
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
---Server response: [05/30/19 14:42:03]
We successfully retrieve the current date and time on line 8.
You might be curious to know what the client script sent to the server. To do this, we’ll use our generic TCP server (see the “link” section):

- in [1], the utilities folder;
- in [2], the TCP server is running on port 100;
- in [3], waiting for a command entered via the keyboard;
We modify the script’s configuration file [date-time-client.php]:
This time, the client contacts the server [localhost] on port 100. Therefore, our generic TCP server will be called upon. When we run the console script [date-time-client.php], the console of the generic TCP server changes as follows:

- in [3], the HTTP GET request constructed by the client script;
- in [4], the console script’s signature;
- in [5], the server’s response to the client script. Note that this is not a valid HTTP response:
- there should be HTTP headers;
- followed by a blank line;
- then the text document sent to the client;
- in [6], we close the connection with the client script so that it detects that it has received the entire response;
On the client-side script, the console displays the following:

- in [7], what the Symfony client received;
17.6.3. The server script – version 2
By default, PHP functions for writing a web script are not object-oriented. On the server side, we are therefore forced to mix traditional PHP classes and functions. To achieve a more consistent coding style, we will use the [HttpFoundation] library from the Symfony framework. It encapsulates all traditional PHP functions for a web service into a system of classes and interfaces. The library’s documentation is available at [https://symfony.com/doc/current/components/http_foundation.html] (May 2019).
To install the library, follow these steps in a Laragon terminal (see linked section):

- [2-3]: Make sure you are in the [<laragon>/www] folder;
- [4]: the [composer] command, which will install the [HttpFoundation] library;
- [5]: In this example, the library was already installed;
Upon first installation, you should see console logs similar to the following:
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
The second version of the web server [date-time-server-2.php] is as follows:
<?php
// using Symfony libraries
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
// Set the Content-Type header
$response = new Response();
$response->headers->set("content-type", "text/plain");
$response->setCharset("utf-8");
// Set the response content
//
// send date and time
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: day (2 digits)
// m: 2-digit month
// y: 2-digit year
// H: hour 0,23
// i: minutes
// s: seconds
$response->setContent(date("d/m/y H:i:s", time()));
// send the response
$response->send();
Comments
- Line 7: The [Response] class from Symfony's [HttpFoundation] library handles the entire response to the web service's clients;
- line 10: creation of an instance of the [Response] class;
- line 11: specifies that the response is of type [text/plain];
- line 12: the response is UTF-8 text;
- line 25: the response body is set to the content requested by the client;
- line 28: the response is sent to the client;
17.6.4. The client script – version 2
The client script remains unchanged. We only modify its configuration file [config-date-time-client.json]:
The results are the same as in version 1.
17.7. A JSON data server
A web script’s response can consist of multiple data points that can be organized into arrays and objects. The script can then send these various elements within a JSON string that the client will decode.

17.7.1. The server script
The [json-server.php] script uses the following [Person] class:
<?php
namespace Models;
class Person implements \JsonSerializable {
// attributes
private $lastName;
private $lastName;
private $age;
// Convert an associative array to a [Person] object
public function setFromArray(array $assoc): Person {
// initialize the current object with the associative array
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// result
return $this;
}
// getters and setters
public function getName() {
return $this->name;
}
public function getLastName() {
return $this->firstName;
}
public function setLastName($lastName) {
$this->lastName = $lastName;
return $this;
}
public function setLastName($lastName) {
$this->firstName = $firstName;
return $this;
}
public function getAge() {
return $this->age;
}
public function setAge($age) {
$this->age = $age;
return $this;
}
// toString
public function __toString(): string {
return "Person [$this->first_name, $this->last_name, $this->age]";
}
// implements the JsonSerializable interface
public function jsonSerialize(): array {
// returns an associative array with the object's attributes as keys
// this array can then be encoded as JSON
return get_object_vars($this);
}
// Convert JSON to a [Person] object
public static function jsonUnserialize(string $json): Person {
// create a Person from the JSON string
return (new Person())->setFromArray(json_decode($json, true));
}
}
Comments
- line 5: the class implements the PHP [JsonSerializable] interface. This requires it to implement the [jsonSerialize] method in lines 55–59. The method must return an associative array that will be serialized into JSON. When using the expression [json_encode($person)], the [json_encode] function checks whether the [Person] class implements the [JsonSerializable] interface. If so, the expression becomes [json_encode($person→serialize())];
- Lines 12–19: The class does not have a constructor but has an initializer. The [Person] class can then be instantiated using the expression [(new Person()) → setFromArray($array)]. There can be various types of initializers, whereas there can only be one constructor. These initializers allow for various instantiation modes of the form [(new Person())→initializer(…));
- lines 62–65: The static function [jsonUnserialize] allows you to create a [Person] object from its JSON string;
The [json-server.php] script will be as follows:
<?php
// dependencies
require_once __DIR__ . "/Person.php";
use \Models\Person;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
// Set the Content-Type header and the character set library used
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// create a Person object
$person = (new Person) -> setFromArray([
"lastName" => "de la Hûche",
"firstName" => "jean-paul",
"age" => 27]);
// an associative array
$assoc = ["attr1" => "value1",
"attr2" => [
"first_name" => "Jean-Paul",
"last_name" => "de la Hûche"
]
];
// The response content is JSON
$response->setContent(json_encode([$person, $assoc]));
// send the response
$response->send();
Comments
- lines 4-5: import the [Person] class;
- line 11: we specify that the document will be of type [application/json]. Upon receiving this header, browsers will display the JSON string formatted rather than as plain text;
- line 12: the JSON string will contain UTF-8 characters;
- lines 15-18: we create a [Person] object;
- lines 20–25: a two-level associative array is created;
- line 27: we send the JSON string of an array to the client:
- the [$person] element will be serialized to JSON using its [jsonSerialize] method;
- the [$assoc] element will be natively serialized to JSON;
If we run this server-side script (Laragon must be running), we get the following response in a browser:


Comments
- in [2], the formatted JSON response;
- in [4], the raw JSON response. Note the encoding of accented characters;
- In [6], it was the content type [application/json] sent by the server that caused the browser to format the output this way;
17.7.2. The client

The client [json-client.php] is configured by the following JSON file [config-json-client.json]:
The script [json-client.php] is as follows:
<?php
// JSON service client
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Person.php";
use \Models\Person;
// client configuration
const CONFIG_FILE_NAME = "config-json-client.json";
// Load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// send the request
$response = $httpClient->request('GET', $config['url']);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// retrieve the JSON body of the response
list($person, $assoc) = json_decode($response->getContent(), true);
// instantiate a Person object from the array of its attributes
$person = (new Person) -> setFromArray($person);
// display the server response
print "---Server response\n";
print "$person\n";
print "array=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
// Display the error
print "Communication error with the server: " . $ex->getMessage() . "\n";
}
Comments
- lines 12-13: import the [Person] class;
- line 30: create the HTTP client;
- line 44: decoding the JSON string sent by the server. We know that what was encoded is a two-element array containing two associative arrays;
- line 46: create a [Person] object to display it on line 49;
- line 50: the second associative array is displayed. The [print] statement cannot display arrays. Therefore, we convert this one into a JSON string. To correctly display accented characters, we must set the second parameter to [JSON_UNESCAPED_UNICODE]. We have seen that the accented characters are indeed encoded in the JSON string;
Executing the client-side script yields the following results:
---Response with status: 200
---Response headers
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
---Server response
Person [Jean-Paul, de la Hûche, 27]
array={"attr1":"value1","attr2":{"first-name":"Jean-Paul","last-name":"de la Hûche"}}
Lines 11 and 12: accented characters were retrieved correctly.
17.8. Retrieving web service environment variables
A server script runs in a web environment that it can access. This environment is stored in the $_SERVER dictionary, a global PHP variable. If we use the [HttpFoundation] library, this environment will be found in the [Request→server] field, where [Request] is the HTTP request processed by the web script.
17.8.1. The server script
We are writing a server application that sends its execution environment to its clients.

The web script [env-server.php] is as follows:
<?php
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// retrieve the request
$request = Request::createFromGlobals();
// build the response
$response = new Response();
// The response content is JSON in UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// Set the JSON content of the response
$response->setContent(json_encode($request->server->all()));
// send the response
$response->send();
- line 9: we retrieve the [Request] object, which encapsulates all available information about the HTTP request received by the web script as well as its execution environment;
- lines 13–14: we send plain text with UTF-8 characters to the client;
- line 16: the information sent to the client will be a string obtained by JSON serialization of the [$request→server→all()] object: [$request→server] represents the web script’s execution environment. It is an object of type [ServerBag], a kind of dictionary. [$request→server→all()] is a true dictionary, containing the contents of the [ServerBag];
- Line 18: The information is sent;
If this script is run from NetBeans, the browser displays the following page:

- in [2], the various keys of the environment dictionary;
- in [3], the values of these keys;
17.8.2. The client script

The client script [env-client.php] is configured by the following JSON file [config-env-client.json]:
The client script [env-client.php] is as follows:
<?php
// environment of a server-side script
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-env-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// Send the request to the server
$response = $httpClient->request('GET', $config['url']);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response\n";
$env = json_decode($response->getContent());
foreach ($env as $key => $value) {
print "[$key]=>$value\n";
}
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
Comments
- line 42: deserialize the JSON response from the server. This returns a hash;
- lines 43–45: display all values in this associative array;
The following console output is obtained:
---Response with status: 200
---Response headers
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
---Server response
[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
Here is the meaning of some of the variables (for Windows. On Linux, they would be different):
The value xxx of the HTTP header [Host: xxx] sent by the client | |
the value xxx of the HTTP header [User_Agent: xxx] sent by the client | |
the value xxx of the HTTP header [Accept-Encoding: xxx] sent by the client | |
the path to the executables on the machine where the server script is running | |
the DOS command prompt path | |
executable file extensions | |
the Windows installation folder | |
the web server signature. Nothing here. | |
the type of web server | |
The Internet name of the web server machine | |
the web server's listening port | |
the IP address of the web server machine, here 127.0.0.1 | |
the client's IP address. Here, the client was on the same machine as the server. | |
the client's communication port | |
the root of the directory tree of documents served by the web server | |
the TCP protocol of the URL request (http://localhost/php7/, etc.) | |
the email address of the web server administrator | |
the full path of the server script | |
the port from which the client made its request | |
the version of the HTTP protocol used by the web server | |
the HTTP method used by the client. There are four: GET, POST, PUT, DELETE | |
the parameters sent with a GET request /url?parameters | |
The URL requested by the client. If the browser requests the URL http://machine[:port]/uri, REQUEST_URI will be uri | |
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME'] |
17.9. Retrieval by the server of parameters sent by a client
17.9.1. Introduction
In the HTTP protocol, a client has two methods for passing parameters to the web server:
- it requests the service URL in the form
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
where the valid values must first be encoded so that certain reserved characters are replaced by their hexadecimal values;
- it requests the service URL in the form
POST url HTTP/1.0
then, among the HTTP headers sent to the server, includes the following header:
The rest of the headers sent by the client end with a blank line. It can then send its data in the form
where the valid values must, as with the GET method, be encoded beforehand. The number of characters sent to the server must be N, where N is the value declared in the header
The PHP script of the web service that retrieves the previous parami parameters sent by the client obtains their values from the array:
- $_GET["parami"] for a GET request;
- $_POST["parami"] for a POST request;
This applies to basic PHP functions. If the [HttpFoundation] library is used, these parameters will be found in:
- [Request]->query->get('parami') for a GET request;
- [Request]->request->get('parami') for a POST request;
where [Request] represents all the information about the request received by the web script;
17.9.2. The GET client – version 1

Client scripts are configured using the following JSON file [config-parameters-client.json]:
- line 1: the URL of the target web script for GET clients;
- line 2: the URL of the target web script for POST clients;
GET clients send three parameters [last_name, first_name, age] to the server. The client [parameters-get-client.php] is as follows:
<?php
// GET client of a web server
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// prepare the parameters
list($firstName, $lastName, $age) = array("jean-paul", "de la hûche", 45);
// we encode the information
$parameters = "first_name=" . urlencode($first_name) .
"&last_name=" . urlencode($last_name) .
"&age=$age”;
// send the request
$response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Communication error with the server: " . $ex->getMessage() . "\n";
}
Comments
- lines 33-35: encoding of parameters sent to the server. The parameters [$first_name, $last_name], which may contain UTF-8 characters, are encoded using the [urlencode] function. All non-alphanumeric characters (as defined by relational expressions) are replaced by %xx, where xx is the hexadecimal value of the character. Spaces are replaced by the + sign;
- line 37: the requested URL is $URL?$parameters, where $parameters is in the form name=val1&firstname=val2&age=val3;
- line 48: the client will simply display the server’s response;
You might be curious to see what the server receives during a parameterized GET request. To do this, we launch our generic server [RawTcpServer] on port 100 of the local machine from a Laragon terminal (see link section):

Verify that in [4], you are indeed in the utilities folder.
We modify the JSON file [parameters-get-client.json] that configures the GET and POST clients:
- Line 2: We have changed the web server port. Therefore, [RawTcpServer] will be contacted;
We run the client. In the [RawTcpServer] window, we see the following information:

- in [1], the GET request sent by the client. We can clearly see the encoding of certain characters;
17.9.3. The GET / POST Server

The server script [parameters-server.php] is as follows:
<?php
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// retrieve the request
$request = Request::createFromGlobals();
// retrieve the request parameters
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
// build the response
$response = new Response();
// The response content is UTF-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// response content - an array encoded in JSON
$response->setContent(json_encode([
"method" => $request->getMethod(),
"uri" => $request->getRequestUri(),
"getParameters" => $getParameters,
"bodyParameters" => $bodyParameters
], JSON_UNESCAPED_UNICODE));
// send the response
$response->send();
Comments
- line 9: creation of the [Request] object for the web script. This object encapsulates all the information the web script has received from the client;
- line 11: the [Request→query] object is of type [ParameterBag] and collects the parameters of any GET operation from a client. The expression [Request→query→get("X")] retrieves the parameter named X from the GET parameters [name=val1&firstname=val2&age=val3]. The expression [Request→query→all()] retrieves the dictionary of GET parameters;
- line 12: the object [Request→request] is of type [ParameterBag] and contains the parameters sent as a document from the client to the server. These parameters are also said to be uploaded because they belong to a document that the client sends to the server. The expression [Request→request→get("X")] retrieves the parameter named X from the uploaded parameters [last_name=val1&first_name=val2&age=val3]. The expression [Request→request→all()] retrieves the dictionary of uploaded parameters;
- lines 17–18: the client is informed that it will receive JSON encoded in UTF-8;
- lines 20–25: the server returns to the client all the parameters it received, the type of operation [GET / POST / …] performed by the client, and the requested URI. This method is obtained via the expression [$request→getMethod()]. The document sent to the client is the JSON string of an associative array, some of whose values are themselves associative arrays. The [JSON_UNESCAPED_UNICODE] parameter ensures that Unicode characters (such as accented characters, for example) are sent as-is and not encoded;
- line 27: the response is sent to the client;
Executing the client-side script yields the following results:
- line 10:
- [method]: the method is GET;
- [uri]: the URL-encoded parameters of the GET request are visible in the requested URI;
- [getParameters]: the array of GET parameters;
- [bodyParameters]: the array of uploaded parameters: it is empty;
17.9.4. The GET client – version 2
In the previous version of the client script, we URL-encoded the parameters sent to the server ourselves, for educational purposes. The [HttpClient] object can handle this task on its own. Here is the corresponding [parameters-get-client-2.php] script:
<?php
// GET request from a web server
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// prepare the parameters
list($firstName, $lastName, $age) = array("jean-paul", "de la hûche", 45);
// send the request to the server
$response = $httpClient->request('GET', $config['url-get'],
["query" => [
"first_name" => $first_name,
"name" => $name,
"age" => $age
]]);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
Comments
- lines 33–37: adding parameters to the GET request from line 32. The [HttpClient] object will handle the URL encoding itself;
17.9.5. The POST client
An HTTP client sends the following text sequence to the web server: HTTP headers, blank line, document. In the previous client, this sequence was as follows:
There was no document. There is another way to send parameters, known as the POST method. In this case, the text sequence sent to the web server is as follows:
This time, the parameters that were included in the HTTP headers for the GET client are part of the document sent after the headers in the POST client.
The POST client script [parameters-postclient.php] is as follows:
<?php
// POST client for a web server
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// prepare the parameters
list($firstName, $lastName, $age) = array("jean-paul", "de la hûche", 45);
// send the request to the server
$response = $httpClient->request('POST', $config['url-post'],
["body" => [
"last_name" => $last_name,
"last_name" => $last_name,
"age" => $age
]]);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
- line 32: we now have a POST HTTP request;
- lines 33–37: The POST parameters are called the body of the POST request: this is the document sent by the client to the server. Here, three parameters are sent [last_name, first_name, age];
- line 48: we display the server's JSON response;
The results of running the client script are as follows:
---Response status: 200
---Response headers
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
---Server response [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"firstName":"jean-paul","lastName":"de la hûche","age":"45"}}]
- Line 10: the method is [Post] and the parameters are of type [bodyParameters]. There are no [getParameters] as shown by the [uri];
You might be curious to see what the server receives during a POST request. To do this, we launch our generic [RawTcpServer] on port 100 of the local machine from a Laragon terminal (see linked paragraph):

Verify that in [4], you are indeed in the utilities folder.
We modify the JSON file [config-parameters-client.json] that configures the POST client:
- Line 3: We have changed the web server port. Therefore, [RawTcpServer] will be contacted;
We run the client. In the [RawTcpServer] window, we see the following information:

- in [6], the POST request;
- in [7]: the HTTP header [Content-Length] specifies the number of bytes in the document that the client will send to the server. The HTTP header [Content-Type] specifies the nature of this document. The type [application/x-www-form-urlencoded] denotes URL-encoded text;
- in [8], the blank line that marks the end of the HTTP headers and the start of the 44-byte document. What the screenshot does not show is the document itself. It is the URL-encoded string of parameters: [first_name=jean-paul&last_name=de+la+h%C3%BBche&age=45]. The reader can verify that it indeed has 44 characters;
17.9.6. A mixed POST client
In a POST request, you can mix parameters encoded in the URL with those encoded in the document sent by the client after the HTTP headers. Here is an example [parameters-mixte-postclient.php]:
<?php
// POST client from a web server
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// prepare the parameters
list($firstName, $lastName, $age) = array("jean-paul", "de la hûche", 45);
// send the request to the server
$response = $httpClient->request('POST', $config['url-post'],
[
// document parameters (body)
"body" => [
"lastName" => $lastName,
"last_name" => $last_name,
"age" => $age
],
// URL parameters (query)
"query" => [
"first_name2" => $first_name,
"last_name2" => $last_name,
"age2" => $age
]]);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
Comments
- line 32: a POST request;
- lines 40–45: URL-encoded parameters in the URL;
- lines 35–39: URL-encoded parameters in the request body (body, document);
Upon execution, the following console output is obtained:
- line 10: we can see that the server was able to retrieve both types of parameters;
17.9.7. A mixed GET client
We'll try to do the same thing as before with a GET request. The script [parameters-mixte-get-client.php] is as follows:
<?php
// POST client from a web server
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// prepare the parameters
list($firstName, $lastName, $age) = array("jean-paul", "de la hûche", 45);
// send the request to the server
$response = $httpClient->request('GET', $config['url-post'],
[
// document parameters (body)
"body" => [
"last_name" => $last_name,
"last_name" => $last_name,
"age" => $age
],
// URL parameters (query)
"query" => [
"first_name2" => $first_name,
"last_name2" => $last_name,
"age2" => $age
]]);
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server response
print "---Server response [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
Comments
- line 32: a POST request;
- lines 40–45: URL-encoded parameters in the URL;
- lines 35–39: URL-encoded parameters in the request body (body, document);
Upon execution, the following console output is obtained:
- Line 10: We can see that the server did not receive any URL-encoded parameters in the document sent by the client. When we look at the HTTP headers sent by the client, we see that it did indeed send a 44-character document, but the server did not process it;
So, which method should you choose to send information to the server?
- The [GET URL?param1=val1¶m2=val2&…] method uses a parameterized URL that can serve as a link. This is its main advantage: the user can bookmark such links;
- In other applications, you may not want to display the parameters sent to the server in a URL. For security reasons, for example. In that case, you’ll use the [POST] method and include the URL-encoded parameters in a document sent to the server;
17.10. Web Session Management
In the previous client/server examples, the process was as follows:
- the client opens a connection to port 80 on the web server machine;
- it sends the text sequence: HTTP headers, blank line, [document];
- in response, the server sends a sequence of the same type;
- the server closes the connection to the client;
- the client closes the connection to the server;
If the same client makes a new request to the web server shortly thereafter, a new connection is established between the client and the server. The server cannot tell whether the connecting client has visited before or if this is a first-time request. Between connections, the server “forgets” its client. For this reason, the HTTP protocol is said to be a stateless protocol. However, it is useful for the server to remember its clients. For example, if an application is secure, the client will send the server a username and password to authenticate itself. If the server "forgets" its client between connections, the client would have to authenticate itself with every new connection, which is not feasible.
To track a client, the server proceeds as follows: when a client makes an initial request, the server includes an identifier in its response, which the client must then send back with every new request. Thanks to this identifier, which is unique to each client, the server can recognize a client. It can then manage a memory entry for that client in the form of a memory entry uniquely associated with the client’s identifier.
Technically, this is how it works:
- In the response to a new client, the server includes the HTTP header Set-Cookie: Key=Identifier. It does this only on the first request;
- in subsequent requests, the client will send back its identifier via the HTTP header Cookie: Key=Identifier so that the server can recognize it;
One might wonder how the server knows it is dealing with a new client rather than a returning one. It is the presence of the HTTP Cookie header in the client’s HTTP headers that tells it. For a new client, this header is absent.
All connections from a given client are referred to as a session.
17.10.1. The configuration file [php.ini]
For session management to work correctly with PHP, you must verify that it is properly configured. On Windows, its configuration file is php.ini. Depending on the execution context (console, web), the [php.ini] configuration file must be located in different directories. To find these, use the following script:
Line 4: The phpinfo function provides information about the PHP interpreter running the script. In particular, it provides the path to the configuration file [php.ini] being used.
We have already used this script in a console environment (see the "link" section). In a web environment, we get the following result:

- In [1-2], the [php.ini] file that configures the web script interpreter. This file contains a session section:
- line 2: client session data is saved to a file;
- line 3: the directory where session data is saved. If this directory does not exist, no error is reported and session management does not work;
- lines 4–6: indicate that the session ID is managed by the Set-Cookie and Cookie HTTP headers;
- line 7: the Set-Cookie header will be in the form Set-Cookie: PHPSESSID=session_id;
- line 8: a client session is not started automatically. The server script must explicitly request it using the session_start() function;
- line 9: the session cookie remains valid until the client browser is closed;
- line 10: the path for which the session cookie must be sent. If [session.cookie_path = /xxx], then every time the browser requests a URL of the form [/xxx/yyy/zzz], it must send the cookie. Here, the path [/] indicates that the cookie must be sent for every URL on the site;
- line 13: certain session objects must be serialized in order to be stored in a file. PHP handles this serialization/deserialization using the [serialize / unserialize] functions;
- line 16: timeout period after which session objects stored in the session file are considered obsolete;
- line 19: session lifetime. After this period, a new session is created and the objects saved in the previous session are lost;
17.10.2. Example 1
17.10.2.1. The server

Session ID management is transparent to a web service. This ID is managed by the web server. A web service accesses the client’s session via the session_start() function. From that point on, the web service can read/write data to the client’s session via the $_SESSION dictionary. If the [HttpFoundation] library is used, the session is available via the [Request→getSession] expression.
The following code [session-server.php] demonstrates session-based management of three counters. With each new request, the web script increments these counters and stores them in the session so they can be retrieved during the next request.
<?php
// dependencies
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;
//
// retrieve the request
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// retrieve three counters from the session
if ($session->has("N1")) {
// increment counter N1
$session->set("N1", (int) $session->get("N1") + 1);
} else {
// Counter N1 is not in the session—create it
$session->set("N1", 0);
}
if ($session->has("N2")) {
// increment counter N2
$session->set("N2", (int) $session->get("N2") + 1);
} else {
// The N2 counter is not in the session—create it
$session->set("N2", 10);
}
if ($session->has("N3")) {
// increment counter N3
$session->set("N3", (int) $session->get("N3") + 1);
} else {
// The N3 counter is not in the session—we create it
$session->set("N3", 100);
}
// build the response
$response = new Response();
// The response content is UTF-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// The response will be a JSON array containing the three counters
$response->setContent(json_encode([
"N1" => $session->get("N1"),
"N2" => $session->get("N2"),
"N3" => $session->get("N3")]));
// Send the response
$response->send();
- line 10: the [$request] object encapsulates all the information about the request received by the web script;
- lines 12-13: a session is created and activated. The [Session] object encapsulates the session data corresponding to the session cookie sent by the client. If the client has not sent such a cookie, then no data is stored in [Session]. The web script will include the HTTP header [Set-Cookie: PHPSESSID=xxx] in its first response. In subsequent requests, the client will send the HTTP header [Cookie: PHPSESSID=xxx] to indicate the session whose contents it wants to use. A session is a client’s memory;
- line 15: we check if the session has a key named [N1]. This will be the name of our first counter. If not (line 20), we set its value to 0 and add it to the session. If so (line 23), we:
- retrieve it from the session;
- increment its value by 1;
- put it back into the session;
- lines 22–35: we do the same for the other two counters, N2 and N3;
- lines 36–40: prepare a response of type [application/json];
- lines 42–45: the response will be the JSON string of an array containing the three counters;
- line 48: send the response to the client;
In the client/server relationship, managing the client session on the server depends on both parties, the client and the server:
- the server is responsible for sending an identifier to the client during its first request
- the client is responsible for sending this identifier back with each new request. If it does not, the server will assume it is a new client and generate a new identifier for a new session.
Results
We use a web browser as the client. By default (actually, by configuration), the browser does indeed send back to the server the session identifiers that the server sends to it. As requests are made, the browser will receive the three counters sent by the server and will see their values increment.

- In [2], the first request to the web service;
- in [4], the fourth request shows that the counters have indeed been incremented. The counter values are indeed stored over the course of the requests;
Let’s use developer mode to view the HTTP headers exchanged between the server and the client. We close Firefox to end the current session with the server, reopen it, and enable developer mode (F12). This will clear the browser’s current session, causing it to start a new one. We request the service [session-server.php]:

In [5], we see the session ID sent by the server in its response to the client’s first request. It uses the Set-Cookie HTTP header.
Let’s make a new request by refreshing (F5) the page in the web browser:

Here, we’ll notice two things:
- In [11], the web browser sends the session ID back with the HTTP Cookie header.
- In [12], the web service no longer includes this identifier in its response. It is now the client’s responsibility to send it with each of its requests.
17.10.2.2. The client
We will now write a client-side script based on the previous server-side script. In its session management, it must behave like the web browser:
- in the server’s response to its first request, it must find the session ID that the server sends it. It knows it will find it in the HTTP Set-Cookie header.
- For each subsequent request, it must send the received identifier back to the server. It will do this using the HTTP Cookie header.

The client [session-client] is configured by the following JSON file [config-session-client.json]:
The client code [session-client] is as follows:
<?php
// session management
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-session-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create();
try {
// we will make 10 requests
for ($i = 0; $i < 10; $i++) {
// send the request to the server
if (!isset($sessionCookie)) {
// without a session
$response = $httpClient->request('GET', $config['url']);
} else {
// with session
$response = $httpClient->request('GET', $config['url'],
["headers" => ["Cookie" => $sessionCookie]]);
}
// response status
$statusCode = $response->getStatusCode();
print "---Response with status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// retrieve the session cookie if it exists
if (isset($headers["set-cookie"])) {
// Session cookie?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $fields);
if ($match) {
$sessionCookie = "PHPSESSID=" . $fields[1];
}
}
}
}
// Display the server's JSON response
print "---Server response: {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Communication error with the server: " . $ex->getMessage() . "\n";
}
Comments
- line 27: creation of the HTTP client;
- line 30: we will send the same request to the server [session-server.php] 10 times;
- line 32: the variable [$sessionCookie] will be set to the value of the [Set-Cookie] HTTP header received by the client;
- lines 32–34: if this variable does not exist, it means the session has not yet started. We send the [GET] request without the [Cookie] header;
- lines 35–38: otherwise, the session has started, and we send the [GET] request with the [Cookie] header. The value of this header will be [$sessionCookie];
- line 50: if the [Set-Cookie] header is among the received HTTP headers, then we look for the session cookie;
- line 52: the web server may send multiple [Set-Cookie] headers. The session cookie is just one of them. In our example, it has the specific format [PHPSESSID=xxx;];
- lines 53–57: a regular expression is used to find the session cookie;
- line 62: once the 10 requests have been made, we display the last JSON response from the server;
Executing the client script causes the following to be displayed in the NetBeans console:
"C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\web-clients\06\session-client.php"
---Response with status: 200
---Response headers
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
---Response with status: 200
---Response headers
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
---Response with status: 200
---Response headers
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
---Response with status: 200
…………………………………………………………
---Response with status: 200
---Response headers
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
---Server response: {"N1":9,"N2":19,"N3":109}
- Line 8: In its first response, the server sends the session ID. In subsequent responses, it no longer sends it;
- line 41: the three counters [N1, N2, N3] have indeed been incremented 9 times. During request #1, they were reset to zero;
The following example shows that you can also save the values of an array or an object in the session.
17.10.3. Example 2
17.10.3.1. The server

We are going to put a [Person] object in the session. The definition of this class is as follows:
<?php
namespace Models;
class Person implements \JsonSerializable {
// attributes
private $lastName;
private $lastName;
private $age;
// Convert an associative array to a [Person] object
public function setFromArray(array $assoc): Person {
// initialize the current object with the associative array
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// result
return $this;
}
// getters and setters
public function getName() {
return $this->name;
}
public function getLastName() {
return $this->firstName;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setLastName($lastName) {
$this->firstName = $firstName;
return $this;
}
public function getAge() {
return $this->age;
}
public function setAge($age) {
$this->age = $age;
return $this;
}
// toString
public function __toString(): string {
return "Person [$this->firstName, $this->lastName, $this->age]";
}
// implements the JsonSerializable interface
public function jsonSerialize(): array {
// returns an associative array with the object's attributes as keys
// this array can then be encoded as JSON
return get_object_vars($this);
}
// Convert a JSON string to a [Person] object
public static function jsonUnserialize(string $json): Person {
// Create a Person from the JSON string
return (new Person) -> setFromArray(json_decode($json, true));
}
}
The server script will be as follows:
<?php
// dependencies
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__ . "/Person.php";
use \Models\Person;
//
// retrieve the current request
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// retrieve various data from the session
// array
if ($session->has("array")) {
// the array is in the session - increment all its values
$array = $session->get("array");
for ($i = 0; $i < count($array); $i++) {
$array[$i] += 1;
}
// Put the array back into the session
$session->set("array", $array);
} else {
// The array is not in the session—create it
$array = [0, 10, 100];
// add it to the session
$session->set("array", $array);
}
// dictionary
if ($session->has("assoc")) {
// [assoc] is in the session - increment all its elements
$assoc = $session->get("assoc");
foreach ($assoc as $key => $value) {
$assoc[$key] = $value + 1;
}
// we put $assoc into the session
$session->set("assoc", $assoc);
} else {
// [assoc] is not in the session—create it
$assoc = ["one" => 0, "two" => 10, "three" => 100];
// Put $assoc in the session
$session->set("assoc", $assoc);
}
// Person object
if ($session->has("person")) {
// [person] is in the session - we increment their age
$person = $session->get("person");
$person->setAge($person->getAge() + 1);
} else {
// [person] is not in the session - create it
$person = (new Person) ->setFromArray(
["firstName" => "Leonard", "lastName" => "Huche", "age" => 0]);
// we put $person in the session
$session->set("person", $person);
}
// we build the response
$response = new Response();
// The response content is JSON in UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
"table" => $table,
"assoc" => $assoc,
"person" => $person], JSON_UNESCAPED_UNICODE));
// send the response
$response->send();
Comments
- lines 16-17: retrieve the current session and activate it;
- lines 21-34: we manage an array [array] stored in the session. With each new request, its elements are incremented by 1;
- lines 36-49: manage an associative array [assoc] stored in the session. With each new request, its elements are incremented by 1;
- lines 51–61: manage a sessioned object [Person]. With each new request, this person’s age is incremented by 1;
- lines 62–73: a JSON response is sent to the client: the JSON string of an associative array;
Let’s run this script from NetBeans. The first two requests yield the following results (press F5 in the browser for the second one):

- we see that in [6-8], all counters have been incremented;
17.10.3.2. The client

The client is the same as in Example 1 (link section). We only modify its configuration file [config-session-client]:
The execution produces the following results:
---Response with status: 200
---Response headers
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
---Response with status: 200
………….……………………………………………………….
---Response with status: 200
---Response headers
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
---Server response: {"tableau":[9,19,109],"assoc":{"one":9,"two":19,"three":109},"person":{"last_name":"Hûche","first_name":"Léonard","age":9}}
- line [22], we can see that all counters have been incremented;
17.11. Authentication
We will now focus on web services intended for specific users only. The client must therefore authenticate with the web service before receiving a response.
17.11.1. The client

The client code [auth-client.php] is as follows:
<?php
// session management
//
// error handling
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// client configuration
const CONFIG_FILE_NAME = "config-auth-client.json";
// load the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "The configuration file [" . CONFIG_FILE_NAME . "] does not exist\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Error parsing the JSON configuration file [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// Create an HTTP client
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
// "verify_peer" => false,
// "verify_host" => false
]);
try {
// Send the request to the server
$response = $httpClient->request('GET', $config['url']);
// response status
$statusCode = $response->getStatusCode();
print "---Response status: $statusCode\n";
// retrieve the headers
print "---Response headers\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// display the server's JSON response
print "---Server response: {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// display the error
print "Error communicating with the server: " . $ex->getMessage() . "\n";
}
Comments
- lines 27–31: we passed a parameter to the static method [HttpClient::create], a hash;
- line 28: the [auth_basic] key has a value of a two-element array [user, password]. The client will use these elements to authenticate with the web service. The [auth_basic] key refers to an authentication type called [Basic Authorization], named after the HTTP header that the client will send. There are other types of authentication;
- Apart from this code, the client is identical to the previous ones;
To view the HTTP headers sent by the client, we will connect it to the generic TCP server [RawTcpServer] as we have done many times before:

We launch the client with the following [config-auth-client.json] configuration:
The [RawTcpServer] then receives the following lines:

- In [5], we see the [Authorization: Basic XXX] header sent by the client. The XXX string is the [user:password] string encoded in Base64;
To verify this, you can decode the received string on the website [https://www.base64decode.org/]:

17.11.2. The server

The [auth-server.php] server is as follows:
<?php
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// authorized users
$users = ["admin" => "admin"];
//
// retrieve the current request
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// Does the user exist?
$found = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// preparing the response
$response = new Response();
// Set the response status code
if (!$found) {
// not found - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 for example\"")]);
} else {
// found - status code 200
$response->setStatusCode(Response::HTTP_OK);
}
// the response has no content, only HTTP headers
$response->send();
Comments
- line 9: authorized users, in this case a single user with the login [admin] and password [admin];
- line 14: the user’s ID is retrieved from the [PHP-AUTH-USER] header. This is not a header sent by the client, but one constructed by the server’s PHP;
- line 15: the user’s password is retrieved from the [PHP-AUTH-PW] header, a header constructed by PHP;
- line 17: we search for the user attempting to log in within the list of authorized users;
- lines 23–24: if the user is not recognized, the following is sent to the client
- line 23: the [401 Unauthorized] status code;
- line 24: a [WWW-Authenticate: Basic realm=”something”] header. Most browsers recognize this header and will display an authentication window prompting the user to log in. HTTP headers must be encoded in ISO 8859-1. NetBeans text, however, is encoded in UTF-8. The [utf8_decode] function handles the conversion from UTF-8 to ISO 8859-1. Here, it wasn’t necessary because the characters in the string [PHP7 in this example] are the same in both UTF-8 and ISO 8859-1. The function is included solely as a reminder of the encoding used by the HTTP headers;
- line 25: if the user has been recognized, we send the client the [200 OK] code;
Let’s request the URL [auth-server.php] using a browser:

We see that the browser displays an authentication window. In [2], we see the value of the [WWW-Authenticate] header sent by the server. If we look at the HTTP headers received by the browser, we find the following:
- Line 1: the response status code [401 Unauthorized];
- line 6: the HTTP header [WWW-Authenticate];
- line 7: the response body is empty;
If, in [3-4], you type [admin] twice, the server's response is as follows:
- line 1: the 200 OK response code;
- line 6: the response body is empty;
If, in [3-4], incorrect credentials are entered, the browser [Firefox] used for testing continuously displays the authentication window until the correct credentials are entered. Each time a round trip to the server occurs, the same response is received, which triggers the browser’s authentication window.
Let’s run the client [auth-client.php] with an unauthorized user. The server’s response is as follows:
---Response with status: 401
---Response headers
Communication error with the server: HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
- In [1], the client did indeed receive a 401 code;
- in [3], an exception was thrown in the client. It was the Symfony [HttpClient] that threw it: it throws an exception when the HTTP response status code indicates a server-side error, and the client attempts to read the headers or the content of the server’s response. The message on line 3 shows that the server responded with [HTTP/1.0 401 Unauthorized] to indicate that the user was not recognized;
Now let’s run the client [auth-client.php] with the authorized user [‘admin’,’admin’]. The server’s response is then as follows:
---Response with status: 200
---Response headers
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
---Server response:
- line 1: the server responded [HTTP/1.2 200 OK];
- line 7: the response has no content (0 bytes);
17.11.3. Securing the client/server connection
We have seen that to authenticate with the server, the client sends the header:
If this line is intercepted by spyware, it can easily retrieve the [username, password] credentials encoded in Base64 within the string [YWRtaW46YWRtaW4=]. For this reason, authentication must take place over a secure connection between the client and the server. Secure URLs use the [HTTPS] protocol instead of the HTTP protocol. The [HTTPS] protocol is the HTTP protocol within a secure client/server connection. Secure URLs are in the form [https://chemin_document].
Not all web servers accept URLs in this format. They must be modified to be secure. Laragon’s Apache server is a secure server, but the HTTPS protocol is not enabled by default. You must enable it in the Laragon menu:

- in [4], enable SSL encryption for the Apache server;
Once this is done, the Apache server is automatically restarted:

- in [1], a green padlock appears: this indicates that the HTTPS protocol has been enabled;
- in [2], a new service port appears, port 443 in this case. This is the service port for the secure HTTPS protocol;
Now that we have a secure server, let's modify the client's configuration file [config-auth-client.json] as follows:
In [2], the protocol has changed to [https] and the port to [443].
Now let’s run the client [auth-client.php] with the authorized user [admin, admin]. The console output is as follows:
The Symfony [HttpClient] threw an exception because the server sent it a trust certificate that [HttpClient] did not accept. SSL communication relies on trust certificates issued by official authorities. When we enabled the HTTPS protocol on the Laragon Apache server, a self-signed certificate was generated for the Apache server. A self-signed certificate is a certificate that has not been validated by an official authority. The Symfony [HttpClient] client rejected this self-signed certificate.
It is possible to instruct [HttpClient] not to verify the validity of the certificate sent by the server. This is done using options in the [HttpClient::create] method:
// create an HTTP client
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
"verify_peer" => false
]);
Line 4 specifies that the server certificate should not be verified. We had already encountered this issue in the [http-02.php] script in the linked section. That script used the [libcurl] library to connect to HTTP and HTTPS sites. We had used the following configuration for that library:
// Initialize a cURL session
$curl = curl_init($url);
if ($curl === FALSE) {
// An error occurred
return "Error initializing the cURL session for the site [$site]";
}
// curl options
$options = [
// verbose mode
CURLOPT_VERBOSE => true,
// new connection - no cache
CURLOPT_FRESH_CONNECT => true,
// request timeout (in seconds)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// Do not verify the validity of SSL certificates
CURLOPT_SSL_VERIFYPEER => false,
// follow redirects
CURLOPT_FOLLOWLOCATION => true,
// retrieve the requested document as a string
CURLOPT_RETURNTRANSFER => true
];
// configure curl
curl_setopt_array($curl, $options);
Line 17: The constant [CURLOPT_SSL_VERIFYPEER] controls whether or not to verify the certificate sent by the server. The [HttpClient] client is actually a [curl] client when the [curl] extension is enabled in the PHP configuration, as is the case here. The class instantiated by [HttpClient::create] is then the [CurlHttpClient] class. The [curl] constants are available in this class but under different names:
$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 => '', // Keep track of cookies during redirects
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'],
];
We have highlighted in yellow the constants used by [CurlHttpClient].
If we now run the [auth-client] client with the user [admin, admin], we get the following result:
---Response with status: 200
---Response headers
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
---Server response:
The user was successfully authenticated. If we run the [auth-client] client with a user other than [admin, admin], we get the following result:
Now we know how to authenticate with a secure server.