22. Ejercicio práctico – versión 11
Todavía es común que los servicios web envíen su respuesta en forma de un flujo XML en lugar de un flujo jSON:
- el flujo jSON es más ligero, pero se necesita una guía de uso para entenderlo;
- el flujo XML es más detallado, pero se explica por sí mismo. Se entiende de inmediato;
Modificamos la versión 11 del cliente/servidor para que el servidor envíe ahora un flujo XML como respuesta a sus clientes:

22.1. El servidor

Esta arquitectura se implementará mediante los siguientes scripts:

22.1.1. La clase [Utilitaires]
Retomamos la clase [Utilitaires] utilizada desde la versión 03 (véase el párrafo del enlace):
<?php
// espacio de nombres
namespace Application;
// una clase de funciones de utilidad
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
…
}
// de https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// se analizan los atributos de la tabla
foreach ($arrayOfAttributes as $attribute => $value) {
// ¿El atributo es numérico?
if (is_numeric($attribute)) {
// caso del índice de la matriz (pero también otros casos)
$attribute = 'i' . $attribute;
}
// ¿Es $value un array?
if (is_array($value)) {
// Ahora exploraremos el array [$value]
// Agregamos un nodo al grafo XML
$subnode = $node->addChild($attribute);
// llamada recursiva para explorar el array [$value]
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// se agrega el nodo al grafo XML
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
Comentarios
- líneas 14-36: introducimos el método estático [getXmlForArrayOfAttributes], que devuelve la cadena XML de un array [arrayOfAttributes] pasado como parámetro. El segundo parámetro es la referencia de un nodo de un grafo XML, de tipo [SimpleXmlElement]. Tras la ejecución, este nodo contiene el grafo XML del array [arrayOfAttributes];
Escribimos la prueba [testXml.php] de la siguiente manera:

<?php
// dependencia
require __DIR__ . "/Utilitaires.php";
// tabla asociativa
$array = ["nom" => "amédée", "prénom" => "sylvain", "âge" => 40,
"enfants" => [["nom" => "amédée", "prénom" => "béatrice", "âge" => 6],
["nom" => "amédée", "prénom" => "bertrand", "âge" => 4]]];
// XML
header("Content-Type: application/xml");
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><root></root>");
\Application\Utilitaires::getXmlForArrayOfAttributes($array, $node);
print $node->asXML();
Al ejecutar este script [2], obtenemos lo siguiente en un navegador Chrome:

22.1.2. El script del servidor
El script de servidor [impots-server.php] debe modificarse, así como su archivo de configuración [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/Métier/InterfaceServerMetier.php",
"/../version-08/Métier/ServerMetier.php",
"/../version-09/Utilities/Logger.php",
"/../version-09/Utilities/SendAdminMail.php",
"/Utilities/Utilitaires.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": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Data/logs.txt"
}
Comentarios
- la raíz del proyecto ahora es la carpeta de la versión 11;
- línea 16: se incluye la nueva clase [Utilitaires];
Los cambios en el script del servidor son los siguientes:
<?php
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
…
// preparación de la respuesta JSON del servidor
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// creación de la capa [métier]
$métier = new ServerMetier($dao);
// cálculo del impuesto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// se envía la respuesta
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// fin
exit;
function doInternalServerError(string $message, Response $response, array $infos,
…
}
// función para enviar la respuesta HTTP al cliente
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: respuesta HTTP
// $result: tabla de resultados
// $statusCode: estado HTTP de la respuesta
// $headers: encabezados HTTP que deben incluirse en la respuesta
// $logger: el registrador de la aplicación
// $predisClient: un cliente [predis]
//
// estado HTTTP
$response->setStatusCode($statusCode);
// cuerpo XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// encabezados
$response->headers->add($headers);
// envío
$response->send();
// registro
if ($logger != NULL) {
// registro en jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// cierre de la conexión [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Comentarios
- línea 12: se indica que la respuesta es de tipo [application/xml];
- líneas 29-59: la respuesta del servidor ahora es XML;
- línea 41: se crea el nodo raíz [<réponse></réponse>] del grafo XML;
- línea 42: este grafo se complementa con el grafo XML de la tabla [$result] de los resultados que se enviarán al cliente;
- línea 43: el grafo XML se convierte en la cadena XML para su envío al cliente;
Prueba
Directamente en un navegador Chrome, se ingresa URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]. Se obtiene el siguiente resultado [1] en un navegador Chrome:

22.2. El cliente
Ahora nos enfocamos en la parte del cliente de la aplicación.

Esta arquitectura se implementará mediante los siguientes scripts:

En la nueva versión, solo cambian:
- el archivo de configuración [config-client.json];
- la capa [dao] del cliente;
El archivo de configuración [config-client.json] queda así:
{
"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/ExceptionImpots.php",
"/../version-08/Utilities/Utilitaires.php",
"/../version-08/Dao/InterfaceClientDao.php",
"/../version-08/Dao/TraitDao.php",
"/Dao/ClientDao.php",
"/../version-08/Métier/InterfaceClientMetier.php",
"/../version-08/Métier/ClientMetier.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. La capa [dao]
El cliente [ClientDao.php] (línea 13 anterior) se modifica para tener en cuenta el nuevo formato de la respuesta. Se utiliza [simpleXML] para procesarla:
<?php
namespace Application;
// dependencias
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// uso de un Trait
use TraitDao;
// atributos
private $urlServer;
private $user;
private $sessionCookie;
// constructor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// cálculo del impuesto
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// se obtiene la respuesta XML
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// registros
// imprimir "$réponse\n";
// se obtiene el estado de la respuesta
$statusCode = $response->getStatusCode();
// ¿Error?
if ($statusCode !== 200) {
// hay un error; se lanza una excepción
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// se recupera la cookie de sesión
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// ¿Cookie de sesión?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// se devuelve la respuesta en forma de matriz
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
Comentarios
- líneas 26-27: se lee la respuesta del servidor. Se trata de un documento XML [<réponse>…</réponse>]. Se crea un objeto [SimpleXMLElement] a partir del documento XML recibido;
- líneas 33-37: en caso de error, el mensaje de la excepción será la cadena jSON de la respuesta del servidor, en lugar de la cadena XML recopilada. De hecho, la cadena jSON es más concisa;
- línea 53: se devuelve la tabla de resultados en dos pasos:
- el objeto [$xml], de tipo [\SimpleXMLElement], se convierte en jSON;
- se transforma la cadena jSON obtenida en una tabla asociativa. Este es el resultado que se debe devolver;
Prueba
Si se ejecuta el cliente con un entorno adecuado (base de datos, autenticación, registros), se obtienen los resultados habituales (verifique los archivos [taxpayersdata.json, results.txt, errors.json]). Por parte del servidor, los registros son los siguientes:
06/07/19 07:41:32:877 :
---nouvelle requête
06/07/19 07:41:32:882 : Autentification en cours…
06/07/19 07:41:32:883 : Authentification réussie [admin, admin]
06/07/19 07:41:32:883 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
06/07/19 07:41:32:908 : données fiscales prises en base de données
06/07/19 07:41:32:959 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:070 :
---nouvelle requête
06/07/19 07:41:33:077 : Authentification prise en session…
06/07/19 07:41:33:077 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
06/07/19 07:41:33:099 : données fiscales prises dans redis
06/07/19 07:41:33:100 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
06/07/19 07:41:33:189 :
---nouvelle requête
06/07/19 07:41:33:202 : Authentification prise en session…
06/07/19 07:41:33:202 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
06/07/19 07:41:33:233 : données fiscales prises dans redis
06/07/19 07:41:33:233 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:318 :
…
22.2.2. Pruebas [Codeception]

La prueba [ClientMetierTest] es la siguiente:
<?php
// Se respetan estrictamente los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// definición de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11");
// ruta del archivo de configuración
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// se recupera la configuración
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// clase de prueba
class ClientMetierTest extends Unit {
// capa de negocio
private $métier;
public function __construct() {
parent::__construct();
// se recupera la configuración
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creación de la capa [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creación de la capa [métier]
$this->métier = new ClientMetier($clientDao);
}
// pruebas
…
}
Los resultados de la prueba son los siguientes:
