22. Practical Exercise – Version 11
It is still common for web services to send their response in the form of a XML document rather than a JSON document:
- the JSON document is smaller, but you need instructions to understand it;
- the XML document is more verbose but is self-documenting. It is immediately understandable.
We are modifying the client/server application from version 10 so that the server now sends a XML document in response to its clients. In doing so, we are putting the previous chapter into practice.
22.1. The server

This architecture will be implemented by the following files:
| scripts-web/tax/version-11
├── Data
│ ├── .htaccess
│ ├── config-server.json
│ └── logs.txt
├── Utilities
│ ├── XmlUtilities.php
│ └── testXml.php
└── tax-server.php
|
22.1.1. The [XmlUtilities] class
The PHP 7 course added a method, [getXmlForArrayOfAttributes], to the [Utilities] class that rendered the XML document from an array. Our server already uses the [Utilities] class from version 5 (for the [Database] class): we create a new [XmlUtilities] class:
| <?php
declare(strict_types=1);
// namespace
namespace Application;
// utility functions for the XML format: the class can be neither instantiated nor derived
final class XmlUtilities
{
// private constructor: the class cannot be instantiated
private function __construct()
{
}
// returns the XML document of an array [$data]: the root element is named [$root]
// the keys of the array become element names, the values their content
public static function arrayToXml(array $data, string $root): string
{
// an empty XML document (new DOM API of PHP 8.4)
$document = \Dom\XMLDocument::createEmpty();
// the document will be indented
$document->formatOutput = true;
// the root element and its descendants
$document->append(self::element($document, $root, $data));
// the XML text of the document
return $document->saveXml();
}
// returns the XML element [<$name>$value</$name>]
private static function element(\Dom\XMLDocument $document, string $name, mixed $value): \Dom\Element
{
$element = $document->createElement($name);
if (is_array($value)) {
// an array: each element of the array becomes a child element (recursive call)
foreach ($value as $key => $subValue) {
// the elements of a list (keys 0, 1, 2...) are named [item]
$element->append(self::element($document, is_int($key) ? "item" : $key, $subValue));
}
} else {
// a simple value becomes the text of the element (the characters <, >, & are encoded by DOM)
$element->textContent = match (true) {
is_bool($value) => $value ? "true" : "false",
default => (string) $value,
};
}
return $element;
}
}
|
Comments
- line 9: Like the [Utilities] class in version 5, this class has only static methods. It is [final], and its constructor is private: it cannot be derived from or instantiated;
- line 18: The static method [arrayToXml] returns the document XML from the array [$data] passed as a parameter. The second parameter is the name of the document’s root element;
- line 21: We use the new API DOM from PHP 8.4, introduced in the previous chapter. The static method [createEmpty] creates an empty XML document. Script PHP 7 used [SimpleXML], which can also generate a document, but not as well: you had to code the special characters in the text yourself using the [htmlspecialchars] function;
- line 23: the resulting document will be indented, making it more readable;
- line 25: the document’s root element, created by the [element] method, is added to the document;
- line 27: the method [saveXml] renders the text XML in the document;
- line 31: the private method [element] returns the element XML with the name [$name], representing the value [$value];
- line 33: the [createElement] method of the document creates the element;
- Lines 34–38: If the value is an array, each of its elements becomes a child element, created by a recursive call to the [element] method. The keys of the array become the names of the child elements. The keys of a list are the integers 0, 1, 2..., which cannot be XML element names: these elements are therefore named [item]. The script PHP 7 referred to them as [i0], [i1], [i2]...;
- lines 40–45: otherwise, the value becomes the element’s text, assigned to its [textContent] property. The characters [<], [>], and [&] in the text are encoded as DOM ([<], [>], [&]). Booleans are written as [true] and [false]; other values are converted to strings. The expression [match (true)] selects the first branch whose condition is true.
We write the following test script, [testXml.php]:
| <?php
// strict compliance with the declared types of function parameters and results
declare(strict_types=1);
// dependency
require_once __DIR__ . "/XmlUtilities.php";
use Application\XmlUtilities;
// an associative array
$array = [
"lastName" => "amédée",
"firstName" => "sylvain",
"age" => 40,
"children" => [
["lastName" => "amédée", "firstName" => "béatrice", "age" => 6],
["lastName" => "amédée", "firstName" => "bertrand", "age" => 4],
],
"remark" => "<married> & happy",
];
// the response is an XML document
header("Content-Type: application/xml; charset=utf-8");
print XmlUtilities::arrayToXml($array, "person");
|
- lines 12–21: an associative array containing a list and text with special characters from XML;
- line 23: the function [header] sets the header HTTP [Content-Type] of the response: it is a XML document;
- line 24: the XML document in the table, with root element [person], is sent to the client.
Let’s request URL and [http://localhost/php8-oct-2026/scripts-web/tax/version-11/Utilities/testXml.php] along with [curl]:
| > curl http://localhost/php8-oct-2026/scripts-web/tax/version-11/Utilities/testXml.php
<?xml version="1.0" encoding="UTF-8"?>
<person>
<lastName>amédée</lastName>
<firstName>sylvain</firstName>
<age>40</age>
<children>
<item>
<lastName>amédée</lastName>
<firstName>béatrice</firstName>
<age>6</age>
</item>
<item>
<lastName>amédée</lastName>
<firstName>bertrand</firstName>
<age>4</age>
</item>
</children>
<remark><married> & happy</remark>
</person>
|
- lines 7–18: the list [children] and its two elements [item];
- line 19: the characters [<], [>], and [&] have been encoded.
A browser displays the same document in tree form:

Note that [Chrome] decoded the text of the element [remark].
22.1.2. The server-side script
The server-side script [tax-server.php] must be modified, along with its configuration file [config-server.json]:
| {
"databaseFilename": "../version-08/Data/database-mysql.json",
"dependencies": [
"../../../scripts-console/tax/version-05/Entities/TaxException.php",
"../../../scripts-console/tax/version-05/Entities/TaxAdminData.php",
"../../../scripts-console/tax/version-05/Entities/Database.php",
"../../../scripts-console/tax/version-05/Utilities/Utilities.php",
"../version-08/Dao/InterfaceServerDao.php",
"../version-08/Dao/ServerDao.php",
"../version-09/Dao/ServerDaoWithSession.php",
"../version-08/Business/InterfaceServerBusiness.php",
"../version-08/Business/ServerBusiness.php",
"../version-09/Utilities/Logger.php",
"../version-09/Utilities/SendAdminMail.php",
"Utilities/XmlUtilities.php"
],
|
- Line 15: Include the new class [XmlUtilities]. The rest of the configuration is the same as in version 10.
The changes to the server script are as follows:
| }
// sending the HTTP response to the client
// $result: the array to send, in the form of the XML document <response>...</response>
// $statusCode: the HTTP status of the response
// $headers: HTTP headers to add to the response
// $logger: the application's log (null if it could not be created)
// $redis: the Redis client (null if it has not been created yet)
function sendResponse(
array $result,
int $statusCode,
array $headers = [],
?Logger $logger = null,
?RedisClient $redis = null
): void {
// the XML response
$response = new Response(XmlUtilities::arrayToXml($result, "response"), $statusCode, $headers);
$response->headers->set("Content-Type", "application/xml; charset=utf-8");
// sending
$response->send();
// log in JSON, more concise than XML
$logger?->write(json_encode(
["response" => $result],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
));
// closing the connection to the Redis server
$redis?->disconnect();
}
|
Comments
- The [JsonResponse] class is no longer used: the response is a simple [Response] response;
- Line 212: The server response is now the XML document, with root element [response] from the [$result] array;
- line 213: it is indicated that the response is a document named XML encoded as UTF-8;
- lines 217–220: the response is logged as JSON, which is more concise than XML.
The rest of the script remains unchanged. Since all responses pass through the [sendResponse] function, simply modifying this function ensures that all responses—including error responses—are XML documents.
Test
Let’s request the URL, [http://localhost/php8-oct-2026/scripts-web/tax/version-11/tax-server.php?married=yes&children=2&salary=60000], and [curl]:
| > curl -i -u admin:admin "http://localhost/php8-oct-2026/scripts-web/tax/version-11/tax-server.php?married=yes&children=2&salary=60000"
HTTP/1.0 200 OK
Date: Fri, 02 Oct 2026 12:29:50 GMT
Server: Apache/2.4.58 (Ubuntu)
X-Powered-By: PHP/8.5.11
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: PHPSESSID=cdcb082d1342f5b0a273f76a30f92fc6; path=/
Cache-Control: no-cache, private
Vary: Accept-Encoding
Connection: close
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<response>
<tax>3375</tax>
<surcharge>0</surcharge>
<discount>0</discount>
<reduction>0</reduction>
<rate>0.14</rate>
</response>
|
- line 7: the session cookie sent by the server in the first request;
- line 11: the type of document sent;
- Lines 13–20: The document XML from the response.
In a browser, after authentication, the following result is obtained:

Error responses are also XML documents. A request with incorrect parameters:
| > curl -u admin:admin "http://localhost/php8-oct-2026/scripts-web/tax/version-11/tax-server.php?married=x&children=2"
<?xml version="1.0" encoding="UTF-8"?>
<response>
<errors>
<item>GET method required with only the parameters [married, children, salary]</item>
<item>invalid married parameter [x]</item>
<item>missing salary parameter</item>
</errors>
</response>
|
A request from an unknown user:
| > curl -u x:x "http://localhost/php8-oct-2026/scripts-web/tax/version-11/tax-server.php"
<?xml version="1.0" encoding="UTF-8"?>
<response>
<error>Authentication failed [x]</error>
</response>
|
22.2. The Client
We will now focus on the client side of the application.

This architecture will be implemented by the following files:
| scripts-console/tax/version-11
├── Dao
│ └── ClientDao.php
├── Data
│ ├── config-client-ko.json
│ ├── config-client.json
│ ├── errors.json
│ ├── results.json
│ └── taxpayersdata.json
├── Main
│ └── MainTaxClient.php
└── Tests
└── ClientBusinessTest.php
|
In the new version, the only changes are:
- the configuration file [config-client.json];
- the client layer [dao].
The configuration file [config-client.json] becomes the following:
| {
"urlServer": "http://localhost/php8-oct-2026/scripts-web/tax/version-11/tax-server.php",
"user": {
"login": "admin",
"passwd": "admin"
},
"httpClientOptions": {
"verify_peer": false,
"verify_host": false
},
"taxPayersDataFilename": "Data/taxpayersdata.json",
"resultsFilename": "Data/results.json",
"errorsFilename": "Data/errors.json",
"dependencies": [
"../version-05/Entities/TaxException.php",
"../version-05/Entities/TaxPayerData.php",
"../version-05/Utilities/Utilities.php",
"../version-05/Dao/TraitDao.php",
"../version-08/Dao/InterfaceClientDao.php",
"Dao/ClientDao.php",
"../version-08/Business/InterfaceClientBusiness.php",
"../version-08/Business/ClientBusiness.php"
]
}
|
- line 2: the server’s URL from version 11;
- line 20: the new client layer [dao].
22.2.1. The [dao] layer
The [ClientDao] class from version 9 is modified to account for the new response format. [SimpleXML] is used to process this response:
| try {
// we make the request to the server
$response = $this->httpClient->request("GET", $this->urlServer, $options);
// the status, the headers and the XML document of the response, even in case of error (parameter false)
$statusCode = $response->getStatusCode();
$headers = $response->getHeaders(false);
$content = $response->getContent(false);
} catch (ExceptionInterface $ex) {
// the server could not be reached
throw new TaxException("Error communicating with the server ({$ex->getMessage()})", 40, $ex);
}
// logs
// print "$content\n";
// the server sends the XML document <response>...</response>
// the LIBXML_NOERROR and LIBXML_NOWARNING options avoid PHP warnings if the XML is invalid
$xml = simplexml_load_string($content, options: LIBXML_NOERROR | LIBXML_NOWARNING);
if ($xml === false || $xml->getName() !== "response") {
throw new TaxException("Invalid server response [$content]", 41);
}
// error?
if ($statusCode !== 200) {
// we throw an exception whose message is the server's response, converted to JSON (more concise),
// completed by its HTTP status
$serverResponse = json_decode(json_encode($xml, JSON_THROW_ON_ERROR), true, flags: JSON_THROW_ON_ERROR);
$message = json_encode(
["HTTP status" => $statusCode] + $serverResponse,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);
throw new TaxException($message, 42);
}
// we retrieve the session cookie if it is not known yet
if ($this->sessionCookie === null) {
foreach ($headers["set-cookie"] ?? [] as $cookie) {
if (preg_match('/^PHPSESSID=(.+?);/', $cookie, $fields) === 1) {
$this->sessionCookie = "PHPSESSID=$fields[1]";
}
}
}
// we return the response: the array [tax, surcharge, discount, reduction, rate]
// in the XML document, all the values are texts: we convert them into their type
return [
"tax" => (int) $xml->tax,
"surcharge" => (int) $xml->surcharge,
"discount" => (int) $xml->discount,
"reduction" => (int) $xml->reduction,
"rate" => (float) $xml->rate,
];
}
}
|
Comments
- line 63: The server’s response is read. It is a XML [<response>…</response>] document;
- line 72: the [simplexml_load_string] function parses the received XML document and returns a [SimpleXMLElement] object representing its root element, or [false] if the document is not a valid XML document. By default, the [libxml] library used by [SimpleXML] also reports document errors as PHP warnings: the options [LIBXML_NOERROR] and [LIBXML_NOWARNING] suppress them. They are passed via the argument named [options], which avoids having to write the parameter [class_name] that precedes it in the function signature;
- line 73: the method [getName] returns the name of the root element, which must be [response];
- Lines 77–85: In the event of an error, the exception message will be the string JSON from the server’s response rather than the received string XML, because the string JSON is more concise. To obtain the associative array for the response XML, we use a classic trick: the function [json_encode] can convert an object [SimpleXMLElement] into JSON, and [json_decode] then converts this JSON into an array. The exception message thus has the same format as in previous versions;
- lines 97–103: the results array is returned. The child elements of the [response] element are accessed as properties of the [$xml] object ([$xml->tax]). In a XML document, all values are text: unlike JSON, XML does not distinguish between integers, real numbers, and character strings. Each value is therefore converted to the type expected by the [business] layer using the [(int)] and [(float)] operators. The PHP 7 client returned the values as strings.
Test
If you run the client in a properly configured environment (database, authentication, logs, Redis), you get the usual results:
| > php MainTaxClient.php
The results have been saved in the file [Data/results.json]
Done
|
Check the files in the [taxpayersdata.json, results.json, errors.json] folder: the [results.json] file is identical to that of previous versions. On the server side, the logs are as follows:
| 01/10/26 18:12:15.370 : ---new request
01/10/26 18:12:15.370 : Authentication in progress…
01/10/26 18:12:15.370 : Authentication successful [admin]
01/10/26 18:12:15.370 : valid parameters ['married'=>yes, 'children'=>2, 'salary'=>55555]
01/10/26 18:12:15.371 : tax data taken from the database
01/10/26 18:12:15.373 : {"response":{"tax":2814,"surcharge":0,"discount":0,"reduction":0,"rate":0.14}}
01/10/26 18:12:15.376 : ---new request
01/10/26 18:12:15.376 : Authentication taken from the session…
01/10/26 18:12:15.376 : valid parameters ['married'=>yes, 'children'=>2, 'salary'=>50000]
01/10/26 18:12:15.377 : tax data taken from Redis
01/10/26 18:12:15.377 : {"response":{"tax":1384,"surcharge":0,"discount":384,"reduction":347,"rate":0.14}}
…
|
With the configuration file [config-client-ko.json] (unknown user), the output is:
| > php MainTaxClient.php config-client-ko.json
Error no. 42: {"HTTP status":401,"error":"Authentication failed [x]"}
Done
|
The error message has the same format as in previous versions, while the server responded with a document named XML.
22.2.2. [PHPUnit] Tests
The [Tests/ClientBusinessTest.php] test is for version 10, while the test environment is that of the version 11 client. The results are as follows:
| > php vendor/bin/phpunit scripts-console/tax/version-11/Tests --testdox
PHPUnit 12.5.37 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.11
Configuration: .../php8-oct-2026/phpunit.xml
........... 11 / 11 (100%)
Time: 00:00.073, Memory: 16.00 MB
Client Business (Application\Tests\ClientBusiness)
✔ Calculate tax with data set "couple, 2 children, 55555"
✔ Calculate tax with data set "couple, 2 children, 50000"
✔ Calculate tax with data set "couple, 3 children, 50000"
✔ Calculate tax with data set "single, 2 children, 100000"
✔ Calculate tax with data set "single, 3 children, 100000"
✔ Calculate tax with data set "couple, 3 children, 100000"
✔ Calculate tax with data set "couple, 5 children, 100000"
✔ Calculate tax with data set "single, 0 children, 100000"
✔ Calculate tax with data set "couple, 2 children, 30000"
✔ Calculate tax with data set "single, 0 children, 200000"
✔ Calculate tax with data set "couple, 3 children, 200000"
OK (11 tests, 22 assertions)
|
The tests pass with the [assertSame] validation: the values returned by the [dao] layer are indeed of the expected types, thanks to the conversions performed after reading the XML document.