22. Exercício prático – versão 11
Ainda é frequente que os serviços web enviem a sua resposta sob a forma de um fluxo XML, em vez de um fluxo jSON:
- o fluxo jSON é mais leve, mas é necessário um manual de instruções para o compreender;
- o fluxo XML é mais detalhado, mas é autodocumentado. A sua compreensão é imediata;
Alteramos a versão 11 do cliente/servidor para que o servidor passe a enviar um fluxo XML como resposta aos seus clientes:

22.1. O servidor

Esta arquitetura será implementada pelos seguintes scripts:

22.1.1. A classe [Utilitaires]
Retomamos a classe [Utilitaires] utilizada desde a versão 03 (ver parágrafo «ligação»):
<?php
// espaço de nomes
namespace Application;
// uma classe de funções utilitárias
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 {
// analisam-se os atributos da tabela
foreach ($arrayOfAttributes as $attribute => $value) {
// o atributo é numérico?
if (is_numeric($attribute)) {
// caso do índice da matriz (mas também outros casos)
$attribute = 'i' . $attribute;
}
// $value é um array?
if (is_array($value)) {
// Vamos explorar, por sua vez, a matriz [$value]
// adicionamos um nó ao grafo XML
$subnode = $node->addChild($attribute);
// chamada recursiva para explorar o tabuleiro [$value]
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// adiciona-se o nó ao grafo XML
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
Comentários
- linhas 14-36: introduzimos o método estático [getXmlForArrayOfAttributes], que devolve a cadeia XML de um tabuleiro [arrayOfAttributes] passado como parâmetro. O segundo parâmetro é a referência de um nó de um grafo XML, do tipo [SimpleXmlElement]. Após a execução, este nó contém o grafo XML da matriz [arrayOfAttributes];
Escrevemos o seguinte teste [testXml.php]:

<?php
// dependência
require __DIR__ . "/Utilitaires.php";
// tabela associativa
$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();
Ao executar este script [2], obtemos o seguinte num navegador Chrome:

22.1.2. O script do servidor
O script do servidor [impots-server.php] deve ser alterado, bem como o seu ficheiro de configuração [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"
}
Comentários
- a raiz do projeto é agora a pasta da versão 11;
- linha 16: inclui-se a nova classe [Utilitaires];
As alterações no script do servidor são as seguintes:
<?php
// respeito rigoroso pelos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
…
// preparação da resposta JSON do servidor
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// criação da camada [métier]
$métier = new ServerMetier($dao);
// cálculo do imposto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// envio da resposta
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// fim
exit;
function doInternalServerError(string $message, Response $response, array $infos,
…
}
// função de envio da resposta HTTP ao cliente
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: resposta HTTP
// $result: tabela de resultados
// $statusCode: estado HTTP da resposta
// $headers: cabeçalhos HTTP a incluir na resposta
// $logger: o logger da aplicação
// $predisClient: um cliente [predis]
//
// estado HTTTP
$response->setStatusCode($statusCode);
// corpo XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// cabeçalhos
$response->headers->add($headers);
// envio
$response->send();
// registo
if ($logger != NULL) {
// registo em jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// encerramento da ligação [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Comentários
- linha 12: indica-se que a resposta é do tipo [application/xml];
- linhas 29-59: a resposta do servidor é agora do tipo XML;
- linha 41: criação do nó raiz [<réponse></réponse>] do grafo XML;
- linha 42: este gráfico é completado com o gráfico XML da tabela [$result] dos resultados a enviar ao cliente;
- linha 43: o gráfico XML é convertido na cadeia XML para envio ao cliente;
Teste
Diretamente num navegador Chrome, digita-se o URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]. Obtém-se o seguinte resultado [1] num navegador Chrome:

22.2. O cliente
Vamos agora centrar-nos na parte do cliente da aplicação.

Esta arquitetura será implementada pelos seguintes scripts:

Na nova versão, apenas se alteram:
- o ficheiro de configuração [config-client.json];
- a camada [dao] do cliente;
O ficheiro de configuração [config-client.json] passa a ter o seguinte conteúdo:
{
"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. A camada [dao]
O cliente [ClientDao.php] (linha 13 acima) é alterado para ter em conta o novo formato da resposta. Utiliza-se [simpleXML] para processar esta resposta:
<?php
namespace Application;
// dependências
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// utilização de um Trait
use TraitDao;
// atributos
private $urlServer;
private $user;
private $sessionCookie;
// construtor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// cálculo do imposto
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// recuperação da resposta XML
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// registos
// imprimir "$réponse\n";
// obtém-se o estado da resposta
$statusCode = $response->getStatusCode();
// erro?
if ($statusCode !== 200) {
// ocorreu um erro - lança-se uma exceção
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// recuperamos o cookie de sessão
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// cookie de sessão?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// retornamos a resposta na forma de uma tabela
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
Comentários
- linhas 26-27: a resposta do servidor é lida. Trata-se de um documento XML [<réponse>…</réponse>]. É criado um objeto [SimpleXMLElement] a partir do documento XML recebido;
- linhas 33-37: em caso de erro, a mensagem de exceção será a cadeia jSON da resposta do servidor, em vez da cadeia XML recolhida. Com efeito, a cadeia jSON é mais concisa;
- linha 53: a tabela de resultados é devolvida em duas etapas:
- o objeto [$xml], do tipo [\SimpleXMLElement], é convertido em jSON;
- a cadeia jSON obtida é transformada numa tabela associativa. Este é o resultado a apresentar;
Teste
Se o cliente for executado num ambiente correto (base de dados, autenticação, registos), obtêm-se os resultados habituais (verifique os ficheiros [taxpayersdata.json, results.txt, errors.json]). Do lado do servidor, os registos são os seguintes:
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. Testes [Codeception]

O teste [ClientMetierTest] é o seguinte:
<?php
// respeito rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// definição de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11");
// caminho do ficheiro de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// recuperar a configuração
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// classe de teste
class ClientMetierTest extends Unit {
// camada de negócio
private $métier;
public function __construct() {
parent::__construct();
// recuperação da configuração
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// criação da camada [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// criação da camada [métier]
$this->métier = new ClientMetier($clientDao);
}
// testes
…
}
Os resultados do teste são os seguintes:
