Skip to content

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:

Image

22.1. The server

Image

This architecture will be implemented by the following scripts:

Image

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]:

Image


<?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:

Image

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:

Image

22.2. The client

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

Image

This architecture will be implemented by the following scripts:

Image

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]

Image

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:

Image