22. Application Exercise – Version 11
It is still common for web services to send their response as an XML feed rather than a JSON feed:
- the JSON stream is lighter, but you need a user guide to understand it;
- the XML feed is more verbose but is self-documenting. It is immediately understandable;
We modify the client/server version 11 so that the server now sends an XML feed as a response to its clients:

22.1. The server

This architecture will be implemented by the following scripts:

22.1.1. The [Utilities] class
We are reusing the [Utilities] class used since version 03 (see linked paragraph):
<?php
// namespace
namespace Application;
// a class of utility functions
abstract class Utilities {
public static function cutNewLinechar(string $line): string {
…
}
// from https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// scan the attributes of the array
foreach ($arrayOfAttributes as $attribute => $value) {
// Is the attribute numeric?
if (is_numeric($attribute)) {
// case of the array index (but also other cases)
$attribute = 'i' . $attribute;
}
// Is $value an array?
if (is_array($value)) {
// we will explore the array [$value] in turn
// we add a node to the XML graph
$subnode = $node->addChild($attribute);
// Recursive call to traverse the array [$value]
Utilities::getXmlForArrayOfAttributes($value, $subnode);
} else {
// add the node to the XML tree
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
Comments
- lines 14–36: we introduce the static method [getXmlForArrayOfAttributes], which returns the XML string of an array [arrayOfAttributes] passed as a parameter. The second parameter is the reference to an XML graph node of type [SimpleXmlElement]. After execution, this node contains the XML tree of the [arrayOfAttributes] array;
We write the following test [testXml.php]:

<?php
// dependency
require __DIR__ . "/Utilities.php";
// associative array
$array = ["last_name" => "Amédée", "first_name" => "Sylvain", "age" => 40,
"children" => [["last_name" => "Amédée", "first_name" => "Béatrice", "age" => 6],
["last_name" => "Amédée", "first_name" => "Bertrand", "age" => 4]]];
// xml
header("Content-Type: application/xml");
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><root></root>");
\Application\Utilities::getXmlForArrayOfAttributes($array, $node);
print $node->asXML();
When we run this script [2], we get the following in a Chrome browser:

22.1.2. The server script
The server script [impots-server.php] must be modified, as well as its configuration file [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-11",
"databaseFilename": "Data/database.json",
"relativeDependencies": [
"/../version-08/Entities/BaseEntity.php",
"/../version-08/Entities/ExceptionImpots.php",
"/../version-08/Entities/TaxAdminData.php",
"/../version-08/Entities/Database.php",
"/../version-08/Dao/InterfaceServerDao.php",
"/../version-08/Dao/ServerDao.php",
"/../version-09/Dao/ServerDaoWithSession.php",
"/../version-08/Business/BusinessServerInterface.php",
"/../version-08/Business/ServerBusiness.php",
"/../version-09/Utilities/Logger.php",
"/../version-09/Utilities/SendAdminMail.php",
"/Utilities/Utilities.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
"users": [
{
"login": "admin",
"passwd": "admin"
}
],
"adminMail": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "Tax calculation server crash",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Data/logs.txt"
}
Comments
- The project root is now the version 11 folder;
- line 16: the new [Utilities] class is included;
The changes to the server script are as follows:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
…
// Prepare the server's JSON response
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// Create the [business] layer
$business = new ServerBusiness($dao);
// Calculate tax
$result = $businessLayer->calculateTax($married, (int) $children, (int) $salary);
// return the response
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
function doInternalServerError(string $message, Response $response, array $infos,
…
}
// function to send the HTTP response to the client
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: HTTP response
// $result: array of results
// $statusCode: HTTP status of the response
// $headers: HTTP headers to include in the response
// $logger: the application logger
// $predisClient: a [predis] client
//
// HTTP status
$response->setStatusCode($statusCode);
// XML body
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><response></response>");
Utilities::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// headers
$response->headers->add($headers);
// send
$response->send();
// log
if ($logger != NULL) {
// log in JSON
$log = \json_encode(["response" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// Close the [redis] connection
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Comments
- line 12: specifies that the response is of type [application/xml];
- lines 29–59: the server response is now XML;
- line 41: creation of the root node [<response></response>] of the XML graph;
- line 42: this tree is populated with the XML tree from the [$result] array of results to be sent to the client;
- line 43: the XML tree is converted to an XML string for sending to the client;
Test
Directly in a Chrome browser, enter the URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]. The following result [1] is displayed in a Chrome browser:

22.2. The client
We will now focus on the client-side of the application.

This architecture will be implemented by the following scripts:

In the new version, the only changes are:
- the configuration file [config-client.json];
- the client's [dao] layer;
The configuration file [config-client.json] becomes the following:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"/../version-08/Entities/BaseEntity.php",
"/../version-08/Entities/TaxPayerData.php",
"/../version-08/Entities/TaxExceptions.php",
"/../version-08/Utilities/Utilitaires.php",
"/../version-08/Dao/InterfaceClientDao.php",
"/../version-08/Dao/DaoProcess.php",
"/Dao/ClientDao.php",
"/../version-08/Business/BusinessClientInterface.php",
"/../version-08/Business/BusinessClient.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-11/impots-server.php"
}
22.2.1. The [dao] layer
The client [ClientDao.php] (line 13 above) is modified to account for the new response format. We use [simpleXML] to process it:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// Using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
private $sessionCookie;
// constructor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// Calculate tax
public function calculateTax(string $married, int $children, int $salary): array {
…
// retrieve the XML response
$response = $response->getContent(false);
$xml = new \SimpleXMLElement($response);
// logs
// print "$response\n";
// retrieve the response status
$statusCode = $response->getStatusCode();
// error?
if ($statusCode !== 200) {
// there is an error - throw an exception
$message = \json_encode(["HTTP status" => $statusCode, "response" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// retrieve the session cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// Session cookie?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $fields);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $fields[1];
}
}
}
}
// Return the response as an array
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
Comments
- lines 26-27: the server’s response is read. It is an XML document [<response>…</response>]. A [SimpleXMLElement] object is constructed from the received XML document;
- lines 33-37: in case of an error, the exception message will be the JSON string of the server response rather than the XML string. This is because the JSON string is more concise;
- line 53: the results array is returned in two steps:
- the [$xml] object of type [\SimpleXMLElement] is converted to JSON;
- We convert the resulting JSON string into an associative array. This is the result to be returned;
Test
If we run the client in a proper environment (database, authentication, logs), we get the usual results (check the files [taxpayersdata.json, results.txt, errors.json]). On the server side, the logs are as follows:
07/06/19 07:41:32:877 :
---new request
07/06/19 07:41:32:882: Authentication in progress…
07/06/19 07:41:32:883: Authentication successful [admin, admin]
07/06/19 07:41:32:883: parameters ['married'=>yes, 'children'=>2, 'salary'=>55555] valid
07/06/19 07:41:32:908: Tax data retrieved from database
07/06/19 07:41:32:959: {"response":{"tax":2814,"surcharge":0,"discount":0,"reduction":0,"rate":0.14}}
07/06/19 07:41:33:070 :
---new request
07/06/19 07:41:33:077 : Authentication saved in session…
07/06/19 07:41:33:077 : Valid parameters ['married'=>yes, 'children'=>2, 'salary'=>50000]
07/06/19 07:41:33:099 : tax data retrieved from Redis
07/06/19 07:41:33:100 : {"response":{"tax":1384,"surcharge":0,"discount":384,"reduction":347,"rate":0.14}}
07/06/19 07:41:33:189 :
---new request
07/06/19 07:41:33:202 : Authentication saved in session…
07/06/19 07:41:33:202 : valid parameters ['married'=>yes, 'children'=>3, 'salary'=>50000]
07/06/19 07:41:33:233 : Tax data retrieved from Redis
07/06/19 07:41:33:233 : {"response":{"tax":0,"surcharge":0,"discount":720,"reduction":0,"rate":0.14}}
07/06/19 07:41:33:318 :
…
22.2.2. Tests [Codeception]

The [ClientMetierTest] test is as follows:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11");
// path to the configuration file
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// test class
class ClientMetierTest extends Unit {
// business layer
private $business;
public function __construct() {
parent::__construct();
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// create the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// Create the [business] layer
$this->business = new ClientBusiness($clientDao);
}
// tests
…
}
The test results are as follows:
