23. Exercício prático – versão 12
Neste capítulo, vamos escrever uma aplicação web que siga a arquitetura MVC (Modelo-Visão-Controlador). A aplicação poderá fornecer suas respostas em três formatos: jSON, XML, HTML. Há um salto de complexidade entre o que faremos agora e o que foi feito anteriormente. Reutilizaremos a maioria dos conceitos abordados até agora e detalharemos todas as etapas que levam à aplicação final.
23.1. Arquitetura MVC
Vamos implementar o modelo de arquitetura conhecido como MVC (Modelo – Visão – Controlador) da seguinte maneira:

O processamento de uma solicitação de um cliente ocorrerá da seguinte maneira:
- 1 - solicitação
As solicitações URL terão o formato http://machine:port/contexte/….?action=uneAction¶m1=v1¶m2=v2&… O [Contrôleur principal] utilizará um arquivo de configuração para “encaminhar” a solicitação ao controlador correto e à ação correta dentro desse controlador. Para isso, utilizará o campo [action] do URL. O restante do URL [param1=v1¶m2=v2&…] é composto por parâmetros opcionais que serão transmitidos à ação. O C de MVC é, neste caso, a sequência [Contrôleur principal, Contrôleur / Action]. Se nenhum controlador puder processar a ação solicitada, o servidor web responderá que a ação URL solicitada não foi encontrada.
- 2 - processamento
- a ação selecionada [2a] pode utilizar os parâmetros parami que a ação [Contrôleur principal] lhe transmitiu. Esses parâmetros podem provir de várias fontes:
- do caminho [/param1/param2/…] do URL,
- dos parâmetros [param1=v1¶m2=v2] do URL,
- dos parâmetros enviados pelo navegador junto com sua solicitação;
- No processamento da solicitação do usuário, a ação pode precisar da camada [métier] [2b]. Uma vez processada a solicitação do cliente, ela pode gerar diversas respostas. Um exemplo clássico é:
- uma resposta de erro, caso a solicitação não tenha sido processada corretamente;
- uma resposta de confirmação, caso contrário;
- o [Contrôleur / Action] enviará sua resposta [2c] ao controlador principal, juntamente com um código de estado. Esses códigos de estado representarão de forma exclusiva o estado em que a aplicação se encontra. Serão códigos de sucesso ou códigos de erro;
- a ação selecionada [2a] pode utilizar os parâmetros parami que a ação [Contrôleur principal] lhe transmitiu. Esses parâmetros podem provir de várias fontes:
- 3 - resposta
- dependendo se o cliente solicitou uma resposta jSON, XML ou HTML, o [Contrôleur principal] instanciará o [3a], o tipo de resposta apropriado, e solicitará que este envie a resposta ao cliente. O [Contrôleur principal] transmitirá a ele tanto a resposta quanto o código de status fornecidos pelo [Contrôleur / Action] que foi executado;
- se a resposta desejada for do tipo jSON ou XML, a resposta selecionada formatará a resposta do [Contrôleur / Action] que lhe foi fornecida e a enviará ao [3c]. O cliente capaz de processar essa resposta pode ser um script de console PHP ou um script JavaScript hospedado em uma página HTML;
- se a resposta desejada for do tipo HTML, a resposta selecionada escolherá [3b] uma das visualizações HTML [Vuei] por meio do código de estado que lhe foi fornecido. Esse é o V de MVC. A cada código de estado corresponde uma única visualização. Essa visualização V exibirá a resposta do [Contrôleur / Action] que foi executado. Ela apresenta os dados dessa resposta utilizando HTML, CSS e JavaScript. Esses dados são chamados de modelo da vista. É o M de MVC. O cliente, na maioria das vezes, é um navegador;
Agora, vamos esclarecer a relação entre a arquitetura web MVC e a arquitetura em camadas. Dependendo da definição que se dá ao modelo, esses dois conceitos podem estar ou não relacionados. Consideremos uma aplicação web MVC de camada única:

No exemplo acima, cada um dos [Contrôleur / Action] integra uma parte das camadas [métier] e [dao]. Na camada [web], existe de fato uma arquitetura MVC, mas o conjunto da aplicação não possui uma arquitetura em camadas. Aqui, há apenas uma camada que realiza todas as funções.
Agora, vamos considerar uma arquitetura web multicamadas:

A camada [web] pode ser implementada sem seguir o modelo MVC. Temos, portanto, uma arquitetura multicamadas, mas a camada web não implementa o modelo MVC.
Por exemplo, no mundo .NET, a camada [web] acimaacima pode ser implementada com ASP.NET e MVC, e temos, então, uma arquitetura em camadas com uma camada [web] do tipo MVC. Feito isso, é possível substituir essa camada ASP.NET MVC por uma camada ASP.NET clássica (WebForms), mantendo o restante (negócio, DAO, Piloto) inalterado. Temos, então, uma arquitetura em camadas com uma camada [web] que não é mais do tipo MVC.
Em MVC, afirmamos que o modelo M era o da vista V, c.a.d, ou seja, o conjunto de dados exibidos pela vista V. É fornecida outra definição do modelo M de MVC:

Muitos autores consideram que o que está à direita da camada [web] forma o modelo M do MVC. Para evitar ambiguidades, pode-se referir-se:
- do modelo do domínio, ao se referir a tudo o que está à direita da camada [web];
- do modelo da visualização, quando se refere aos dados exibidos por uma visualização V;
23.2. Estrutura do projeto NetBeans
Para o projeto NetBeans, adotaremos uma arquitetura que reflita o modelo MVC:

- [3]: [main.php] é o controlador principal do nosso modelo MVC. É o C de MVC;
- [4]: a pasta [Controllers] conterá os controladores secundários. Cada um deles processa uma ação específica. Essa ação é indicada no URL, por exemplo, […/main.php?action=authentifier-utilisateur]. Com essa ação, o [Contrôleur principal] [main.php] selecionará um [Contrôleur secondaire], neste caso o [AuthentifierUtilisateurController], para processar a ação solicitada. Esses controladores também fazem parte do C de MVC;
- [5]: a pasta [Model] conterá as camadas [métier] e [dao] do aplicativo. De acordo com os termos adotados anteriormente, esses elementos representam o modelo do domínio e, de acordo com a terminologia adotada para o M, podem representar o M de MVC;
- [6]: a pasta [Responses] contém as classes responsáveis por enviar a resposta ao cliente. Há uma classe para cada tipo de resposta desejada:
- [JsonResponse]: para uma resposta jSON;
- [XmlResponse]: para uma resposta XML;
- [HtmlResponse]: para uma resposta HTML;
- [7]: a pasta [Views] contém as visualizações HTML quando se deseja uma resposta HTML. Esse é o V de MVC. Elas são ativadas pela classe [HtmlResponse], que lhes transmite os dados a serem exibidos. Esses dados constituem o modelo da visualização. De acordo com a terminologia adotada para o M, esses dados podem ser o M de MVC;
- [8]: a pasta [Utilities] contém utilitários:
- [Logger]: a classe que permite gerar logs em um arquivo de texto;
- [Sendmail]: a classe que permite enviar e-mails;
- [9]: a pasta [Logs] contém o arquivo de logs [logs.txt];
- [10]: a pasta [Entities] contém classes utilizadas pelos diversos controladores;
Com base nessa estrutura de diretórios, é possível descrever o fluxo de processamento de uma ação solicitada por um cliente:
- [main.php] [3] recebe a solicitação;
- após realizar algumas verificações preliminares (a ação faz parte das ações aceitas?), ele encaminha a solicitação ao controlador secundário [4], responsável por processar essa ação;
- o controlador secundário realiza o que lhe cabe. Em seu trabalho, ele pode precisar das camadas [métier], [dao] e [5], bem como das entidades do arquivo [10]. Ele envia sua resposta ao controlador principal [main.php], que o ativou;
- dependendo do tipo de resposta [jSON, XML, HTML] solicitado pelo cliente, o controlador principal [main.php] ativa uma das respostas da pasta [Responses] [6];
- as respostas [JsonResponse, XmlResponse] enviam, respectivamente, a resposta jSON ou XML ao cliente;
- a resposta [HtmlResponse] utiliza uma das visualizações da pasta [Views] [7] para enviar uma resposta HTML ao cliente;
- os diferentes controladores têm acesso à classe [Logger] da pasta [8] para gravar registros no arquivo de logs da pasta [9]. São registrados:
- a ação solicitada;
- a resposta do respectivo controlador. Esta é registrada no formato jSON, independentemente do tipo [jSON, XML, HTML] solicitado;
- em caso de um erro fatal (HTTP_INTERNAL_SERVER_ERROR), o controlador principal [main.php] envia um e-mail ao administrador por meio da classe [SendMail] da pasta [8];
23.3. As ações do aplicativo
O cliente transmite ao servidor web a ação a ser executada na forma de um parâmetro [action] no URL [/main.php?action=xxx]. As ações autorizadas estão listadas no arquivo [config.json], que configura o controlador principal [main.php]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
- linha 1: a chave [actions] do dicionário jSON;
- linhas 3 a 9: um dicionário [action:contrôleur]. A cada ação está associado o controlador secundário responsável por processá-la;
- linha 3: [init-session]: inicia uma sessão de simulações de cálculos de impostos. Essa ação indica o tipo de respostas desejadas [jSON, XML, HTML];
- linha 4: uma vez definido o tipo de sessão, o cliente deverá se autenticar com a ação [authentifier-utilisateur]. Enquanto não estiver identificado, todas as outras ações serão bloqueadas, com exceção da ação [init-session];
- linha 5: uma vez identificado, o cliente poderá realizar uma série de cálculos de impostos com a ação [calculer-impot];
- linha 6: a qualquer momento, o cliente pode solicitar a exibição da lista das simulações que realizou com a ação [lister-simulations];
- linha 7: ele poderá excluir algumas delas com a ação [supprimer-simulation];
- linha 8: o cliente encerra sua sessão de simulações com a ação [fin-session]. A partir desse momento, ele deverá se autenticar novamente caso queira utilizar o aplicativo;
- linha 9: no aplicativo HTML, a ação [afficher-calcul-impot] solicita a exibição do formulário que permite o cálculo do imposto;
23.4. Configuração do aplicativo web
A aplicação é configurada pelo seguinte arquivo jSON [config.json]:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.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": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
Comentários
- linha 2: nome do arquivo jSON que contém a configuração do acesso ao banco de dados;
- linhas 3-39: configuração das dependências do projeto. Aqui são listados todos os scripts PHP da árvore de diretórios do projeto;
- linhas 40-44: o usuário autorizado a utilizar o aplicativo;
- linhas 46-54: os dados de contato por e-mail do administrador do aplicativo;
- linha 55: o caminho do arquivo de logs;
- linhas 56-65: associações [action => contrôleur secondaire chargé de la traiter];
- linhas 66-70: associações [type de réponse => classe Response chargée d’envoyer la réponse au client];
- linhas 71-75: associações [vue HTML => tableau des codes d’état menant à cette vue];
- linha 76: a visualização [vue-erreurs] é exibida em uma sessão HTML sempre que ocorre um erro anormal:
- uma aplicação jSON ou XML é normalmente consultada por meio de um cliente programado. Este envia ao servidor parâmetros que podem estar ausentes ou incorretos. Todos os controladores tratam esses casos e retornam códigos de erro ao cliente. Todos os casos de erro possíveis devem ser tratados;
- com uma aplicação HTML, a situação é um pouco diferente. Em condições normais de uso, a aplicação web utiliza apenas uma parte dos casos de uso possíveis dos clientes jSON e XML. Vejamos um exemplo: a ação [calculer-impot] espera três parâmetros enviados por POST (enviados por um POST): [marié, enfants, salaire].
- Se tivermos um cliente jSON que permita digitar manualmente os URL, é possível solicitar a ação [calculer-impot] com um GET em vez de um POST, ou com um POST sem nenhum parâmetro enviado, embora sejam necessários três, etc… O servidor jSON deve processar todos esses casos;
- Em um aplicativo web, a ação [calculer-impot] será solicitada a partir de um formulário web, onde nenhuma das duas situações anteriores será possível: a ação [calculer-impot] será solicitada com um POST e os três parâmetros [marié, enfants, salaire]. Alguns desses parâmetros podem ter um valor incorreto, mas estarão presentes. No entanto, o usuário pode reproduzir certos erros digitando ele mesmo os URL no navegador. Por segurança, é preciso lidar com esse caso;
- a visualização [vue-erreurs] será exibida sempre que um controlador secundário retornar um código de estado incompatível com a aplicação web, ou seja, um código de estado não presente nas linhas 72 a 74 do arquivo de configuração. Optamos por essa solução por uma questão pedagógica. Outra opção possível seria não fazer nada e simplesmente reexibir a visualização atualmente exibida no navegador do cliente, para que o usuário tenha a impressão de que o servidor não está respondendo às suas URL criadas manualmente;
23.5. Instalação de ferramentas e bibliotecas
23.5.1. Postman
O [Postman] é a ferramenta que nos permitirá consultar as diferentes URL de nossa aplicação web. Ela nos permite:
- utilizar qualquer URL: estas são criadas manualmente;
- enviar solicitações ao servidor web por meio de um GET, POST, PUT, OPTIONS…;
- especificar os parâmetros do GET ou do POST;
- definir os cabeçalhos HTTP da solicitação;
- receber uma resposta nos formatos jSON, XML, HTML,
- ter acesso aos cabeçalhos HTTP da resposta. Assim, temos acesso à resposta completa HTTP do servidor;
Como estamos criando manualmente as consultas URL, poderemos testar todos os casos de erro possíveis e observar como o servidor reage.
[Postman] está disponível no URL [https://www.getpostman.com/downloads/]. A versão disponível em junho de 2019 é a 7.2. Essa versão apresenta uma anomalia: quando se fazem solicitações sucessivas ao servidor web em questão, o cliente [Postman 7.2] não reenvia automaticamente os cookies que o servidor lhe envia, principalmente o cookie de sessão. Para manter a sessão, é necessário, então, copiar manualmente o cookie de sessão nos cabeçalhos HTTP das solicitações sucessivas. Não é muito complicado, mas não é prático. Trata-se de um bug que não existia nas versões anteriores. Ciente do bug, a equipe do [Postman] o corrigiu em uma versão alfa (que pode ser instável) chamada [Postman Canary], disponível no URL [https://www.getpostman.com/downloads/canary]. É essa versão que está sendo usada aqui. Vamos descrever sua instalação. Se uma versão estável [Postman 7.3] ou posterior estiver disponível, você pode baixá-la: o bug provavelmente já terá sido corrigido.
Proceda à instalação da sua versão do [Postman]. Durante a instalação, será solicitado que você crie uma conta: ela não será necessária neste caso. A conta [Postman] serve para sincronizar diferentes dispositivos, de modo que a configuração de um seja replicada em outro. Nada disso é útil neste caso.
Uma vez instalado, o [Postman] apresenta a seguinte interface:

- no [2-3], temos acesso às configurações do produto;

- no [6], a versão utilizada neste documento;
- se você criou uma conta, ocorre uma sincronização entre o seu computador e um servidor remoto [Postman]. Isso é simbolizado pela roda [7] que gira sempre que você faz alterações no projeto [Postman]. Para interromper essa sincronização desnecessária, desconecte-se do [8-9];
23.5.2. A biblioteca Symfony / Serializer
Para serializar objetos em jSON e XML, vamos usar a biblioteca [Symfony / Serializer]. Ela apresenta aqui duas vantagens:
- é consistente em sua utilização para serializar em jSON ou XML: isso evita que os desenvolvedores precisem aprender a usar duas bibliotecas com APIs (Interfaces de Programação de Aplicativos) diferentes;
- por padrão, ela é capaz de serializar objetos em jSON ou XML, mesmo que os atributos desses objetos sejam privados. Lembramos que, em jSON, para serializar um objeto, era necessário que a classe desse objeto implementasse a interface [\JsonSerializable]. O resultado obtido era a string jSON de um array associativo cujas chaves eram os atributos da classe. Ao deserializar essa string jSON, recuperava-se o array associativo primitivo, que precisava então ser transformado em um objeto da classe que havia sido serializada. Com [Symfony / Serializer], a desserialização produz imediatamente um objeto da classe serializada. É mais simples;
A documentação da biblioteca [Symfony / Serializer] está disponível no URL: [https://symfony.com/doc/current/components/serializer.html] (junho de 2019).
Para instalar essa biblioteca, abra um terminal do Laragon (veja o parágrafo com o link) e digite o seguinte comando:

- em [1], o comando de instalação da biblioteca [symfony/serializer];
- em [2], outra biblioteca necessária para o nosso projeto: permite a serialização de objetos;

23.6. As entidades do aplicativo

As entidades [BaseEntity, Database, ExceptionImpots, TaxAdminData] foram utilizadas a partir da versão 08 do serviço web (ver parágrafo com link).
A classe [Simulation] servirá para encapsular os elementos de uma simulação de cálculo de imposto:
<?php
namespace Application;
class Simulation extends BaseEntity {
// atributos de uma simulação de cálculo de imposto
protected $marié;
protected $enfants;
protected $salaire;
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// getters
public function getMarié() {
return $this->marié;
}
public function getEnfants() {
return $this->enfants;
}
public function getSalaire() {
return $this->salaire;
}
public function getImpôt() {
return $this->impôt;
}
public function getSurcôte() {
return $this->surcôte;
}
public function getDécôte() {
return $this->décôte;
}
public function getRéduction() {
return $this->réduction;
}
public function getTaux() {
return $this->taux;
}
}
Comentários
- linha 5: a classe [Simulation] estende a classe [BaseEntity] e, portanto, herda os métodos:
- [setFromArrayOfAttributes($arrayOfAttributes)]: que permite inicializar os atributos da classe;
- [__toString]: que retorna a string jSON do objeto;
- linhas 7-14: os atributos da simulação;
- linhas 16-47: os getters da classe;
23.7. Os utilitários do aplicativo
![]()
A classe [Logger] permite registrar eventos em um arquivo de texto. Essa classe foi descrita no parágrafo “link”.
A classe [SendAdminMail] permite enviar um e-mail ao administrador do aplicativo. Essa classe foi descrita no parágrafo [link].
23.8. As camadas [métier] e [dao]


As classes e interfaces das camadas [métier] e [dao] estão agrupadas na pasta [Model]. Todas elas foram definidas e utilizadas em versões anteriores:
ExceptionImpots | A classe das exceções lançadas pela camada [dao]. Definida no parágrafo “link”. |
InterfaceServerDao | Interface implementada pela camada [dao] do servidor. Definida no parágrafo “link”. |
ServerDao | Implementação da interface [InterfaceServerDao]. Implementa a camada [dao] do servidor. Definida no parágrafo “link”. |
ServerDaoWithSession | Implementação da interface [InterfaceServerDao]. Implementa a camada [dao] do servidor. Definida no parágrafo “link”. |
InterfaceServerMetier | Interface implementada pela camada [métier] do servidor. Definida no parágrafo “link”. |
ServerMetier | Implementação da interface [InterfaceMetier]. Implementa a camada [metier] do servidor. Definida no parágrafo “link”. |
A aplicação em desenvolvimento utiliza muitos elementos já apresentados e utilizados:
- as camadas [métier] e [dao];
- os utilitários [Logger] e [SendAdminMail];
- as entidades [ExceptionImpots, TaxAdminData, Database];
Vamos nos concentrar na camada [web] do aplicativo:

23.9. O controlador principal [main.php]
23.9.1. Introdução

- [1-2]: o controlador principal [main.php] [1] é configurado pelo arquivo [config.json] [2];
Vale lembrar a posição do controlador principal em nossa arquitetura MVC:

No [1], o controlador principal [main.php] é o primeiro elemento da arquitetura MVC a processar a solicitação do cliente. Ele desempenha várias funções:
- primeiramente, ele realiza as verificações básicas:
- se seu arquivo de configuração existe e é válido;
- carrega todas as dependências do projeto. Isso equivale a carregar todos os elementos da arquitetura MVC;
- a ação solicitada foi especificada? Se sim, ela é válida?
- se a ação solicitada for válida, selecionar [2a] o controlador secundário que irá processá-la e passar a ele as informações necessárias: a solicitação HTTP, a sessão, a configuração do aplicativo;
- recuperar [2c] a resposta do controlador secundário. De acordo com o tipo (jSON, XML, HTML) do aplicativo solicitado pelo cliente, selecionar [3a] a resposta (JsonResponse, XmlResponse, HtmlResponse) encarregada de enviar a resposta ao cliente e repassar a ele todas as informações necessárias (a solicitação HTTP, a sessão, a configuração do aplicativo, a resposta do controlador secundário);
- uma vez enviada essa resposta ([3c]), proceder à liberação dos recursos que possam ter sido mobilizados para o processamento da solicitação;
23.9.2. [main.php] - 1
O código do controlador principal [main.php] é o seguinte:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestão de erros por PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// recuperando a configuração
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// erro?
if (!$fileContents) {
// registra-se o erro
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// recuperamos o código JSON do arquivo de configuração em um array associativo
$config = \json_decode($fileContents, true);
// erro?
if (!$config) {
// registra-se o erro
$erreur = TRUE;
$état = 132;
$message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
}
}
// erro?
if ($erreur) {
// preparação da resposta JSON do servidor
// não é possível utilizar o arquivo de configuração
// dependências do Symfony
require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
// preparação da resposta
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// código de status
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
// conteúdo
$response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
// envio
$response->send();
// fim
exit;
}
…
Comentários
- linhas 10-12: o controlador principal utiliza os seguintes objetos do Symfony:
- [Request]: a solicitação HTTP em processamento;
- [Session]: a sessão do aplicativo web;
- [Response]: a resposta HTTP para o cliente;
- linha 15: durante todo o desenvolvimento, manteremos esta linha comentada: os erros PHP são, então, incorporados ao fluxo de texto enviado ao cliente. Se esse cliente for um navegador, isso permite visualizar os erros encontrados pelo servidor. Trata-se de uma ajuda para a depuração;
- linha 16: todos os erros são relatados (E_ALL), exceto os avisos (! E_WARNING) e as informações não fatais (! E_NOTICE). Por exemplo, se um arquivo não puder ser aberto, PHP gera um erro do tipo [E_NOTICE]. Se a linha 15 permitir a exibição de erros, o erro de abertura do arquivo aparecerá no navegador do cliente. Isso é bom se você tiver esquecido de testar o resultado da abertura do arquivo, mas não é tão bom se você tiver planejado o teste: uma linha de [notice] passa então a poluir a resposta do servidor ao cliente. Na fase de desenvolvimento, a linha 16 também deve ser comentada: você não quer perder nenhum erro;
- linha 19: o arquivo de configuração é lido;
- linhas 22-27: se essa leitura não ocorreu corretamente, registra-se o erro (linha 25), coloca-se o aplicativo no estado [131] e prepara-se uma mensagem de erro;
- linha 30: decodifica-se a sequência jSON do arquivo de configuração;
- linhas 32-37: se essa decodificação falhar, registra-se o erro (linha 34), coloca-se o aplicativo no estado [132] e prepara-se uma mensagem de erro;
- linhas 40-57: em caso de erro na leitura do arquivo de configuração, não é possível prosseguir. Prepara-se, então, uma resposta jSON para o cliente:
- linha 44: como o arquivo de configuração não foi lido, é necessário importar manualmente o arquivo [autoload] necessário para o [Symfony];
- linhas 46-47: prepara-se uma resposta jSON;
- linha 50: o código HTTP da resposta será 500 INTERNAL_SERVER_ERROR;
- linha 52: define-se o conteúdo jSON da resposta. Todas as respostas fornecidas pelo aplicativo web em análise terão três chaves:
- [action]: a ação solicitada pelo cliente;
- [état]: o estado do aplicativo após a execução dessa ação;
- [réponse]: a resposta do servidor web;
- linha 54: a resposta jSON é enviada ao cliente;
23.9.3. Testes [Postman] - 1
Vamos verificar o comportamento do servidor quando o arquivo de configuração estiver ausente ou estiver incorreto:

Vamos agrupar as diferentes solicitações que nosso cliente [Postman] enviará ao servidor de impostos em coleções.
- No [1], crie uma nova coleção;
- em [2], atribua um nome a ela;
- em [3], a descrição é opcional;

- nas coleções [4], agora aparece uma coleção chamada [impots-server-tests-version12] [5];
- em [6], é possível adicionar uma nova consulta à coleção;

- em [7], atribui-se um nome à consulta;
- em [8], a descrição é opcional;

- em [9-11], a consulta adicionada à coleção;
- em [12], escolha do tipo de consulta, neste caso uma consulta [GET]. Em [19], os diferentes tipos de consulta disponíveis;
- em [13], insere-se aqui o URL do servidor;
- em [14], insere-se aqui os parâmetros adicionados ao URL e que, portanto, serão parâmetros do GET. A vantagem de colocá-los aqui, em vez de diretamente no URL, é que eles serão codificados como URL pelo [Postman]. Se você mesmo os inserir no URL, caberá a você codificá-los como URL;
- no [15], o [Authorization] serve para definir o usuário que vai se conectar. Não precisaremos usar essa opção;
- no [16], os cabeçalhos HTTP que acompanharão a solicitação. Alguns cabeçalhos são incluídos automaticamente na solicitação. Aqui, você pode adicionar novos;
- Em [17], [Body] designa os parâmetros de uma operação [POST]. Teremos que utilizar essa opção;
Faremos o seguinte teste:
- em [main.php], indicamos que o arquivo de configuração é [config2.json], que não existe:

- a linha 16 do código deve ser descomentada;
- linha 18: o erro no nome do arquivo de configuração;
Vamos acessar o [Postman], o [13, 20] e o URL do servidor web de cálculo de impostos e executá-lo ([21]):

A resposta retornada pelo servidor (é claro que o Laragon precisa estar ativo) é a seguinte:

- em [22], o servidor retornou um código HTTP [500 Internal Server Error];
- em [23], [Body] designa o corpo da resposta, ou seja, o documento enviado pelo servidor após os cabeçalhos HTTP [28];
- em [26], vemos que [Postman] recebeu uma resposta jSON;
- em [27], a resposta jSON formatada;
- em [28], a resposta jSON em formato bruto, sem formatação;
- em [29], o modo [Preview] é utilizado quando a resposta é do tipo HTML. O modo [Preview] exibe, então, a página recebida;
- em [30], a resposta jSON do servidor. É exatamente a que esperávamos;
No [25], os cabeçalhos HTTP enviados na resposta do servidor são os seguintes:

- em [32], o tipo jSON da resposta;
Esse primeiro teste nos permitiu constatar que:
- é possível enviar qualquer tipo de solicitação ao servidor testado;
- é possível definir os parâmetros do GET ou do POST;
- temos a resposta completa: cabeçalhos HTTP e o documento que se segue a esses cabeçalhos [Body];
Agora, vamos fazer um segundo teste:

- em [1-3], o arquivo [config3.json] é um arquivo jSON com sintaxe incorreta;
- em [4], o [main.php] está configurado para usar o [config3.json];
Adicionamos uma nova consulta no [Postman]:

- No [1-3], clica-se com o botão direito do mouse em [2] e seleciona-se a opção [duplicate] para duplicar a consulta [2];
- em [4], a nova consulta tem um nome predefinido que deve ser alterado para [5];

- em [6], a consulta renomeada;
- em [9-10], enviamos a mesma solicitação GET que anteriormente;

- em [11], a resposta jSON do servidor;
Mostramos aqui como seriam testadas as diferentes ações do serviço web de cálculo de impostos.
23.9.4. [main.php] – 2
Retomamos a análise do código do controlador principal [main.php]:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestão de erros por PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// recuperamos a configuração
$configFilename = "config.json";
…
// inclui-se as dependências necessárias para o script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require_once "$rootDirectory$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
require_once "$dependency";
}
// criação do arquivo de logs
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// não foi possível criar o arquivo de logs — erro interno do servidor
$état = 133;
(new JsonResponse())->send(
NULL, NULL, $config,
Response::HTTP_INTERNAL_SERVER_ERROR,
["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
[]);
// concluído
exit;
}
Comentários
- linha 18: temos um arquivo de configuração [config.json] que agora existe e está sintaticamente correto. Seria necessário, além disso, testar se as chaves esperadas nesse arquivo estão de fato presentes. Consideraremos que isso faz parte do trabalho normal de depuração do desenvolvedor. Poderíamos ter feito o mesmo raciocínio para os dois erros anteriores;
- linhas 20-28: incluímos todas as dependências necessárias para o projeto web. Já nos deparamos com esse código várias vezes;
- linhas 31-43: tentamos criar o objeto [Logger], que nos permitirá registrar eventos no arquivo [$config['logsFilename']]. Essa criação pode falhar;
- linhas 33-43: tratamento do erro na criação do objeto [Logger];
- linha 35: define-se um número de status;
- linhas 36-40: envio de uma resposta jSON;
- linha 42: o script é interrompido;
Todas as respostas enviadas ao cliente implementam a seguinte interface [InterfaceResponse]:

O código da interface [InterfaceResponse] é o seguinte:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// array $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- linhas 19-27: a interface [InterfaceResponse] possui um único método, [send], para enviar a resposta ao cliente;
- linhas 11-17: o significado dos diferentes parâmetros do método [send];
- linhas 23-25: os parâmetros [$statusCode, $content, $headers] constam no resultado padrão dos controladores secundários do aplicativo. No entanto, a resposta pode precisar de outras informações. Por isso, são fornecidos os três primeiros parâmetros (linhas 20-22), que lhe dão acesso a todas as informações relativas à solicitação, à sessão e à configuração;
- linha 26: a resposta precisa do [Logger], pois irá registrar a resposta enviada ao cliente;
A classe [JsonResponse] implementa a interface [InterfaceResponse] da seguinte maneira:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// preparação do serializador do Symfony
$serializer = new Serializer(
[
// necessário para a serialização de objetos
new ObjectNormalizer()],
// codificador jSON
// para as opções, insira OU entre as diferentes opções
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// serialização jSON
$json = $serializer->serialize($content, 'json');
// cabeçalhos
$headers = array_merge($headers, ["content-type" => "application/json"]);
// envio da resposta
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Comentários
- linha 13: a classe implementa a interface [InterfaceResponse];
- linha 13: a classe estende a classe [ParentResponse]. Todos os tipos de [Response] estendem essa classe. É essa classe pai que envia a resposta ao cliente (linha 46). Como esse código era comum a todos os tipos de [Response], ele foi fatorizado em uma classe pai;
- linhas 33-40: instanciação do serializador [Symfony], que converterá a resposta do servidor [$content] em uma string jSON (linha 42);
- linhas 34-36: o primeiro parâmetro do construtor de [Serializer] é um array. Nele, insere-se uma instância da classe [ObjectNormalizer], necessária para a serialização de objetos. Esse caso ocorre nesta aplicação com uma lista de simulações, em que cada simulação é uma instância da classe [Simulation];
- linha 39: o segundo parâmetro do construtor de [Serializer] também é um array: nele são colocados todos os codificadores utilizados em uma serialização (XML, jSON, CSV…);
- linha 39: haverá apenas um codificador aqui, do tipo [JsonEncoder]. O construtor sem parâmetros poderia ter sido suficiente. Aqui, passamos um parâmetro [JsonEncode] para o construtor, apenas para passar as opções de codificação jSON;
- linha 39: o parâmetro do construtor [JsonEncode] é uma matriz de opções. Aqui, usamos a opção [JSON_UNESCAPED_UNICODE] para solicitar que os caracteres UTF-8 da string jSON sejam representados nativamente e não “escapados”;
- linha 42: o corpo da resposta HTTP é serializado em jSON graças ao serializador anterior;
- linha 44: adiciona-se o cabeçalho HTTP, que informa ao cliente que será enviado o arquivo jSON;
- linha 46: solicitamos à classe pai que envie a resposta ao cliente;
- linhas 48-50: registramos a resposta jSON;
O código da classe pai [ParentResponse] é o seguinte:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: o código HTTP do status da resposta
// string $content: o corpo da resposta a ser enviada
// dependendo do caso, é uma string jSON, XML, HTML
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// preparação da resposta de texto do servidor
$response = new Response();
$response->setCharset("utf-8");
// código de status
$response->setStatusCode($statusCode);
// cabeçalhos
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// envio da resposta
$response->setContent($content);
$response->send();
}
}
Comentários
- linhas 10-13: o significado dos três parâmetros do método [send];
- linha 17: observe-se que o corpo da resposta é do tipo [string] e, portanto, pronto para ser enviado (linha 30);
- linha 22: a resposta conterá caracteres UTF-8;
- linha 24: código de status HTTP da resposta;
- linhas 26-28: adição dos cabeçalhos HTTP fornecidos pelo código do chamador;
- linhas 30-31: envio da resposta ao cliente;
Detalhamos todo o ciclo de uma resposta jSON. Não voltaremos a abordar esse assunto a seguir. Basta lembrar a assinatura da interface [InterfaceResponse]:
interface InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// matriz $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
O controlador principal [main.php] deverá respeitar essa assinatura sempre que solicitar o envio da resposta ao cliente.
23.9.5. Testes [Postman] – 2
Alteramos o arquivo [config.json] da seguinte maneira:

- em [1], indicamos que o arquivo de logs é [Logs], que é uma pasta [2]. A criação do arquivo [Logs] deve, portanto, falhar;
Criamos uma nova solicitação [Postman] [3], denominada [erreur-133]:

- [2-4]: definimos a mesma consulta dos dois testes anteriores;
- [5-7]: obtemos, de fato, a resposta esperada jSON;
23.9.6. [main.php] – 3
Vamos continuar a análise do controlador principal [main.php]:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestão de erros por PHP
…
// criação do arquivo de logs
…
// primeiro log
$logger->write("\n---nouvelle requête\n");
// solicitação atual
$request = Request::createFromGlobals();
// sessão
$session = new Session();
$session->start();
// lista de erros
$erreurs = [];
$erreur = FALSE;
// processando a ação solicitada
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// a ação está sendo armazenada
$action = strtolower($request->query->get("action"));
}
// a ação está sendo registrada no log
$logger->write("action [$action] demandée\n");
// a ação existe?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
$erreurs[] = "action [$action] invalide";
$erreur = TRUE;
$état = 102;
}
// o tipo de sessão deve ser conhecido antes de realizar certas ações
if (!$erreur && !$session->has("type") && $action !== "init-session") {
$erreurs[] = "pas de session en cours. Commencer par action [init-session]";
$erreur = TRUE;
$état = 103;
}
// para certas ações, é necessário estar autenticado
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
$erreurs[] = "action demandée par utilisateur non authentifié";
$erreur = TRUE;
$état = 104;
}
// erros?
if ($erreurs) {
// prepara-se a resposta sem enviá-la
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// a ação é executada por meio de seu controlador
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- a resposta é enviada
// caso de erro fatal HTTP_INTERNAL_SERVER_ERROR
// envia-se um e-mail ao administrador, se possível
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// a resposta depende do tipo de sessão
if ($session->has("type")) {
// o tipo de sessão está na sessão
$type = $session->get("type");
} else {
// se não houver tipo na sessão, então, por padrão, a resposta será em jSON
$type = "json";
}
// adicionamos as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instanciamos o objeto [Response] responsável por enviar a resposta ao cliente
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// a resposta foi enviada — os recursos são liberados
$logger->close();
exit;
Comentários
- uma vez que as primeiras verificações tenham sido feitas e ele saiba que pode operar, o controlador principal se concentra na ação que lhe foi solicitada: ela deve atender a certas condições;
- linha 21: registramos no log o fato de termos uma nova solicitação. Não era possível fazer isso antes, pois não tínhamos certeza de que havíamos um arquivo de logs válido;
- linha 23: encapsulamos todas as informações da solicitação do cliente no objeto Symfony [Request];
- linha 26: iniciamos uma nova sessão ou recuperamos a sessão existente, caso ela exista;
- linha 27: a sessão é ativada;
- linha 29: um array de mensagens de erro;
- linha 30: um booleano que, ao longo dos testes, indica se ocorreu ou não um erro;
- linha 32: o parâmetro [action] deve fazer parte do URL na forma [main.php?action=uneAction]. O parâmetro [action] faz, portanto, parte dos parâmetros [$request→query];
- linhas 33-36: caso de ausência do parâmetro [action] no URL. O erro é registrado e um estado [101] é atribuído a ele;
- linha 39: se o parâmetro [action] estiver presente no URL, ele é armazenado;
- linha 42: o tipo da ação é registrado;
- linhas 45-49: se o parâmetro [action] estiver presente, ele deve ser válido. Todas as ações autorizadas estão definidas na tabela associativa [$config["actions"]];
- linhas 46-48: se a ação for inválida, o erro é registrado e o estado [102] é atribuído a ela;
- linhas 52-56: temos uma ação válida. Ela ainda precisa atender a outras condições. O aplicativo web fornece três tipos de resposta (jSON, XML, HTML). Esse tipo é definido pela ação [init-session]. Essa ação insere o tipo da sessão na chave [type];
- linha 52: fora da ação [init-session], qualquer outra ação deve ser executada com a chave [type] na sessão;
- linhas 53-55: caso contrário, o erro é registrado e o estado [103] é atribuído a ele;
- linhas 58-63: com exceção das ações [init-session] e [authentifier-utilisateur], todas as outras ações devem ser realizadas após a autenticação. A autenticação é realizada por meio da ação [authentifier-utilisateur], que, caso seja bem-sucedida, insere uma chave [user] na sessão;
- linha 59: se a ação não for nem [init-session] nem [authentifier-utilisateur] e a chave [user] não estiver na sessão, ocorre um erro;
- linhas 60-62: registra-se o erro e atribui-se a ele o estado [104];
- linhas 66-71: verifica-se se a matriz [$erreurs] não está vazia. Se for o caso, a ação solicitada ou seu contexto de execução estão incorretos;
- linhas 68-70: prepara-se a resposta a ser enviada ao cliente, mas ainda não é enviada;
- linha 68: código de status HTTP;
- linha 69: corpo da resposta;
- linha 70: cabeçalhos a serem adicionados à resposta; nenhum neste caso;
- linha 73: temos uma ação válida. Vamos solicitar ao seu controlador (secundário) que a processe;
- linha 74: construímos o nome da classe do controlador a ser executado. [__NAMESPACE__] é o espaço de nomes no qual estamos, aqui [Application] (linha 7);
- os nomes das classes do controlador secundário estão no arquivo [config.json]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
A cada ação corresponde um controlador secundário. Se a ação for [authentifier-utilisateur], a variável [$controller] da linha 74 terá, portanto, o valor [Application/AuthentifierUtilisateurController];
- linha 75: registra-se o nome do controlador secundário, para verificação durante o desenvolvimento;
- linha 76: o controlador secundário é executado. Voltaremos a falar sobre os controladores secundários um pouco mais adiante;
- linha 76: todos os controladores secundários retornam o mesmo tipo de resultado, que é uma matriz:
- o primeiro elemento da matriz [$statusCode] é o código de status HTTP da resposta a ser enviada;
- o segundo elemento, [$état], é o estado da aplicação após a execução do controlador;
- o terceiro elemento, [$content], é um array associativo com a chave única [réponse], que corresponde ao corpo da resposta a ser enviada ao cliente;
- o quarto elemento [$headers] é um array de cabeçalhos HTTP a ser adicionado à resposta enviada ao cliente;
- linha 79: chegamos aqui:
- ou porque ocorreu um erro (linhas 68-70);
- ou após a execução de um controlador (linhas 72-76);
- em ambos os casos, os elementos [$statusCode, $état, $content, $headers] necessários para a elaboração da resposta ao cliente são conhecidos;
- linhas 82-87: tratam do caso específico do código de status [500 Internal Server Error]. Se um controlador atribuiu esse código de status, significa que o aplicativo não pode funcionar. Esse é o caso, por exemplo, do cálculo do imposto, se o SGBD utilizado não tiver sido iniciado ou não estiver mais respondendo. Nesse caso, envia-se um e-mail ao administrador do aplicativo para notificá-lo. Não faremos comentários específicos sobre esse código. O uso da classe [SendAdminMail] já foi apresentado (parágrafo com link);
- linhas 89-95: determina-se o tipo [jSON, XML, HTML] do aplicativo web. Se a ação [init-session] foi executada com sucesso, esse tipo está na sessão associada à chave [type] (linha 91). Caso contrário, define-se arbitrariamente um tipo para a resposta, o tipo jSON (linha 94);
- linha 97: [$content] é um array com uma única chave, [réponse], e um único valor, o corpo da resposta a ser enviada ao cliente. São adicionadas a ele as chaves [action] e [état]. A chave [action] permitirá acompanhar melhor os logs do arquivo [logs.txt]. A chave [état] terá duas funções:
- ela permitirá que os clientes jSON e XML saibam em que estado a ação executada deixou o aplicativo web;
- no caso de uma resposta HTML, ela permitirá escolher a visualização HTML que deve ser enviada ao navegador do cliente;
- linha 99: escolhe-se o tipo de classe [Response] a ser executada para enviar a resposta ao cliente;
Já apresentamos a classe [JsonResponse] no parágrafo sobre links. Ela implementa a interface [InterfaceResponse] e estende a classe [ParentResponse]. O mesmo se aplica às outras duas classes, [XmlResponse] e [HtmlResponse].
As respostas estão reunidas na pasta [Responses]:

Todas essas classes implementam a interface [InterfaceResponse], também apresentada no parágrafo [link]:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// array $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Essa interface possui um único método, [send], responsável por enviar a resposta ao cliente. Esse método possui os 7 parâmetros descritos nas linhas 11 a 17. Todas as classes e interfaces da pasta [Responses] estão no espaço de nomes [Application] (linha 3).
Voltemos ao código de [main.php]:
…
// adiciona-se as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instanciamos o objeto [Response] responsável por enviar a resposta ao cliente
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// a resposta foi enviada — os recursos são liberados
$logger->close();
exit;
- linha 5: instanciamos a classe [Response] adequada ao tipo de aplicação. Essas classes são definidas no arquivo [config.json] da seguinte maneira:
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
- linha 5: o nome da classe é precedido pelo seu namespace;
- linha 6: a classe [Response] é instanciada e seu método [send] é chamado com os 7 parâmetros que ele espera. Esses parâmetros são os da interface [InterfaceResponse], que todas as classes de resposta implementam. Isso envia a resposta ao cliente;
- linha 9: o arquivo de logs é fechado;
- linha 10: o controlador principal concluiu seu trabalho;
23.9.7. Testes [Postman] – 3
Vamos testar diversos casos de erro do parâmetro [action] do URL.

- no [1]:
- [erreur-101]: caso em que o parâmetro [action] está ausente no URL;
- [erreur-102]: caso em que o parâmetro [action] está presente no URL, mas não é reconhecido;
- [erreur-103]: caso do parâmetro [action] presente no URL, reconhecido, mas sem que o tipo de resposta esperado [json, xml, html] tenha sido definido;
Cada consulta é executada. Apresentamos diretamente os resultados obtidos:
Acima:
- no [2-4], uma consulta sem o parâmetro [action] no URL [4];
- em [5-7], o resultado jSON;

Acima:
- em [5-9], uma consulta com um parâmetro [action] inválido;
- em [10-13], a resposta jSON;

Acima:
- em [14-19], uma ação reconhecida, mas o tipo (json, xml, html) ainda não foi especificado;
- em [20-23], a resposta jSON do servidor;
23.10. Os controladores secundários
Cada ação é executada por um dos controladores da pasta [Controllers]:


Na arquitetura geral do aplicativo acima, os controladores secundários estão em [2a].
Cada controlador implementa a seguinte interface [InterfaceController]:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna uma tabela [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos=NULL): array;
}
Comentários
- todos os controladores secundários são executados por meio do método [execute] da linha 17. São passadas para esse método as informações conhecidas do controlador principal:
- linha 18: [array $config], que encapsula a configuração do aplicativo;
- linha 19: [Request $request], que é a solicitação HTTP em processamento;
- linha 20: [Session $session], que é a sessão atual do aplicativo web;
- linha 21: [array $infos=NULL], que é um array adicional de informações para o controlador, caso os três primeiros parâmetros do método não sejam suficientes. Nesta aplicação, esse parâmetro nunca foi utilizado. Ele está presente por precaução;
- linha 21: o método [execute] retorna a matriz [$statusCode, $état, $content, $headers]
- [int $statusCode]: o código de status da resposta HTTP;
- [int $état]: o estado em que se encontra o aplicativo ao final da execução;
- [array $content]: uma tabela associativa [réponse=>résultat], em que [résultat] é de qualquer tipo: trata-se do resultado gerado pelo controlador e que será enviado ao cliente, uma vez que esse resultado seja serializado na forma de uma sequência de caracteres;
- [array $headers]: a lista de cabeçalhos HTTP a ser incorporada à resposta HTTP do servidor;
Cada controlador secundário é chamado pelo seguinte código do controlador principal:
// a ação é executada por meio de seu controlador
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
Na linha 3, percebe-se que o quarto parâmetro [array $infos=NULL] do método [execute] não é utilizado.
23.11. As ações
Vamos agora examinar as diferentes ações possíveis do serviço web:
Ação | Função | Contexto de execução |
init-session | Serve para definir o tipo (json, xml, html) das respostas desejadas | Solicitação GET main.php?action=init-session&type=x pode ser emitida a qualquer momento |
autenticar-usuário | Autoriza ou não um usuário a fazer login | Solicitação POST main.php?action=autenticar-usuário A solicitação deve conter dois parâmetros enviados por POST [user, password] Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido |
calcular-imposto | Realiza uma simulação de cálculo de imposto | Solicitação POST main.php?action=calcular-imposto A solicitação deve conter três parâmetros enviados via POST: [marié, enfants, salaire] Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
listar-simulações | Solicita a exibição da lista de simulações realizadas desde o início da sessão | Solicitação GET main.php?action=lister-simulations A solicitação não aceita nenhum outro parâmetro Só pode ser enviada se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
excluir-simulação | Exclui uma simulação da lista de simulações | Solicitação GET main.php?action=lister-simulations&número=x A solicitação não aceita nenhum outro parâmetro Só pode ser enviada se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
fim-sessão | Encerra a sessão de simulações. | Tecnicamente, a sessão web anterior é excluída e uma nova sessão é criada Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
Todos os controladores secundários procedem da mesma forma:
- eles verificam seus parâmetros. Estes são encontrados no objeto [Request→query] para os parâmetros presentes no URL e no objeto [Request→request] para aqueles que são enviados (solicitação POST);
- um controlador se assemelha a uma função ou método que verifica a validade de seus parâmetros. No caso do controlador, porém, é um pouco mais complicado:
- os parâmetros esperados podem estar ausentes;
- os parâmetros esperados são todos cadeias de caracteres, enquanto uma função pode definir o tipo de seus parâmetros. Se o parâmetro esperado for um número, é preciso verificar se a cadeia do parâmetro corresponde de fato a um número;
- uma vez verificado que os parâmetros esperados estão presentes e são sintaticamente corretos, é preciso verificar se eles são válidos no contexto de execução atual. Esse contexto está presente na sessão. O exemplo da autenticação é um exemplo de contexto de execução. Certas ações só devem ser processadas após a autenticação do cliente. Geralmente, uma chave na sessão indica se essa autenticação ocorreu ou não;
- uma vez realizadas as verificações anteriores, o controlador secundário pode operar. Esse trabalho de verificação dos parâmetros é muito importante. Não se pode aceitar que um cliente nos envie qualquer coisa em qualquer momento da vida útil do aplicativo. É preciso controlar totalmente a vida útil do aplicativo;
- uma vez concluído seu trabalho, o controlador secundário retorna a tabela [$statusCode, $état, $content, $headers] esperada pelo controlador principal que o chamou;
Vamos agora examinar os diferentes controladores ou, o que dá no mesmo, as diferentes ações que marcam o ciclo de vida do aplicativo web.
23.11.1. A ação [init-session]
A ação [init-session] é processada pelo controlador [InitSessionController] a seguir:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
class InitSessionController implements InterfaceController {
// $config é a configuração da aplicação
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// é necessário ter um GET e um único parâmetro diferente de [action]
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
if ($erreur) {
$état = 701;
$message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// recuperamos os parâmetros do GET
$erreur = FALSE;
// tipo
if (!$request->query->has("type")) {
$erreur = TRUE;
$état = 702;
$message = "paramètre [type] manquant";
} else {
$type = strtolower($request->query->get("type"));
}
// verificação do tipo
if (!$erreur && !array_key_exists($type, $config["types"])) {
$erreur = TRUE;
$état = 703;
$message = "paramètre type [$type] invalide";
}
// erro?
if ($erreur) {
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// definimos o tipo de sessão na sessão
$session->set("type", $type);
// mensagem de sucesso
$message = "session démarrée avec type [$type]";
$état = 700;
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
Comentários
- aguarda-se uma solicitação [GET main.php?action=init-session&type=xxx]
- linhas 25-26: verifica-se se a solicitação é uma solicitação GET com dois parâmetros no URL;
- linhas 27-31: se não for o caso, registra-se o erro e envia-se um resultado [$statusCode, $état, $content, $headers] ao controlador principal;
- linhas 35-39: verifica-se se o parâmetro [type] está presente no URL. Caso contrário, registra-se o erro;
- linha 40: registra-se o tipo da sessão;
- linhas 43-47: verifica-se se o tipo da sessão é um dos seguintes (json, xml, html). Caso contrário, registra-se o erro;
- linhas 49-51: se houver erro, envia-se um resultado [$statusCode, $état, $content, $headers] ao controlador principal;
- linha 53: o tipo da sessão é inserido na sessão do aplicativo web;
- linhas 55-57: o controlador concluiu seu trabalho. Envia-se um resultado de sucesso [$statusCode, $état, $content, $headers] ao controlador principal;
Vamos relembrar o que o controlador principal faz com a resposta dos controladores secundários:
// erros?
if ($erreurs) {
// preparando a resposta sem enviá-la
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// executa-se a ação por meio de seu controlador
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- enviando a resposta
// caso de erro fatal HTTP_INTERNAL_SERVER_ERROR
// envia-se um e-mail ao administrador, se possível
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// a resposta depende do tipo de sessão
if ($session->has("type")) {
// o tipo de sessão está na sessão
$type = $session->get("type");
} else {
// se não houver tipo na sessão, então, por padrão, a resposta será em jSON
$type = "json";
}
// adicionamos as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] responsável por enviar a resposta ao cliente
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// a resposta foi enviada — libera-se os recursos
$logger->close();
exit;
- linha 12: o controlador principal recupera o resultado do controlador secundário;
- linhas 35-36: após algumas verificações, ele envia a resposta instanciando uma das classes [JsonResponse, XmlResponse, HtmlResponse], de acordo com o tipo (json, xml, html) da sessão atual;
A seguir, realizaremos testes com a classe [Postman] no âmbito de uma sessão de simulações com o tipo [json]. O funcionamento da classe [JsonResponse] foi apresentado no parágrafo com o link.
23.11.2. Testes [Postman]

Acima:
- no [2], três novos testes;
- em [3-7], a ação [init-session] com o parâmetro [type] ausente;
- em [8-11], a resposta jSON do servidor;

Acima:
- em [1-7], a ação [init-session] com um parâmetro [type] incorreto;
- em [8-11], a resposta jSON do servidor;

Acima:
- em [1-8], a ação [init-session] com o tipo jSON;
- em [9-12], a resposta jSON do servidor;
23.11.3. A ação [authentifier-utilisateur]
A ação [authentifier-utilisateur] é executada pelo controlador [AuthentifierUtilisateurController] a seguir:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class AuthentifierUtilisateurController implements InterfaceController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// deve haver um POST e um único parâmetro GET
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
$état = 201;
$message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
// o resultado é enviado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// recuperamos os parâmetros do POST
$erreurs = [];
// usuário
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// senha
if (!$request->request->has("password")) {
$état += 4;
$erreurs[] = "paramètre [password] manquant";
} else {
$password = trim($request->request->get("password"));
}
// erro?
if ($erreurs) {
// envia-se o resultado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// verificação das credenciais do usuário
// o usuário existe?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
$i++;
}
// Encontrado?
if (!$trouvé) {
// mensagem de erro
$message = "Echec de l'authentification [$user, $password]";
$état = 221;
// o resultado é enviado ao controlador principal
return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
} else {
// registramos na sessão que o usuário foi autenticado
$session->set("user", TRUE);
// mensagem de sucesso
$message = "Authentification réussie [$user, $password]";
$état = 200;
// retorna o resultado ao controlador principal
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
}
Comentários
- aguarda-se uma solicitação [POST main.php?action=authentifier-utilisateur] com dois parâmetros enviados via POST [user, password];
- linhas 24-25: verifica-se se há uma solicitação POST com um único parâmetro no URL;
- linhas 26-31: se houver erro, ele é registrado e um resultado [$statusCode, $état, $content, $headers] é retornado ao controlador principal;
- linhas 36-39: verifica-se a presença do parâmetro [user] nos valores postados. Se ele não estiver presente, registra-se o erro;
- linhas 43-45: verifica-se a presença do parâmetro [password] nos valores lançados. Se ele não estiver presente, registra-se o erro;
- linhas 50-53: se algum dos valores lançados estiver faltando, um resultado [$statusCode, $état, $content, $headers] é retornado ao controlador principal;
- linhas 56-62: verifica-se se o par [$user,$password] recuperado está presente na tabela [$config[‘users’]] do arquivo de configuração;
- linhas 64-69: caso contrário, o erro é registrado. O código de status HTTP é alterado para [Response::HTTP_UNAUTHORIZED] e o resultado [$statusCode, $état, $content, $headers] é devolvido ao controlador principal;
- linha 72: a autenticação foi bem-sucedida. Isso é registrado na sessão, inserindo-se nela a chave [user]. É a presença dessa chave que indica uma autenticação bem-sucedida;
- linhas 73-77: é enviado um resultado de sucesso [$statusCode, $état, $content, $headers] ao controlador principal;
23.11.4. Testes [Postman]
Estamos realizando os testes [Postman] do controlador [AuthentifierUtilisateurController] no modo jSON;

Acima:
- em [1-6], a ação [authentifier-utilisateur] com um GET [2], quando o necessário é um POST;
- em [7-10], a resposta jSON do servidor;
Vamos substituir o GET por um POST [2] sem inserir parâmetros no corpo da resposta [7]:

Acima:
- em [1-7], o POST sem parâmetros enviados em [7];
- em [8-11], a resposta jSON do servidor;
Vamos agora adicionar um parâmetro [password] no corpo (body) [4] da solicitação:

Acima:
- em [1-6], uma solicitação POST [2] com um parâmetro [password] enviado como POST [4-6]. Os parâmetros enviados devem ser adicionados ao corpo (body) da solicitação [4]. Existem várias maneiras de enviar valores ao servidor. Escolhemos o método [x-www-form-urlencoded] [5];
- em [8-10], a resposta jSON do servidor;
Agora, vamos definir o parâmetro [user] sem o parâmetro [password]:

Acima:
- em [1-7], uma solicitação POST sem o parâmetro [password] [4-7];
- em [8-11], a resposta jSON do servidor;
Agora, vamos definir os dois parâmetros enviados [user, password], mas com valores que fazem com que a autenticação falhe:

Acima:
- em [1-9], uma solicitação POST com parâmetros enviados por POST [user, password] incorretos;
- em [10-13], a resposta jSON do servidor. Observe-se o código de status [401 Unauthorized] [10] da resposta;
Agora, uma solicitação POST com identificadores válidos:

Acima:
- em [1-9], a solicitação POST [2] com identificadores válidos [6-9];
- em [10-13], a resposta jSON do servidor. Observe-se o código de status HTTP [200 OK] em [10];
23.11.5. A ação [calculer-impot]
A ação [calculer-impot] é processada pelo controlador [CalculerImpotController] a seguir:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias da camada [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class CalculerImpotController implements InterfaceController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// deve haver um parâmetro GET e três parâmetros POST
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
// observa-se o erro
$message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
$état = 301;
// retorno do resultado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// recuperam-se os parâmetros do POST
$erreurs = [];
$état = 310;
// estado civil
if (!$request->request->has("marié")) {
$état += 2;
$erreurs[] = "paramètre [marié] manquant";
} else {
$marié = trim(strtolower($request->request->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
if ($erreur) {
$état += 4;
$erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
}
}
// recupera-se o número de filhos
if (!$request->request->has("enfants")) {
$état += 8;
$erreurs[] = "paramètre [enfants] manquant";
} else {
$enfants = trim($request->request->get("enfants"));
$erreur = !preg_match("/^\d+$/", $enfants);
if ($erreur) {
$état += 9;
$erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
}
}
// recuperação do salário anual
if (!$request->request->has("salaire")) {
$erreurs[] = "paramètre [salaire] manquant";
$état += 16;
} else {
$salaire = trim($request->request->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
if ($erreur) {
$état += 17;
$erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
}
}
// erro?
if ($erreurs) {
// retorno do resultado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// temos tudo o que é necessário para trabalhar
// Redis
\Predis\Autoloader::register();
try {
// cliente [predis]
$redis = new \Predis\Client();
// estamos nos conectando ao servidor para verificar se ele está disponível
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// deu errado
// retorno do resultado com erro para o controlador principal
$état = 350;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// temos parâmetros válidos
// criação da camada [dao]
if (!$redis->get("taxAdminData")) {
try {
// vamos buscar os dados fiscais no banco de dados
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// os dados recuperados são inseridos no Redis
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// ocorreu um erro
// retorno do resultado com erro ao controlador principal
$état = 340;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// os dados fiscais são armazenados na memória de escopo [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// instanciação da camada [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
}
// criação da camada [métier]
$métier = new ServerMetier($dao);
// já temos tudo o que é necessário para trabalhar — cálculo do imposto
$résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// adicionamos à sessão a simulação que acabou de ser feita
$simulation = new Simulation();
$résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
$simulation->setFromArrayOfAttributes($résultat);
// existe uma lista de simulações na sessão?
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// adição da simulação à lista de simulações
$simulations[] = $simulation;
// as simulações são colocadas novamente na sessão
$session->set("simulations", $simulations);
// retorno do resultado ao controlador principal
$état = 300;
return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
}
}
Comentários
- a solicitação esperada é [POST main.php?action=calculer-impot] com três parâmetros enviados por POST em [marié, enfants, salaire]:
- [marié] deve ter seu valor definido em [oui, non];
- [enfants, salaire] deve ser um número inteiro positivo ou zero;
- linhas 26-27: verifica-se se existe de fato um POST com um único parâmetro no URL;
- linhas 28-34: se não for o caso, um resultado de erro é enviado ao controlador principal;
- linha 36: vamos acumular as mensagens de erro na tabela [$erreurs];
- linhas 39-41: verifica-se a presença do parâmetro [marié]. Se ele não estiver presente, o erro é registrado;
- linhas 43-49: verifica-se se o valor de [marié] está presente em [oui, non]. Caso contrário, o erro é registrado;
- linhas 51-54: verifica-se a presença do parâmetro [enfants]. Caso não esteja presente, o erro é registrado;
- linhas 55-61: verifica-se se o valor do parâmetro [enfants] é um número positivo ou zero. Caso contrário, o erro é registrado;
- linhas 63-66: verifica-se a presença do parâmetro [salaire]. Caso ele não esteja presente, o erro é registrado;
- linhas 67-72: verifica-se se o valor do parâmetro [salaire] é um número positivo ou zero. Caso contrário, o erro é registrado;
- linhas 75-78: se a matriz [$erreurs] não estiver vazia, significa que ocorreram erros. Coloca-se a matriz de erros na resposta e retorna-se o resultado ao controlador principal;
- linha 80: temos parâmetros válidos. Podemos calcular o imposto. Para isso, é preciso construir as camadas [dao] e [métier], que realizam esse cálculo;
- linhas 82-94: cria-se um cliente [Redis];
- linhas 88-94: se não foi possível se conectar ao servidor [Redis], enviamos um código [500 Internal Server Error] ao cliente;
- linha 98: verifica-se se o servidor [Redis] possui a chave [taxAdminData]. Essa chave representa os dados da administração fiscal. Se a chave não estiver presente, os dados fiscais devem ser buscados no banco de dados;
- linha 101: construção da camada [dao] quando os dados fiscais precisam ser obtidos do banco de dados. A classe [ServerDaoWithRedis] foi descrita no parágrafo “link”;
- linha 103: os dados recuperados do banco de dados são armazenados na memória [Redis] com a chave [taxAdminData];
- linhas 104-110: se a consulta ao banco de dados falhar, registra-se o erro retornado pela camada [dao] e ele é incorporado ao resultado enviado ao controlador principal;
- linha 109: a mensagem de erro retornada pela camada [PDO] está codificada em [iso-8859-1]. Ela é codificada em [utf-8];
- linhas 111-117: se a chave [taxAdminData] existir na memória [Redis], então os dados fiscais são repassados diretamente ao construtor da camada [dao];
- linha 119: a camada [métier] é criada. A classe [ServerMetier] foi descrita no parágrafo “link”;
- linhas 124-126: com o valor do imposto calculado, é criado um objeto [Simulation]. A classe [Simulation] encapsula os dados de uma simulação e foi descrita no parágrafo “link”;
- linhas 128-132: a simulação que acaba de ser criada deve ser adicionada à lista de simulações já calculadas. Essa lista está na sessão, a menos que ainda não tenha sido realizada nenhuma simulação;
- linhas 133-136: a simulação é adicionada à lista de simulações, e essa lista é colocada novamente na sessão;
- linhas 137-139: o resultado é devolvido ao controlador principal;
23.11.6. Testes [Postman]
Realizamos os testes [Postman] do controlador [CalculerImpotController] no modo jSON;

Acima:
- em [1-7], fazemos uma solicitação [GET] em vez de [POST];
- em [8-11], a resposta jSON do servidor;
Agora, vamos usar um método [POST], com ou sem parâmetros enviados por POST, bem como com parâmetros enviados por POST inválidos:

Acima:
- fazemos uma solicitação [POST] [2] com parâmetros enviados [6-11] [marié, enfants, salaire] inválidos. É possível não enviar um desses parâmetros desmarcando a respectiva caixa de seleção em [16]. Isso permitirá que você teste diferentes cenários. Na captura de tela acima, os três parâmetros estão presentes e todos inválidos;
- no [12-15], a resposta jSON do servidor;
Agora, vamos desmarcar dois dos três parâmetros enviados:

Acima,
- em [5-8], apenas o parâmetro [salaire] é enviado e, além disso, está inválido;
- em [9-11], o resultado jSON do servidor;
Agora, vamos fazer um cálculo de imposto com parâmetros válidos:

Acima:
- em [1118], uma solicitação com parâmetros válidos [6-8];
- em [12-14], a resposta jSON do servidor;
23.11.7. A ação [lister-simulations]
A ação [lister-simulations] é processada pelo controlador secundário [ListerSimulationsController] da seguinte forma:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class ListerSimulationsController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// deve haver um único parâmetro GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
$état = 501;
$message = "GET requis, avec l'unique paramètre [action] dans l'URL";
// retorna um resultado com erro para o controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// recupera-se a lista de simulações na sessão
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// retorna um resultado com sucesso ao controlador principal
$état = 500;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Comentários
- solicitação [GET main.php?action=lister-simulations];
- linhas 24-25: verifica-se se há uma solicitação GET com um único parâmetro;
- linhas 26-31: se não for o caso, um resultado com erro é retornado ao controlador principal;
- linhas 33-37: recupera-se a lista de simulações da sessão, caso ela esteja presente (linha 36); caso contrário, essa lista está vazia (linha 34);
- linhas 39-40: a lista de simulações é devolvida ao controlador principal;
23.11.8. Testes [Postman]
Vamos criar dois testes: um de erro e outro bem-sucedido.

Acima:
- no [1-8], fazemos uma consulta [GET] com um parâmetro [param1] a mais no URL [3, 7-8];
- em [9-12], a resposta jSON do servidor;
Agora, vamos fazer uma solicitação válida:

Acima:
- em [1-5], uma solicitação válida;
O resultado da solicitação é o seguinte:

- em [3-6], a resposta jSON do servidor. Antes deste teste, o teste [Postman] [calculer-impot-300] havia sido executado várias vezes para criar simulações na sessão web do servidor;
23.11.9. A ação [supprimer-simulation]
A ação [supprimer-simulation] é processada pelo controlador secundário [SupprimerSessionController] a seguir:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class SupprimerSimulationController {
/// $config é a configuração do aplicativo
// processamento de uma solicitação (Request)
// utiliza a sessão e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// devem existir dois parâmetros: GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
$état = 600;
if ($erreur) {
$état += 2;
$message = "GET requis, avec les paramètres [action, numéro]";
}
// o parâmetro [numéro] deve existir
if (!$erreur) {
$état += 4;
$erreur = !$request->query->has("numéro");
if ($erreur) {
$message = "paramètre [numéro] manquant";
}
}
// o parâmetro [numéro] deve ser válido
if (!$erreur) {
$état += 8;
$numéro = $request->query->get("numéro");
$erreur = !preg_match("/^\d+$/", $numéro);
if ($erreur) {
$message = "paramètre [$numéro] invalide";
}
}
// o parâmetro [numéro] deve estar no intervalo [0,n-1]
// se n for o número de simulações
if (!$erreur) {
$numéro = (int) $numéro;
$erreur = !$session->has("simulations");
if (!$erreur) {
$simulations = $session->get("simulations");
$erreur = $numéro < 0 || $numéro >= count($simulations);
}
if ($erreur) {
$état += 16;
$message = "la simulation n° [$numéro] n'existe pas";
}
}
// erro?
if ($erreur) {
// o resultado é enviado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// a simulação é excluída $numéro
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// as simulações são recolocadas na sessão
$session->set("simulations", $simulations);
// retorna a lista de simulações ao cliente
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Comentários
- solicitação [GET main.php?action=supprimer-simulation&numéro=x];
- linhas 24-30: verifica-se se há uma solicitação GET com dois parâmetros;
- linhas 32-38: verifica-se se o parâmetro [numéro] existe entre os parâmetros do URL;
- linhas 40-47: verifica-se se o valor do parâmetro [numéro] está sintaticamente correto;
- linhas 50-61: verifica-se se a simulação nº [numéro] realmente existe. Há dois casos de erro:
- a lista de simulações não pode ser encontrada na sessão (linha 52);
- o n.º [numéro] da simulação a ser excluída não existe na lista de simulações;
- linhas 63-66: em caso de erro, um resultado com erro é retornado ao controlador principal;
- linha 68: a simulação nº [numéro] é excluída;
- linha 69: a operação [unset] não altera os índices [0, n-1] da lista. Para atualizá-los, solicitam-se os valores da tabela [$simulations] para eliminar a simulação ausente;
- linha 71: insere-se a nova tabela de simulações na sessão;
- linhas 73-74: a nova lista de simulações é devolvida ao controlador principal;
23.11.10. Testes [Postman]
Vamos realizar testes de erro e de sucesso:

Acima:
- em [1-6], uma solicitação GET sem o parâmetro [numéro];
- em [7-10], a resposta jSON do servidor;
Agora, uma solicitação com um número sintaticamente incorreto:

Acima:
- em [1-5], uma solicitação GET com um parâmetro [numéro] inválido [3, 5];
- em [6-9], a resposta jSON do servidor;
Agora, uma solicitação com um número de simulação que não existe:

Acima:
- em [1-5], uma solicitação com um número de simulação igual a 100 que não existe na lista de simulações;
- em [6-9], a resposta jSON do servidor;
Agora, vamos excluir a simulação nº 0 da lista, ou seja, a primeira simulação. Primeiro, solicitemos novamente essa lista com a consulta [lister-simulations-500]:

- No [1], há atualmente 2 simulações;
Excluímos a primeira simulação (número 0):

Acima:
- no [1-5], excluímos a simulação nº 0, [5];
- em [6-9], a resposta jSON do servidor. Vemos que a simulação nº 0 foi excluída;
Vamos repetir essa operação:

Acima:
- em [1], não há mais simulações na sessão web do servidor;
23.11.11. A ação [fin-session]
A ação [fin-session] é processada pelo controlador secundário [FinSessionController] a seguir:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class FinSessionController implements InterfaceController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// deve haver um único parâmetro GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
// erro?
if ($erreur) {
$état = 401;
// resultado no controlador principal
$message = "GET requis avec le seul paramètre [action] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// o tipo de sessão é armazenado
$type = $session->get("type");
// a sessão atual é invalidada
$session->invalidate();
// reinsere-se o tipo na nova sessão
$session->set("type", $type);
// envio da resposta
$état = 400;
// resultado para o controlador principal
$content = ["réponse" => "session supprimée"];
return [Response::HTTP_OK, $état, $content, []];
}
}
Comentários
- solicitação [GET main.php?action=fin-session];
- linhas 25-33: verifica-se se a ação é uma GET com o único parâmetro [fin-action];
- linha 38: invalida-se a sessão atual. Isso exclui os dados registrados nela e uma nova sessão é iniciada;
- linha 36: antes do término da sessão, armazena-se o tipo [json, xml, html] da mesma;
- linha 40: o tipo da sessão anterior é reposto na nova sessão. Por fim, reiniciamos com uma nova sessão com a chave única [type];
- linhas 44-45: o resultado é devolvido ao controlador principal;
23.11.12. Testes [Postman]
Faremos um teste de erro e um teste de sucesso:

Acima:
- em [1-5], solicitamos o encerramento da sessão [5] com um POST [2] em vez do GET esperado;
- em [6-9], a resposta jSON do servidor;
Agora, um exemplo de sucesso. Vejamos, em primeiro lugar, o cookie de sessão trocado entre o cliente [Postman] e o servidor durante o último teste realizado:

Acima:
- em [3], o cookie de sessão enviado pelo cliente [Postman] ao servidor;
Vejamos agora os cabeçalhos HTTP enviados pelo servidor em sua resposta:

Acima:
- em [3-4], o cookie de sessão não consta na resposta do servidor. Isso é normal. O servidor o envia apenas uma vez: no início de uma nova sessão na web;
Agora, vamos executar uma ação [fin-session] válida:

Acima:
- em [1-3], uma ação [fin-session] válida;
- em [4-7], a resposta jSON do servidor;
Vamos examinar os cabeçalhos HTTP enviados na resposta do servidor:

- em [3], o servidor envia o cabeçalho [Set-Cookie], indicando assim que uma nova sessão da web está sendo iniciada;
23.12. Tipos de resposta do servidor
23.12.1. Introdução
Vamos revisar a arquitetura geral do aplicativo:

Apresentaremos os tipos de resposta possíveis [3a]. Elas estão reunidas na pasta [Responses] do projeto:

Já apresentamos a classe [JsonResponse] no parágrafo sobre links. Ela implementa a interface [InterfaceResponse] e estende a classe [ParentResponse]. O mesmo se aplica às outras duas classes, [XmlResponse] e [HtmlResponse].
Vale lembrar a definição da interface [InterfaceResponse]:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- linhas 19-27: a interface [InterfaceResponse] possui um único método, [send], para enviar a resposta ao cliente;
- linhas 11-17: o significado dos diferentes parâmetros do método [send];
- linhas 23-25: os parâmetros [$statusCode, $content, $headers] constituem a resposta padrão dos controladores secundários do aplicativo. No entanto, a resposta pode precisar de outras informações. Por isso, são fornecidos os três primeiros parâmetros (linhas 20-22), que lhe dão acesso a todas as informações relativas à solicitação, à sessão e à configuração;
- linha 26: a resposta precisa do [Logger], pois registrará a resposta enviada ao cliente;
Vamos agora relembrar o código da classe [ParentResponse], classe pai dos três tipos de resposta que abstrai o que lhes é comum: o envio efetivo de uma resposta de texto ao cliente:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: o código HTTP do status da resposta
// string $content: o corpo da resposta a ser enviada
// dependendo do caso, é uma string jSON, XML, HTML
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// preparação da resposta de texto do servidor
$response = new Response();
$response->setCharset("utf-8");
// código de status
$response->setStatusCode($statusCode);
// cabeçalhos
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// envio da resposta
$response->setContent($content);
$response->send();
}
}
Comentários
- linhas 10-13: o significado dos três parâmetros do método [send];
- linha 17: observe-se que o corpo da resposta é do tipo [string] e, portanto, está pronto para ser enviado (linha 30);
- linha 22: a resposta conterá caracteres UTF-8;
- linha 24: código de status HTTP da resposta;
- linhas 26-28: adição dos cabeçalhos HTTP fornecidos pelo código do chamador;
- linhas 30-31: envio da resposta ao cliente;
Por fim, vale lembrar o código do controlador principal que solicita o envio da resposta ao cliente:
// adicionando as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] responsável por enviar a resposta ao cliente
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// a resposta foi enviada — os recursos são liberados
$logger->close();
exit;
- linha 4: define-se o nome da classe [Response] a ser instanciada;
- linha 5: instanciamos a classe e enviamos a resposta ao cliente por meio do método [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. Como implementam a mesma interface [InterfaceResponse], os métodos [send] dos diferentes tipos de resposta têm todos a mesma assinatura;
23.12.2. A classe [JsonResponse]
Ela já foi apresentada no parágrafo anterior. No entanto, reproduzimos seu código para destacar melhor a homogeneidade das três classes de resposta:
A classe [JsonResponse] implementa a interface [InterfaceResponse] da seguinte maneira:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// matriz $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// array $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// preparação do serializador do Symfony
$serializer = new Serializer(
[
// necessário para a serialização de objetos
new ObjectNormalizer()],
// codificador jSON
// para as opções, insira OU entre as diferentes opções
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// serialização jSON
$json = $serializer->serialize($content, 'json');
// cabeçalhos
$headers = array_merge($headers, ["content-type" => "application/json"]);
// envio da resposta
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Comentários
- linha 13: a classe implementa a interface [InterfaceResponse];
- linha 13: a classe estende a classe [ParentResponse]. Todos os tipos de [Response] estendem essa classe. É essa classe pai que envia a resposta ao cliente (linha 46). Como esse código era comum a todos os tipos de [Response], ele foi fatorizado em uma classe pai;
- linhas 33-40: instanciação do serializador [Symfony], que converterá a resposta do servidor [$content] em uma string jSON (linha 42);
- linhas 34-36: o primeiro parâmetro do construtor de [Serializer] é um array. Nele, insere-se uma instância da classe [ObjectNormalizer], necessária para a serialização de objetos. Esse caso ocorre nesta aplicação com uma lista de simulações, em que cada simulação é uma instância da classe [Simulation];
- linha 39: o segundo parâmetro do construtor de [Serializer] também é um array: nele são colocados todos os codificadores utilizados em uma serialização (XML, jSON, CSV…);
- linha 39: haverá apenas um codificador aqui, do tipo [JsonEncoder]. O construtor sem parâmetros poderia ter sido suficiente. Aqui, passamos um parâmetro [JsonEncode] para o construtor, apenas para passar as opções de codificação jSON;
- linha 39: o parâmetro do construtor [JsonEncode] é uma matriz de opções. Aqui, usamos a opção [JSON_UNESCAPED_UNICODE] para solicitar que os caracteres UTF-8 da string jSON sejam representados nativamente e não “escapados”;
- linha 42: o corpo da resposta HHTP é serializado em jSON graças ao serializador anterior;
- linha 44: adiciona-se o cabeçalho HTTP, que informa ao cliente que será enviado jSON;
- linha 46: solicita-se à classe pai que envie a resposta ao cliente;
- linhas 48-50: registramos a resposta jSON;
23.12.3. A classe [XmlResponse]
A classe [XmlResponse] implementa a interface [InterfaceResponse] da seguinte maneira:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class XmlResponse extends ParentResponse implements InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// preparação do serializador do Symfony
$serializer = new Serializer(
// necessário para a serialização de objetos
[new ObjectNormalizer()],
[
// serialização XML
new XmlEncoder(
[
XmlEncoder::ROOT_NODE_NAME => 'root',
XmlEncoder::ENCODING => 'utf-8'
]
),
// serialização jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// serialização XML
$xml = $serializer->serialize($content, 'xml');
// cabeçalhos
$headers = array_merge($headers, ["content-type" => "application/xml"]);
// envio da resposta
parent::sendResponse($statusCode, $xml, $headers);
// registro
if ($logger !== NULL) {
// registro em jSON
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Comentários
- linhas 34-48: instanciação de um serializador Symfony. O construtor aceita dois parâmetros do tipo array;
- linha 36: o primeiro array contém uma instância do tipo [ObjectNormalizer], que participa da serialização de objetos;
- linhas 37-47: o segundo array contém os codificadores utilizados para a serialização. É possível definir diversos tipos de serialização com o mesmo serializador;
- linhas 38-44: o codificador XML;
- linha 41: define-se a raiz do código XML gerado. Este terá o formato <root>[autres balises XML]</root>;
- linha 42: a codificação utilizará os caracteres UTF-8;
- linha 46; o codificador jSON. Este será utilizado para o registro da resposta no arquivo [logs.txt], que é criado em jSON;
- linha 50: o corpo da resposta enviada ao cliente é serializado em XML;
- linha 52: adiciona-se aos cabeçalhos recebidos como parâmetro (linha 30) o cabeçalho HTTP, que indica ao cliente que está sendo enviado a ele um documento XML;
- linha 54: envio efetivo da resposta ao cliente pela classe pai;
- linhas 56-60: registro da resposta em jSON;
23.12.4. Testes [Postman]
Já realizamos todos os testes de erros possíveis no jSON. Não há mais nada a ser feito no XML. Apresentamos dois exemplos de resposta XML:

Acima:
- em [1-3], a solicitação de início de sessão XML;
- em [4-7], a resposta XML do servidor;
A partir de agora, todas as respostas do servidor serão em XML. Podemos reutilizar todas as solicitações já utilizadas em [Postman] sem alterá-las e, para cada uma delas, teremos uma resposta XML. Vamos, por exemplo, realizar uma autenticação bem-sucedida:

Acima:
- no [1-3], uma solicitação de autenticação válida;
- em [4-7], a resposta XML do servidor;
23.12.5. A resposta [HtmlResponse]
Quando o tipo da sessão é [html], um objeto do tipo [HtmlResponse] é instanciado para enviar a resposta ao cliente. Esse objeto enviará ao cliente um fluxo HTML, que depende do código de estado retornado pelo controlador secundário que processou a ação. Essa correspondência [état=>vue] está registrada no arquivo de configuração [config.json] da seguinte maneira:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Essa configuração deve ser interpretada da seguinte forma: [‘nom de la vue’ => ‘états associés à cette vue’]
- linha 2: se o controlador secundário tiver retornado um estado da tabela [700, 221, 400], então deve-se exibir a visualização [vue-authentification.php];
- linha 3: se o controlador secundário tiver retornado um estado da tabela [200, 300, 341, 350, 800], deve-se exibir a visualização [vue-calcul-impot.php];
- linha 4: se o controlador secundário tiver retornado um estado da tabela [500, 600], então deve-se exibir a visualização [vue-liste-simulations.php];
- linha 6: se o controlador secundário tiver retornado um estado que não consta em nenhuma das tabelas anteriores, deve-se exibir a visualização [vue-erreurs.php];
As visualizações estão reunidas na pasta [Views] do projeto:

O código da classe [HtmlResponse] é o seguinte:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class HtmlResponse extends ParentResponse implements InterfaceResponse {
// Solicitação $request: solicitação em processamento
// Sessão $session: a sessão do aplicativo web
// array $config: a configuração da aplicação
// int statusCode: o código de status da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta
// Logger $logger: o logger para gravar registros
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// preparação do serializador do Symfony
$serializer = new Serializer(
[
// para a serialização de objetos
new ObjectNormalizer()],
[
// para a serialização jSON do log da resposta
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// a resposta HTML depende do código de estado retornado pelo controlador
$état = $content["état"];
// a cada estado corresponde uma visualização — essa é procurada na configuração do aplicativo
// a lista de visualizações
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// percorre-se a lista de visualizações
while (!$trouvé && $i < count($vues)) {
// relatórios associados à visualização nº i
$états = $config["vues"][$vues[$i]];
// o relatório procurado está entre os relatórios associados à visualização nº I?
if (in_array($état, $états)) {
// a visualização exibida será a visualização nº i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// próxima visualização
$i++;
}
// Encontrado?
if (!$trouvé) {
// se não houver nenhuma visualização para o estado atual do aplicativo
// exibe-se a tela de erros
$vueRéponse = $config["vue-erreurs"];
}
// recupera-se a visualização HTML a ser exibida em uma sequência de caracteres
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// indica-se nos cabeçalhos que será enviado o HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// a classe pai se encarrega do envio efetivo da resposta
parent::sendResponse($statusCode, $html, $headers);
// registro em jSON da resposta sem o HTML
if ($logger !== NULL) {
// registro em jSON da resposta do controlador secundário que processou a ação
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Comentários
- linhas 32-41: instanciamos um serializador do Symfony. Ele é necessário para o log jSON da resposta do controlador que processou a ação (linhas 72-82);
- linhas 42-57: procura-se na configuração da aplicação a vista que deve ser exibida. Ela depende do código de estado retornado pelo controlador que processou a ação. Esse código está em [$content[‘état’]] (linha 43);
- linhas 42-61: procura-se a visualização que corresponde a esse estado;
- linhas 62-67: se nenhuma visualização for encontrada, estamos diante de uma situação de código de estado anormal para o aplicativo HTML. Explicaremos mais adiante esse conceito de estados anormais. Nesse caso, exibimos uma visualização de erro;
- linhas 68-70: interpreta-se o código PHP da visualização selecionada e coloca-se o resultado na variável [$html] (linha 71);
- esse código merece algumas explicações. Imaginemos que a visualização selecionada seja [vue-authentification.php], que apresenta um formulário de autenticação na web:
- linha 69: a função [ob_start] inicia o que a documentação chama de temporização de saída. Tudo o que é escrito por operações print, require… e que normalmente seria enviado imediatamente ao cliente vai para um buffer de saída (ob=output buffer) sem ser enviado ao cliente;
- linha 70: carrega-se a vista [vue-authentification.php], que é uma vista dinâmica HTML contendo código PHP. Em seguida, ocorrem duas coisas:
- o código PHP da visualização [vue-authentification.php] é carregado e interpretado. O resultado é uma visualização que chamaremos de [vue-authentification.html], que contém apenas o código HTML, ou mesmo CSS e JavaScript, mas não mais o PHP;
- esse código HTML é normalmente enviado ao cliente. Na verdade, isso ocorre com todo texto encontrado pelo interpretador PHP que não seja código PHP. Devido ao tempo de espera de saída, esse código HTML é colocado no buffer de saída sem ser enviado ao cliente;
- linha 71: a função [ob_get_clean] realiza duas ações:
- ela coloca na variável [$html] o conteúdo do buffer de saída, ou seja, a página [vue-authentification.html] que foi inserida nele;
- ela esvazia o buffer de saída. Para ele, tudo ocorre como se nada tivesse acontecido. Além disso, o cliente ainda não recebeu nada;
- linha 70: estamos aqui durante a execução da classe [HtmlResponse], que se encontra na pasta [Responses]. Para localizar a vista, é preciso, portanto, subir um nível para [..] e, em seguida, acessar a pasta [Views]. [__DIR__] é o nome absoluto da pasta na qual se encontra o script em execução; no nosso exemplo, a pasta [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses];
- linha 73: adicionamos aos cabeçalhos HTTP recebidos como parâmetro (linha 29) o cabeçalho que informa ao cliente que vamos enviar-lhe HTML;
- linha 75: solicita-se à classe pai que proceda ao envio efetivo da resposta ao cliente;
- linhas 77-81: registra-se em jSON a resposta [$content] fornecida pelo controlador secundário que processou a ação em andamento;
23.12.6. Testes [Postman]
Para testar de fato o modo HTML da sessão, precisaríamos examinar todas as visualizações. Faremos isso posteriormente. Realizaremos o seguinte teste:
Vamos examinar a lista de visualizações no arquivo de configuração:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
É possível identificar o contexto que gera alguns dos códigos de estado acima analisando os testes [Postman] realizados:

Vemos que o código de estado [700] corresponde a uma ação [init-session] bem-sucedida [2]. Acima, temos uma resposta jSON, mas ela pode ser do tipo XML ou HTML. É este último caso que será testado. De acordo com o arquivo de configuração, é a visualização [vue-authentification.php] que constitui a resposta HTML. Vamos verificar.

Acima:
- em [1-3], inicializa-se uma sessão HTML. Espera-se, portanto, uma resposta HTML;
- em [4-8], a resposta HTML do servidor;
- a aba [8] permite visualizar uma prévia do código HTML recebido;

- em [8-9], uma pré-visualização da visualização HTML;
23.13. O aplicativo web HTML
23.13.1. Apresentação das visualizações
O aplicativo web HTML utilizará quatro visualizações:
A visualização de autenticação:

A visualização de cálculo do imposto:

A visualização da lista de simulações:

A visualização de erros inesperados:

Vamos descrever essas visualizações uma a uma.
23.13.2. A visualização de autenticação
23.13.2.1. Apresentação da tela
A visualização de autenticação é a seguinte:

A visualização é composta por dois elementos que chamaremos de fragmentos:
- o fragmento [1] é gerado por um script [v-bandeau.php];
- o fragmento [2] é gerado por um script [v-authentification.php];
A visualização de autenticação é gerada pela seguinte página [vue-authentification.php]:
<?php
// dados de teste da página
// encapsulamos os dados da página em $page
…
?>
<!doctype html>
<html lang="fr">
<head>
<!-- Meta tags obrigatórias -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- banner com 1 linha e 12 colunas -->
<?php require "v-bandeau.php"; ?>
<!-- formulário de autenticação com 9 colunas -->
<div class="row">
<div class="col-md-9">
<?php require "v-authentification.php" ?>
</div>
</div>
<?php
// em caso de erro, exibe-se um alerta de erro
if ($modèle->error) {
print <<<EOT
<div class="row">
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
Les erreurs suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT;
}
?>
</div>
</body>
</html>
Comentários
- linha 7: um documento HTML começa com esta linha;
- linhas 8-44: a página HTML está encapsulada nas tags <html> </html>;
- linhas 9-16: cabeçalho (head) do documento HTML;
- linha 11: a tag <meta charset> indica que o documento está codificado em UTF-8;
- linha 12: a tag <meta name=’viewport’> define a exibição inicial da visualização: em toda a largura da tela que a exibe (width) em seu tamanho inicial (initial-scale), sem redimensionamento para se adaptar a uma tela menor (shrink-to-fit);
- linha 14: a tag <link rel=’stylesheet’> define o arquivo CSS, que controla a aparência da visualização. Aqui, utilizamos o framework CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- linha 15: a tag <title> define o título da página:

- linhas 17-43: o corpo da página da web está encapsulado nas tags <body></body>;
- linhas 18-42: a tag <div> delimita uma seção da página exibida. Os atributos [class] utilizados na visualização referem-se todos ao framework CSS Bootstrap. A tag <div class=’container’> delimita um contêiner Bootstrap;
- linha 20: inclui-se o script [v-bandeau.php]. Esse script gera o banner [1] da página. Descreveremos isso em breve;
- linhas 22-26: a tag <div class=’row’> delimita uma linha do Bootstrap. Essas linhas são compostas por 12 colunas;
- linha 23: a tag <div class=’col-md-9’> delimita uma seção de 9 colunas;
- linha 24: incluímos o script [v-authentification.php] que exibe o formulário de autenticação [2] da página. Descreveremos isso em breve;
- linha 27: a tag <?php insere o código PHP dentro da página HTML. Esse código é executado antes da exibição da página HTML e pode alterá-la;
- linha 29: todos os dados dinâmicos da visualização exibida serão encapsulados em um objeto [$modèle] do tipo [stdClass]. Trata-se de uma escolha arbitrária. Teríamos podido escolher um array associativo em vez disso, obtendo o mesmo resultado;
- linha 29: a autenticação falha se o usuário inserir credenciais incorretas. Nesse caso, a tela de autenticação é exibida novamente com uma mensagem de erro. O atributo [$modèle→error] indica se essa mensagem de erro deve ser exibida;
- linhas 30-39: essa sintaxe grava todo o texto situado entre os símbolos PHP <<<EOT (linha 30 – pode-se colocar o que se quiser no lugar de EOT=End Of Text) e o símbolo EOT da linha 39 (deve ser idêntico ao símbolo usado na linha 30). O símbolo deve ser escrito na primeira coluna da linha 39. As variáveis PHP localizadas no texto entre os dois símbolos EOT são interpretadas;
- linhas 33-36: delimitam uma área com fundo rosa (class="alert alert-danger") (linha 33);

- linha 34: um texto;
- linha 35: a tag HTML <ul> (lista não ordenada) exibe uma lista com marcadores. Cada elemento da lista deve ter a sintaxe <li>elemento</li>;
Vamos destacar neste código os elementos dinâmicos a serem definidos:
- [$modèle→error]: para exibir uma mensagem de erro;
- [$modèle→erreurs]: uma lista (no sentido do termo HTML) de mensagens de erro;
23.13.2.2. O fragmento [v-bandeau.php]
O fragmento [v-bandeau.php] exibe a barra superior em todas as visualizações do aplicativo web:

O código do fragmento [v-bandeau.php] é o seguinte:
<!-- Jumbotron do Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Comentários
- linhas 2-13: a faixa superior está encapsulada em uma seção do Bootstrap do tipo Jumbotron [<div class="jumbotron">]. Essa classe do Bootstrap aplica um estilo específico ao conteúdo exibido para destacá-lo;
- linhas 3-12: uma linha do Bootstrap;
- linhas 4-6: uma imagem [img] é colocada nas quatro primeiras colunas da linha;
- linha 5: a sintaxe [<?= $logo ?>] é equivalente à sintaxe [<?php print $logo ?>]. Em outras palavras, o valor do atributo [src] será o valor da variável PHP [$logo];
- linhas 7-11: as outras 8 colunas da linha (lembre-se de que há 12 no total) servirão para inserir um texto (linha 9) em letras grandes (<h1>, linhas 8-10);
Elementos dinâmicos:
- [$logo]: URL da imagem exibida no banner;
23.13.2.3. O fragmento [v-authentification.php]
O fragmento [v-authentification .php] exibe o formulário de autenticação do aplicativo web:

O código do fragmento [v-authentification.php] é o seguinte:
<!-- formulário HTML — os valores são enviados com a ação [authentifier-utilisateur] -->
<form method="post" action="main.php?action=authentifier-utilisateur">
<!-- título -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- formulário Bootstrap -->
<fieldset class="form-group">
<!-- 1ª linha -->
<div class="form-group row">
<!-- texto -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- campo de entrada de texto -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
</div>
</div>
<!-- 2ª linha -->
<div class="form-group row">
<!-- descrição -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- campo de entrada de texto -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe">
</div>
</div>
<!-- botão do tipo [submit] na terceira linha-->
<div class="form-group row">
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Comentários
- linhas 2-39: a tag <form> delimita um formulário HTML. Esse formulário geralmente apresenta as seguintes características:
- define campos de preenchimento (tags <input> nas linhas 17 e 27);
- possui um botão do tipo [submit] (linha 34) que envia os valores inseridos para o URL indicado no atributo [action] da tag [form] (linha 2). O método HTTP utilizado para consultar essa URL é especificado no atributo [method] da tag [form] (linha 2);
- aqui, quando o usuário clicar no botão [Valider] (linha 34), o navegador enviará (linha 2) os valores inseridos no formulário para o URL [main.php?action=authentifier-utilisateur] (linha 2);
- os valores enviados são aqueles inseridos pelo usuário nos campos de entrada das linhas 17 e 27. Eles serão enviados no formato [user=xx&password=yy]. Os nomes dos parâmetros [user, password] correspondem aos atributos [name] dos campos de entrada das linhas 17 e 27;
- linhas 5-7: uma seção Bootstrap para exibir um título em um fundo azul:

- linhas 10-37: um formulário Bootstrap. Todos os elementos do formulário serão então estilizados de determinada maneira;
- linhas 12-20: definem a primeira linha do formulário:
![]()
- a linha 14 define o texto [1] em três colunas. O atributo [for] da tag [label] vincula o rótulo ao atributo [id] do campo de entrada da linha 17;
- linhas 15-19: coloca o campo de entrada em um conjunto de quatro colunas;
- linha 17: a tag HTML [input] descreve um campo de entrada. Ela possui vários parâmetros:
- [type=’text’]: é um campo de entrada de texto. É possível digitar qualquer coisa nele;
- [class=’form-control’]: estilo Bootstrap para o campo de entrada;
- [id=’user’]: identificador do campo de entrada. Esse identificador é geralmente utilizado pelo CSS e pelo código JavaScript;
- [name=’user’]: nome do campo de entrada. É com esse nome que o valor digitado pelo usuário será enviado pelo navegador [user=xx];
- [placeholder=’invite’]: o texto exibido no campo de entrada quando o usuário ainda não digitou nada;
![]()
- [value=’valeur’]: o texto “valor” será exibido no campo de entrada assim que este for exibido, ou seja, antes que o usuário digite qualquer outra coisa. Esse mecanismo é utilizado em caso de erro para exibir a entrada que causou o erro. Aqui, esse valor será o valor da variável PHP [$modèle→login];
- linhas 21-30: um código semelhante para a digitação da senha;
- linha 27: [type=’password’] faz com que haja um campo de entrada de texto (é possível digitar qualquer coisa), mas os caracteres digitados ficam ocultos:
![]()
- linhas 32-36: uma terceira linha para o botão [Valider];
- linha 34: como ele possui o atributo [type=submit], ao clicar nesse botão, o navegador envia ao servidor os valores inseridos, conforme explicado anteriormente. O atributo CSS [class="btn btn-primary"] exibe um botão azul:

Resta-nos explicar uma última coisa. Na linha 2, o atributo [action="main.php?action=authentifier-utilisateur"] define um URL incompleto (ele não começa com http://machine:port/chemin). No nosso exemplo, todos os URL do aplicativo têm o formato [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. A visualização da autenticação será obtida com diversos URL:
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html];
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]
Esses URL indicam um documento [main.php] no caminho [http://localhost/php7/scripts-web/impots/version-12]. Esse será o caso de todos os URL desta aplicação. O parâmetro [action="main.php?action=authentifier-utilisateur"] receberá esse caminho como prefixo no momento do envio dos valores inseridos. Assim, esses valores serão enviados para o URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].
23.13.2.4. Testes visuais
É possível realizar testes nas visualizações bem antes de sua integração na aplicação. Trata-se, neste caso, de testar seu aspecto visual. Reuniremos todas as visualizações de teste na pasta [Tests] do projeto:

Para testar a visualização [vue-authentification.php], precisamos criar o modelo de dados que ela exibirá:
<?php
// dados de teste da página
//
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulamos os dados da página em $modèle
$modèle = new \stdClass();
// identificador do usuário
$modèle->login = "albert";
// lista de erros
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// constrói-se uma lista HTML dos erros
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// imagem do banner
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// gerando o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Meta tags obrigatórias -->
…
</head>
<body>
….
</body>
</html>
Comentários
- linhas 1-5: a vista de autenticação possui partes dinâmicas controladas pelo objeto [$modèle]. Esse objeto é chamado de modelo da vista. De acordo com uma das duas definições fornecidas para a sigla MVC, trata-se do M do MVC;
- linha 5: o modelo da visualização é calculado pela função [getModelForThisView];
- linha 9: o modelo da visualização será encapsulado em um tipo [stdClass];
- linhas 10-22: definem-se valores de teste para os elementos dinâmicos da visualização de autenticação;
O teste visual pode ser feito a partir do NetBeans:

Continuamos esses testes visuais até ficarmos satisfeitos com o resultado.
23.13.2.5. Cálculo do modelo da visualização
Uma vez determinado o aspecto visual da visualização, pode-se prosseguir com o cálculo do modelo da visualização em condições reais. Vale lembrar os códigos de estado que levam a essa visualização. Eles podem ser encontrados no arquivo de configuração:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Portanto, são os códigos de estado [700, 221, 400] que fazem com que a tela de autenticação seja exibida. Para descobrir o significado desses códigos, pode-se recorrer aos testes [Postman] realizados no aplicativo jSON:
- [init-session-json-700]: 700 é o código de estado após uma ação [init-session] bem-sucedida: é então exibido o formulário de autenticação em branco;
- [authentifier-utilisateur-221]: 221 é o código de status após uma ação [authentifier-utilisateur] que falhou (credenciais não reconhecidas): é então exibido o formulário de autenticação para que seja corrigido;
- [fin-session-400]: 400 é o código de status após uma ação [fin-session] bem-sucedida: exibe-se, então, o formulário de autenticação em branco;
Agora que sabemos em quais momentos o formulário de autenticação deve ser exibido, podemos calcular seu modelo em [vue-authentification.php]:

O código para o cálculo do modelo da visualização [vue-authentification.php] é o seguinte:
<?php
// herdam-se as seguintes variáveis
// Solicitação $request: a solicitação atual
// Sessão $session: a sessão do aplicativo
// matriz $config: a configuração do aplicativo
// matriz $content: a resposta do controlador
//
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calcula-se o modelo da visualização
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulando os dados da página em $modèle
$modèle = new stdClass();
// estado da aplicação
$état = $content["état"];
// o modelo depende do estado
switch ($état) {
case 700:
case 400:
// caso de exibição do formulário vazio
$modèle->login = "";
// não há erros a serem exibidos
$modèle->error = FALSE;
break;
case 221:
// autenticação incorreta
// o usuário inserido inicialmente é exibido novamente
$modèle->login = $request->request->get("user");
// há um erro a ser exibido
$modèle->error = TRUE;
// lista HTML de mensagens de erro — aqui há apenas uma
$modèle->erreurs = "<li>Echec de l'authentification</li>";
}
// resultado
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 3-6: são chamadas as variáveis herdadas da classe [HtmlResponse], que faz com que um [require] exiba a vista [vue-authentification.php];
- linhas 9-10: as classes do Symfony utilizadas no código da visualização;
- linhas 15-40: a função [getModelForThisView] é responsável por calcular o modelo da vista;
- linha 19: recupera-se o código de estado retornado pelo controlador que processou a ação em andamento;
- linhas 21-37: o modelo depende desse código de estado;
- linhas 22-28: caso em que é necessário exibir um formulário de autenticação em branco;
- linhas 29-37: caso de autenticação incorreta: exibe-se o identificador digitado pelo usuário e uma mensagem de erro. O usuário pode então tentar novamente a autenticação;
Um modelo específico foi criado para o banner [v-bandeau.php]:
<?php
// logotipo
$scheme = $request->server->get('REQUEST_SCHEME'); // http
$host = $request->server->get('SERVER_NAME'); // localhost
$port = $request->server->get('SERVER_PORT'); // 80
$uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
$champs = [];
preg_match("/(.+)\/.+?$/", $uri, $champs);
$root = $champs[1]; // /php7/scripts-web/impots/version-12
$modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Comentários
- a linha 16 utiliza a variável [$modèle→logo], que corresponde ao URL do logotipo do banner. Em vez de calcular essa variável quatro vezes para as quatro visualizações do aplicativo, esse cálculo é fatorizado no fragmento [v-bandeau.php];
- as linhas 1 a 11 mostram como construir o URL e o [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] a partir das informações encontradas no ambiente do servidor [$request→server];
23.13.2.6. Testes [Postman]
Já criamos consultas que geram os códigos [700, 221, 400], os quais exibem a tela de autenticação. Vamos relembrá-las:
- [init-session-html-700]: 700 é o código de status após uma ação [init-session] bem-sucedida: é então exibido o formulário de autenticação vazio;
- [authentifier-utilisateur-221]: 221 é o código de status após uma ação [authentifier-utilisateur] com falha (credenciais não reconhecidas): nesse caso, é exibido o formulário de autenticação para que seja corrigido;
- [fin-session-400]: 400 é o código de status após uma ação [fin-session] bem-sucedida: exibe-se, então, o formulário de autenticação em branco;
Basta reutilizá-los e verificar se eles exibem corretamente a tela de autenticação. Apresentaremos aqui apenas dois testes:
- [init-session-html-700]: início de uma sessão HTML;

- [authentifier-utilisateur-221]: autenticação do usuário [x, x];

Acima:
- a solicitação enviou a sequência [user=x&password=x];
- em [4], é exibida uma mensagem de erro;
- em [3], o usuário incorreto foi exibido novamente;
23.13.2.7. Conclusão
Conseguimos testar a visualização [vue-authentification.php] sem ter escrito as outras visualizações. Isso foi possível porque:
- todos os controladores estão implementados;
- o [Postman] nos permite enviar solicitações ao servidor sem precisar das visualizações. Ao escrever os controladores, é preciso estar ciente de que qualquer pessoa pode fazer isso. Portanto, é preciso estar preparado para lidar com solicitações que nenhuma vista permitiria. Elas são criadas manualmente em [Postman]. Nunca se deve pensar a priori que “essa solicitação é impossível”. É preciso verificar;
23.13.3. A visualização de cálculo do imposto
23.13.3.1. Apresentação da visualização
A visualização de cálculo do imposto é a seguinte:

A visualização tem três partes:
- 1: a barra superior é gerada pelo fragmento [v-bandeau.php], já apresentado;
- 2: o formulário de cálculo do imposto gerado pelo fragmento [v-calcul-impot.php];
- 3: um menu com dois links, gerado pelo fragmento [v-menu.php];
A visualização do cálculo do imposto é gerada pelo seguinte script [vue-calcul-impot.php]:

<?php
// herda-se as seguintes variáveis
// Solicitação $request: a solicitação atual
// Sessão $session: a sessão do aplicativo
// matriz $config: a configuração do aplicativo
// matriz $content: a resposta do controlador que processou a ação
//
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calcula-se o modelo da visualização
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulando os dados da página em $modèle
$modèle = new \stdClass();
…
// retorna-se o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Meta tags obrigatórias -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- banner -->
<?php require "v-bandeau.php"; ?>
<!-- layout de duas colunas -->
<div class="row">
<!-- o menu -->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- o formulário de cálculo -->
<div class="col-md-9">
<?php require "v-calcul-impot.php" ?>
</div>
</div>
<!-- caso de sucesso -->
<?php
if ($modèle->success) {
// é exibido um aviso de sucesso
print <<<EOT1
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-success" role="alert">
$modèle->impôt</br>
$modèle->décôte</br>\n
$modèle->réduction</br>\n
$modèle->surcôte</br>\n
$modèle->taux</br>\n
</div>
</div>
</div>
EOT1;
}
?>
<?php
if ($modèle->error) {
// lista de erros em 9 colunas
print <<<EOT2
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
L'erreur suivante s'est produite :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT2;
}
?>
</div>
</body>
</html>
Comentários
- comentamos apenas novidades que ainda não foram encontradas;
- linha 37: inclusão da barra superior da visualização na primeira linha Bootstrap da visualização;
- linhas 41-43: inclusão do menu que ocupará três colunas da segunda linha Bootstrap da visualização;
- linhas 45-47: inclusão do formulário de cálculo de imposto, que ocupará nove colunas da segunda linha Bootstrap da visualização;
- linhas 51-69: se o cálculo do imposto for bem-sucedido ([$modèle→success=TRUE]), o resultado do cálculo do imposto é exibido em um quadro verde (linhas 59-65). Essa caixa está na terceira linha Bootstrap da visualização (linha 54) e ocupa nove colunas (linha 58) à direita de três colunas vazias (linhas 55-57). Portanto, essa caixa ficará imediatamente abaixo do formulário de cálculo do imposto;
- linhas 71-87: se o cálculo do imposto falhar ([$modèle→error=TRUE]), uma mensagem de erro será exibida em um quadro rosa (linhas 80-83). Esse quadro está na terceira linha Bootstrap da visualização (linha 75) e ocupa nove colunas (linha 79) à direita de três colunas vazias (linhas 76-78). Portanto, esse quadro ficará imediatamente abaixo do formulário de cálculo do imposto;
23.13.3.2. O fragmento [v-calcul-impot.php]
O fragmento [v-calcul-impot.php] exibe o formulário de autenticação do aplicativo web:

O código do fragmento [v-calcul-impot.php] é o seguinte:
<!-- formulário HTML enviado -->
<form method="post" action="main.php?action=calculer-impot">
<!-- mensagem em 12 colunas com fundo azul -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- elementos do formulário -->
<fieldset class="form-group">
<!-- primeira linha em 9 colunas -->
<div class="row">
<!-- texto em 4 colunas -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- botões de opção em 5 colunas-->
<div class="col-md-5">
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
<label class="form-check-label" for="gridRadios1">
Oui
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
<label class="form-check-label" for="gridRadios2">
Non
</label>
</div>
</div>
</div>
<!-- segunda linha com 9 colunas -->
<div class="form-group row">
<!-- texto em 4 colunas -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- campo de entrada numérica para o número de filhos em 5 colunas -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
</div>
</div>
<!-- terceira linha com 9 colunas -->
<div class="form-group row">
<!-- texto em 4 colunas -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- campo de entrada numérica para o salário em 5 colunas -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
<small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
</div>
</div>
<!-- quarta linha, botão [submit] em 5 colunas -->
<div class="form-group row">
<div class="col-md-5">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Comentários
- linha 2: o formulário HTML será enviado (atributo [method]) para o URL [main.php?action=calculer-impot] (atributo [action]). Os valores lançados serão os valores dos campos de entrada:
- o valor do botão de opção marcado no formato:
- [marié=oui] se o botão de opção [Oui] estiver marcado (linhas 16-22). [marié] é o valor do atributo [name] da linha 18, [oui] é o valor do atributo [value] da linha 18;
- [marié=non] se o botão de opção [Non] estiver marcado (linhas 23-28). [marié] é o valor do atributo [name] da linha 24, [non] é o valor do atributo [value] da linha 24;
- o valor do campo de entrada numérica da linha 37 na forma [enfants=xx], em que [enfants] é o valor do atributo [name] da linha 37, e [xx] é o valor digitado pelo usuário no teclado;
- o valor do campo de entrada numérica da linha 46 na forma [salaire=xx], em que [salaire] é o valor do atributo [name] da linha 46, e [xx] é o valor digitado pelo usuário;
- o valor do botão de opção marcado no formato:
Por fim, o valor lançado terá o formato [marié=xx&enfants=yy&salaire=zz].
- os valores inseridos serão postados quando o usuário clicar no botão do tipo [submit] da linha 53;
- linhas 16-30: os dois botões de opção:
![]()
Os dois botões de opção fazem parte do mesmo grupo de botões de opção, pois possuem o mesmo atributo [name] (linhas 18, 24). O navegador garante que, em um grupo de botões de opção, apenas um esteja marcado a qualquer momento. Portanto, clicar em um desmarca aquele que estava marcado anteriormente;
- são botões de opção devido ao atributo [type="radio"] (linhas 18, 24);
- ao exibir o formulário (antes do preenchimento), um dos botões de opção deverá estar marcado: para isso, basta adicionar o atributo [checked=’checked’] à tag <input type="radio"> em questão. Isso é feito com variáveis dinâmicas:
- [<?= $modèle->checkedOui ?>] na linha 18;
- [<?= $modèle->checkedNon ?>] na linha 24;
Essas variáveis farão parte do modelo da visualização.
- linha 37: um campo de entrada numérica [type="number"] com um valor mínimo de 0 [min="0"]. Em navegadores recentes, isso significa que o usuário só poderá inserir um número >=0. Nesses mesmos navegadores recentes, a inserção pode ser feita por meio de um controle deslizante que pode ser clicado para aumentar ou diminuir o valor. O atributo [step="1"] da linha 37 indica que o controle deslizante funcionará com incrementos de 1 unidade. Isso faz com que o controle deslizante aceite apenas valores inteiros que variam de 0 a n, com um incremento de 1. Para a entrada manual, isso significa que números com vírgula não serão aceitos;
![]()
- linha 37: em certas exibições, o campo de entrada dos filhos deverá ser pré-preenchido com a última entrada feita nesse campo. Para isso, utiliza-se o atributo [value], que define o valor a ser exibido no campo de entrada. Esse valor será dinâmico e gerado pela variável [$modèle→enfants];
- linha 46: as explicações para a inserção do salário são as mesmas que para a inserção dos filhos;
- linha 53: o botão do tipo [submit], que aciona o POST com os valores inseridos no URL e no [main.php?action=calculer-impot];

23.13.3.3. O fragmento [v-menu.php]
Este fragmento exibe um menu à esquerda do formulário de cálculo do imposto:

O código deste fragmento é o seguinte:
<!-- menu Bootstrap -->
<nav class="nav flex-column">
<?php
// exibição de uma lista de links HTML
foreach($modèle->optionsMenu as $texte=>$url){
print <<<EOT3
<a class="nav-link" href="$url">$texte</a>
EOT3;
}
?>
</nav>
Comentários
- linhas 2-11: a tag HTML [nav] delimita uma parte do documento HTML que apresenta links de navegação para outros documentos;
- linha 7: a tag HTML [a] introduz um link de navegação:
- [$url]: é o URL para o qual se navega ao clicar no link [$texte]. Trata-se, portanto, de uma operação [GET $url] realizada pelo navegador. Se [$url] for um URL relativo, então ele é prefixado pela raiz do URL atualmente exibido no endereço do navegador. Assim, para obter o link [1], quando o URL atual do navegador é do tipo [http://chemin/main.php?paramètres], criaremos o link:
- linha 5: o modelo [$modèle→optionsMenu] do fragmento será uma tabela com o seguinte formato:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
- linhas 2, 7: as classes CSS e [nav, flex-column, nav-link] são classes do Bootstrap que definem a aparência do menu;
23.13.3.4. Teste visual
Reunimos esses diferentes elementos na pasta [Tests] e criamos um modelo de teste para a visualização [vue-calcul-impot.php]:

O modelo de dados da visualização [vue-calcul-impot] será o seguinte:
<?php
// dados de teste da página
//
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulando os dados da página em $modèle
$modèle = new \stdClass();
// formulário
$modèle->checkedOui = "";
$modèle->checkedNon = 'checked="checked"';
$modèle->enfants = 2;
$modèle->salaire = 300000;
// mensagem de sucesso
$modèle->success = TRUE;
$modèle->impôt = "Montant de l'impôt : 1000 euros";
$modèle->décôte = "Décôte : 15 euros";
$modèle->réduction = "Réduction : 20 euros";
$modèle->surcôte = "Surcôte : 0 euros";
$modèle->taux = "Taux d'imposition : 14 %";
// mensagem de erro
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// cria-se uma lista HTML dos erros
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// menu
$modèle->optionsMenu = [
''Lista de simulações' => 'main.php?action=lista-simulações',
''Fim da sessão' => 'main.php?action=fim-da-sessão'];
// imagem do banner
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// exibição do modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 7-39: inicializamos todas as partes dinâmicas da visualização [vue-calcul-impot.php] e dos fragmentos [v-calcul-impot.php] e [v-menu.php];
Testamos a visualização [vue-calcul-impot.php]:

Obtém-se o seguinte resultado:

Trabalhamos nessa visualização até que o resultado obtido visualmente nos satisfaça. Em seguida, podemos passar para a integração da visualização no aplicativo web que está sendo desenvolvido.
23.13.3.5. Cálculo do modelo da visualização

Uma vez definido o aspecto visual da visualização, podemos prosseguir com o cálculo do modelo da visualização em condições reais. Vale lembrar os códigos de estado que levam a essa visualização. Eles podem ser encontrados no arquivo de configuração:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Portanto, são os códigos de estado [200, 300, 341, 350, 800] que fazem com que a tela de autenticação seja exibida. Para descobrir o significado desses códigos, pode-se consultar os testes [Postman] realizados no aplicativo jSON:
- [authentifier-utilisateur-200]: 200 é o código de estado após uma ação [authentifier-itilisateur] bem-sucedida: é então exibido o formulário de cálculo de imposto em branco;
- [calculer-impot-300]: 300 é o código de status após o sucesso da ação [calculer-impot]. Em seguida, é exibido o formulário de cálculo com os dados inseridos e o valor do imposto. O usuário pode então realizar outro cálculo;
- [fin-session-400]: 400 é o código de status após o sucesso da ação [fin-session]: é exibido o formulário de autenticação vazio;
- o código de estado [341] é aquele obtido para um cálculo de imposto válido, mas a falta de conexão com o SGBD provoca um erro;
- o código de estado [350] é aquele obtido para um cálculo de imposto válido, mas a falta de conexão com o servidor [Redis] causa um erro;
- o código de estado [800] será apresentado posteriormente. Ainda não o encontramos;
- partimos aqui da hipótese de que o usuário utiliza um navegador recente. Assim, no formulário em análise, não é possível digitar números negativos, sequências de caracteres não numéricos ou números com vírgula nos campos de entrada [enfants, salaire]. Com navegadores mais antigos, isso seria possível. Trataremos esses erros como erros inesperados e, então, exibiremos a visualização [vue-erreurs];
Agora que sabemos em quais momentos o formulário de cálculo do imposto deve ser exibido, podemos definir seu modelo em [vue-calcul-impot.php]:
<?php
// herda-se as seguintes variáveis
// Solicitação $request: a solicitação em andamento
// Sessão $session: a sessão do aplicativo
// matriz $config: a configuração do aplicativo
// array $content: a resposta do controlador que processou a ação
//
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calcula-se o modelo da visualização
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulando os dados da página em $modèle
$modèle = new \stdClass();
// estado da aplicação
$état = $content["état"];
// o modelo depende do estado
switch ($état) {
case 200 :
case 800:
// exibição inicial de um formulário vazio
$modèle->success = FALSE; $modèle->errror = FALSE;
$modèle->checkedNon = 'checked="checked"';
$modèle->checkedOui = "";
$modèle->enfants = "";
$modèle->salaire = "";
break;
case 300:
// cálculo bem-sucedido — exibição do resultado
$modèle->success = TRUE;
$modèle->error = FALSE;
$modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
$modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
$modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
$modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
$modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
// formulário restaurado com os valores inseridos
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
break;
case 341:
// banco de dados HS
case 350:
// servidor Redis HS
// formulário restaurado com os valores inseridos
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
// erro
$modèle->success = FALSE;
$modèle->error = TRUE;
$modèle->erreurs = "<li>{$content["réponse"]}</li>";
break;
}
//menu
$modèle->optionsMenu = [
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session"];
// exibindo o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
<title>Application impots</title>
</head>
<body>
…
</body>
</html>
Comentários
- linhas 22-30: exibição de um formulário vazio;
- linhas 31-45: caso de cálculo de imposto bem-sucedido. São exibidos novamente os valores inseridos, bem como o valor do imposto;
- linhas 46-59: caso de falha no cálculo do imposto devido à indisponibilidade de um dos servidores [Redis] ou [MySQL];
- linhas 62-64: cálculo das duas opções do menu;
23.13.3.6. Testes [Postman]
O teste [calculer-impot-300] nos permite obter o código de status 300. Ele corresponde a um cálculo de imposto bem-sucedido:

- no [3], os valores que levaram ao resultado [2];
Vamos testar um caso de erro: o erro [350] devido à indisponibilidade do servidor [Redis]:

23.13.4. A visualização da lista de simulações
23.13.4.1. Apresentação da visualização
A visualização que apresenta a lista de simulações é a seguinte:

A visualização gerada pelo script [vue-liste-simulations] possui três partes:
- 1: a faixa superior é gerada pelo fragmento [v-bandeau.php], já apresentado;
- 2: a tabela de simulações gerada pelo fragmento [v-liste-simulations.php];
- 3: um menu com dois links, gerado pelo fragmento [v-menu.php];
A visualização das simulações é gerada pelo seguinte script [vue-liste-simulations.php]:

<?php
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsula-se os dados da página em $modèle
$modèle = new \stdClass();
…
// retorna-se o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Meta tags obrigatórias -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- banner -->
<?php require "v-bandeau.php"; ?>
<!-- layout de duas colunas -->
<div class="row">
<!-- menu de três colunas-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- lista de simulações em 9 colunas-->
<div class="col-md-9">
<?php require "v-liste-simulations.php" ?>
</div>
</div>
</div>
</body>
</html>
Comentários
- linha 28: inclusão do banner do aplicativo [1];
- linha 33: inclusão do menu [2]. Ele será exibido em três colunas abaixo do banner;
- linha 37: inclusão da tabela de simulações [3]. Ela será exibida em nove colunas abaixo do banner e à direita do menu;
Já comentamos dois dos três fragmentos desta visualização:
O fragmento [v-liste-simulations.php] é o seguinte:
<!-- mensagem com fundo azul -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- tabela de simulações -->
<table class="table table-sm table-hover table-striped">
<!-- cabeçalhos das seis colunas da tabela -->
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Marié</th>
<th scope="col">Nombre d'enfants</th>
<th scope="col">Salaire annuel</th>
<th scope="col">Montant impôt</th>
<th scope="col">Surcôte</th>
<th scope="col">Décôte</th>
<th scope="col">Réduction</th>
<th scope="col">Taux</th>
<th scope="col"></th>
</tr>
</thead>
<!-- corpo da tabela (dados exibidos) -->
<tbody>
<?php
$i = 0;
// cada simulação é exibida ao percorrer a tabela de simulações
foreach ($modèle->simulations as $simulation) {
// exibição de uma linha da tabela com 6 colunas — tag <tr>
// coluna 1: cabeçalho da linha (n.º da simulação) — tag <th scope='row'>
// coluna 2: valor do parâmetro [marié] - tag <td>
// coluna 3: valor do parâmetro [enfants] - tag <td>
// coluna 4: valor do parâmetro [salaire] - tag <td>
// coluna 5: valor do parâmetro [impôt] (do imposto) - tag <td>
// coluna 6: valor do parâmetro [surcôte] - tag <td>
// coluna 7: valor do parâmetro [décôte] - tag <td>
// coluna 8: valor do parâmetro [réduction] - tag <td>
// coluna 9: valor do parâmetro [taux] (do imposto) - tag <td>
// coluna 10: link para excluir a simulação - tag <td>
print <<<EOT
<tr>
<th scope="row">$i</th>
<td>{$simulation["marié"]}</td>
<td>{$simulation["enfants"]}</td>
<td>{$simulation["salaire"]}</td>
<td>{$simulation["impôt"]}</td>
<td>{$simulation["surcôte"]}</td>
<td>{$simulation["décôte"]}</td>
<td>{$simulation["réduction"]}</td>
<td>{$simulation["taux"]}</td>
<td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
</tr>
EOT;
$i++;
}
?>
</tr>
</tbody>
</table>
Comentários
- uma tabela HTML é criada com a tag <table> (linhas 6 e 58);
- os cabeçalhos das colunas da tabela são definidos dentro de uma tag <thead> (cabeçalho da tabela, linhas 8 e 21). A tag <tr> (linha da tabela, linhas 9 e 20) delimita uma linha. Nas linhas 10 a 15, a tag <th> (cabeçalho da tabela) define um cabeçalho de coluna. Portanto, há dez deles. [scope="col"] indica que o cabeçalho se aplica à coluna. [scope="row"] indica que o cabeçalho se aplica à linha;
- linhas 23-57: a tag <tbody> delimita os dados exibidos pela tabela;
- linhas 40-51: a tag <tr> delimita uma linha da tabela;
- linha 41: a tag <th scope=’row’> define o cabeçalho da linha;
- linhas 42-50: cada tag <td> define uma coluna da linha;
- linha 27: a lista de simulações pode ser encontrada no modelo [$modèle→simulations], que é uma tabela associativa;
- linha 50: um link para excluir a simulação. O modelo URL retoma o número exibido na primeira coluna da tabela (linha 41);
23.13.4.2. Teste visual
Reunimos esses diferentes elementos na pasta [Tests] e criamos um modelo de teste para a visualização [vue-liste-simulations.php]:

O modelo de dados da visualização [vue-liste-simulations] será o seguinte:
<?php
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsula-se os dados da página em $modèle
$modèle = new \stdClass();
// coloca-se as simulações no formato esperado pela página
$modèle->simulations = [
[
"marié" => "oui",
"enfants" => 2,
"salaire" => 60000,
"impôt" => 448,
"décôte" => 100,
"réduction" => 20,
"surcôte" => 0,
"taux" => 0.14
],
[
"marié" => "non",
"enfants" => 2,
"salaire" => 200000,
"impôt" => 25600,
"décôte" => 0,
"réduction" => 0,
"surcôte" => 8400,
"taux" => 0.45
]
];
// as opções do menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// imagem do banner
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// geramos o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 9-30: a tabela de simulações exibida pela tabela HTML;
- linhas 32-34: a tabela de opções de menu;
Vamos exibir essa vista:

Obtemos o seguinte resultado:

Trabalhamos nessa visualização até que o resultado obtido visualmente nos agrade. Em seguida, podemos passar para a integração da visualização no aplicativo web que estamos desenvolvendo.
23.13.4.3. Cálculo do modelo da visualização

Uma vez definido o aspecto visual da visualização, podemos prosseguir com o cálculo do modelo da visualização em condições reais. Vale lembrar os códigos de estado que levam a essa visualização. Eles podem ser encontrados no arquivo de configuração:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Portanto, são os códigos de estado [500, 600] que fazem com que a visualização das simulações seja exibida. Para entender o significado desses códigos, pode-se recorrer aos testes [Postman] realizados no aplicativo jSON:
- [lister-simulations-500]: 500 é o código de estado após uma ação [lister-simulations] bem-sucedida: é então exibida a lista das simulações realizadas pelo usuário;
- [supprimer-simulation-600]: 600 é o código de status após o sucesso da ação [supprimer-simulation]. Em seguida, é exibida a nova lista de simulações obtida após essa exclusão;
Agora que sabemos em quais momentos a lista de simulações deve ser exibida, podemos calcular seu modelo em [vue-liste-simulations.php]:
<?php
// herdam-se as seguintes variáveis
// Solicitação $request: a solicitação em andamento
// Sessão $session: a sessão do aplicativo
// matriz $config: a configuração do aplicativo
// matriz $content: a resposta do controlador
// sem erros possíveis
// array $content: a resposta do controlador
//
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calcula-se o modelo da visualização
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulando os dados da página em $modèle
$modèle = new \stdClass();
// colocamos as simulações no formato esperado pela página
// elas são encontradas na resposta do controlador que executou a ação
// na forma de uma matriz de objetos do tipo [Simulation]
$objetsSimulation = $content["réponse"];
// cada objeto [Simulation] será transformado em uma tabela associativa
$modèle->simulations = [];
foreach ($objetsSimulation as $objetSimulation) {
$modèle->simulations[] = [
"marié" => $objetSimulation->getMarié(),
"enfants" => $objetSimulation->getEnfants(),
"salaire" => $objetSimulation->getSalaire(),
"impôt" => $objetSimulation->getImpôt(),
"surcôte" => $objetSimulation->getSurcôte(),
"décôte" => $objetSimulation->getdécôte(),
"réduction" => $objetSimulation->getRéduction(),
"taux" => $objetSimulation->getTaux()
];
}
// as opções do menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// geramos o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 26-36: cálculo do modelo [$modèle→simulations] utilizado pelo fragmento [v-liste-simulations.php];
- linhas 39-41: cálculo do modelo [$modèle→optionsMenu] utilizado pelo fragmento [v-menu.php];
23.13.4.4. Testes [Postman]
O teste [lister-simulations-500] nos permite obter o código de status 500. Ele corresponde a uma solicitação para visualizar as simulações:

O teste [supprimer-simulation-600] nos permite obter o código de status 600. Ele corresponde à exclusão bem-sucedida da simulação nº 0. O resultado retornado é uma lista de simulações com uma simulação a menos:

23.13.5. Visualização de erros inesperados
Chamamos aqui de erro inesperado um erro que não deveria ter ocorrido no contexto do uso normal do aplicativo web.
Tomemos como exemplo o teste [Postman] [calculer-impot-3xx], definido da seguinte forma:

- em [1-3], uma solicitação POST com a ação [calculer-impot];
- em [4-6]: aqui é possível definir o que se desejar para os três parâmetros do POST:
- [4]: o parâmetro [marié] está faltando;
- [5-6]: os parâmetros [enfants, salaire] estão presentes, mas são inválidos;
- no [9], esses três erros são sinalizados com o código de status 338;
No entanto, no formulário HTML do aplicativo web, esse caso não pode ocorrer:
- todos os parâmetros estão presentes;
- o parâmetro [marié], cujo valor é obtido a partir dos atributos [value] de dois botões de opção, tem necessariamente um dos valores [oui] ou [non];
- em um navegador recente, os atributos <input type=’number’ min=’0’ step=’1’ …> fazem com que os valores inseridos para “filhos” e “salário” sejam necessariamente números inteiros >=0;
No entanto, nada impede que um usuário selecione [Postman] e envie ao nosso servidor o teste [calcul-impot-3xx] mencionado acima. Vimos que nosso aplicativo web soube responder corretamente a essa solicitação. Chamaremos de “erro inesperado” um erro que não deveria ocorrer no contexto do aplicativo HTML. Se ele ocorrer, é provável que alguém esteja tentando “hackear” o aplicativo. Por uma questão de didática, decidimos exibir uma tela de erros para esses casos. Na prática, poderíamos exibir novamente a última página enviada ao cliente. Para isso, basta registrar na sessão a última resposta HTML enviada. Em caso de erro inesperado, reenviamos essa resposta. Assim, o usuário terá a impressão de que o servidor não responde aos seus erros, já que a página exibida não muda.
23.13.5.1. Apresentação da visualização
A visualização que apresenta os erros inesperados é a seguinte:

A visualização gerada pelo script [vue-erreurs.php] tem três partes:
- 1: a faixa superior é gerada pelo fragmento [v-bandeau.php] já apresentado;
- 2: o(s) erro(s) inesperado(s);
- 3: um menu com três links, gerado pelo fragmento [v-menu.php];
A exibição dos erros inesperados é gerada pelo seguinte script [vue-erreurs.php]:

<?php
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsula-se os dados da página em $modèle
$modèle = new \stdClass();
…
// retorna-se o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Meta tags obrigatórias -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- banner em 12 colunas -->
<?php require "v-bandeau.php"; ?>
<!-- linha com duas colunas -->
<div class="row">
<!-- menu em 3 colunas-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- lista de erros -->
<div class="col-md-9">
<?php
print <<<EOT
<div class="alert alert-danger" role="alert">
Les erreurs inattendues suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
EOT;
?>
</div>
</div>
</div>
</body>
</html>
Comentários
- linha 27: inclusão do banner do aplicativo [1];
- linha 32: inclusão do menu [2]. Ele será exibido em três colunas abaixo do banner;
- linhas 34-44: exibição da área de erros em nove colunas;
- linhas 37-44: a operação [print], que exibe os erros inesperados;
- linha 38: essa exibição será feita em um quadro Bootstrap com fundo rosa;
- linha 39: um texto de apresentação;
- linha 40: a tag <ul> delimita uma lista com marcadores. Essa lista com marcadores é fornecida pelo modelo [$modèle->erreurs];
Já comentamos os dois fragmentos desta visualização:
23.13.5.2. Teste visual
Reunimos esses diferentes elementos na pasta [Tests] e criamos um modelo de teste para a visualização [vue-erreurs.php]:

O modelo de dados da visualização [vue-erreurs.php] será o seguinte:
<?php
// calcula-se o modelo da visualização
$modèle = getModelForThisView();
function getModelForThisView(): object {
// os dados da página são encapsulados em $modèle
$modèle = new \stdClass();
// a tabela de erros inesperados
$erreurs = ["erreur1", "erreur2"];
// construímos a lista HTML de erros
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// opções do menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// imagem do banner
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// retornando o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 9-15: construção da lista HTML de erros;
- linhas 17-20: a tabela de opções do menu;
Vamos exibir essa visualização:

Obtemos o seguinte resultado:

Trabalhamos nessa visualização até que o resultado obtido visualmente nos agrade. Em seguida, podemos passar para a integração da visualização no aplicativo web que estamos desenvolvendo.
23.13.5.3. Cálculo do modelo da visualização

Uma vez definido o aspecto visual da visualização, podemos prosseguir com o cálculo do modelo da visualização em condições reais. Vale lembrar os códigos de estado que levam a essa visualização. Eles podem ser encontrados no arquivo de configuração:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Portanto, são os códigos de estado que não constam nas linhas [2-4] que fazem com que a visualização de erros inesperados seja exibida.
O código de cálculo do modelo da visualização [vue-erreurs.php] é o seguinte:
<?php
// herdam-se as seguintes variáveis
// Solicitação $request: a solicitação atual
// Sessão $session: a sessão do aplicativo
// matriz $config: a configuração do aplicativo
// array $content: a resposta do controlador
//
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calcula-se o modelo da visualização
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulando os dados da página em $modèle
$modèle = new \stdClass();
// recuperam-se os erros na resposta do controlador
$réponse = $content["réponse"];
if (!is_array($réponse)) {
// uma única mensagem de erro
$erreurs = [$réponse];
} else {
// várias mensagens de erro
$erreurs = $réponse;
}
// construímos a lista HTML dos erros
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// opções do menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// retorna-se o modelo
return $modèle;
}
?>
<!-- documento HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Comentários
- linhas 19-32: cálculo do modelo [$modèle→erreurs] utilizado pela visualização [vue-erreurs.php];
- linhas 34-37: cálculo do modelo [$modèle→optionsMenu] utilizado pelo fragmento [v-menu.php];
23.13.5.4. Testes [Postman]
O teste [calculer-impot-3xx] nos permite obter o código de estado 338, que não é um código de estado esperado. A resposta HTML é, então, a seguinte:

23.13.6. Implementação das ações do menu do aplicativo
Abordaremos aqui a implementação das ações do menu. Vamos relembrar o significado dos links que encontramos
Visualização | Link | Destino | Função |
Cálculo do imposto | [Liste des simulations] | [main.php?action=lister-simulations] | Solicitar a lista de simulações |
[Fin de session] | [main.php?action=fin-session] | ||
Lista de simulações | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | Exibir a visualização do cálculo do imposto |
[Fin de session] | [main.php?action=fin-session] | ||
Erros inesperados | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | Exibir a visualização do cálculo do imposto |
[Liste des simulations] | [main.php?action=lister-simulations] | ||
[Fin de session] | [main.php?action=fin-session] |
É importante lembrar que um clique em um link gera um GET para o destino do link. As ações [lister-simulations, fin-session] foram implementadas com uma operação GET, o que nos permite defini-las como destinos de links. Quando a ação é realizada por meio de um POST, o uso de um link não é mais possível, a menos que seja associado a JavaScript.
Das ações acima, verifica-se que a ação [afficher-calcul-impot] ainda não foi implementada. Trata-se de uma operação de navegação entre duas visualizações: o servidor jSON ou XML não tem motivo para implementá-la, pois não possui o conceito de visualização. É o servidor HTML que introduz esse conceito.
Portanto, precisamos implementar a ação [afficher-calcul-impot]. Isso nos permitirá revisar o procedimento de implementação de uma ação dentro do servidor.
Primeiramente, precisamos adicionar um novo controlador secundário. Vamos chamá-lo de [AfficherCalculImpotController]:

Esse controlador deve ser adicionado ao arquivo de configuração [config.json]:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
…
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
…
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
…
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
- linha 15: o novo controlador;
- linha 30: a nova ação e seu controlador;
- linha 35: o novo controlador retornará o código de estado 800. Ao mudar de tela, não pode haver erro;
O controlador [AfficherCalculImpotController.php] será o seguinte:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;
class AfficherCalculImpotController implements InterfaceController {
// $config é a configuração do aplicativo
// processamento de uma solicitação Request
// utiliza a sessão Session e pode modificá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// mudança de visualização — basta definir um código de estado
return [Response::HTTP_OK, 800, ["réponse" => ""], []];
}
}
Comentários
- linha 10: assim como os outros controladores secundários, o novo controlador implementa a interface [InterfaceController];
- as alterações de visualização são simples de implementar: basta definir o código de estado associado à visualização de destino, neste caso o código 800, conforme visto acima;
23.13.7. Testes em condições reais
O código foi escrito e cada ação testada com [Postman]. Resta-nos testar a sequência de visualizações em situação real. Precisamos de uma maneira de inicializar a sessão HTML. Sabemos que é preciso enviar os parâmetros [action=init-session&type=html] ao servidor. Para evitar ter que digitá-los na barra de endereços do navegador, vamos adicionar o script [index.php] ao nosso aplicativo:

O script [index.php] será o seguinte:
<?php
// redirecionamento para [main.php] no modo [html]
header('Location: main.php?action=init-session&type=html');
- linha 4: [header] é uma função PHP que adiciona um cabeçalho HTTP à resposta. O cabeçalho HTTP [Location: main.php?action=init-session&type=html] solicita que o navegador do cliente seja redirecionado para o destino URL indicado em [Location]. O script [index.php] é solicitado junto com o URL e o [http://localhost/php7/scripts-web/impots/version-12/index.php]. Quando o navegador do cliente receber o redirecionamento para o URL relativo ao [main.php?action=init-session&type=html], ele solicitará o URL absoluto [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html] e a sessão HTML será iniciada;
O URL de inicialização pode ser simplificado para [http://localhost/php7/scripts-web/impots/version-12/]. Caso nenhuma página seja especificada no URL, as páginas [index.html, index.php] são utilizadas por padrão. Nesse caso, o script [index.php] será, portanto, utilizado;
Vamos lá: apresentaremos agora algumas sequências de visualizações.
No nosso navegador, ativamos o rastreamento de solicitações (F12 no Firefox) e solicitamos a página inicial URL:

- em [4], a primeira resposta do servidor é um redirecionamento 302:
- em [5], uma nova solicitação é enviada para o URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html];
Vamos examinar mais de perto o redirecionamento 302:

- em [8], o código HTTP [302] é um código de redirecionamento: informa-se ao navegador do cliente que o URL solicitado foi movido. A nova URL é especificada como [9]. O navegador seguirá esse redirecionamento com uma nova solicitação GET:

- para [12-13], a nova solicitação feita pelo navegador;
Vamos preencher o formulário que recebemos;

Então, vamos fazer algumas simulações:


Vamos solicitar a lista das simulações:

Vamos excluir a primeira simulação:

Vamos encerrar a sessão:

O leitor é convidado a realizar outros testes.
23.14. Cliente do serviço web jSON
23.14.1. Arquitetura cliente/servidor

Vamos nos concentrar agora no cliente jSON [A] do serviço web [B]. O cliente [A], assim como o serviço web [B], possui uma estrutura em camadas:

Essa arquitetura se reflete na seguinte organização do código:

A maioria das classes já foi apresentada e explicada:
parágrafo com link. | |
parágrafo com link. | |
parágrafo com link. | |
parágrafo com link. | |
parágrafo com link. | |
parágrafo com link. |
23.14.2. A camada [dao]

23.14.2.1. Interface
A interface da camada [dao] será a seguinte [InterfaceClientDao.php]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceClientDao {
// leitura dos dados do contribuinte
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// registro dos resultados
public function saveResults(string $resultsFilename, array $simulations): void;
// autenticação
public function authentifierUtilisateur(String $user, string $password): void;
// lista de simulações
public function listerSimulations(): array;
// excluir uma simulação
public function supprimerSimulation(int $numéro): array;
// início da sessão
public function initSession(string $type = 'json'): void;
// fim da sessão
public function finSession(): void;
}
Comentários
- linha 9: o método [getTaxPayersData] permite processar o arquivo jSON contendo os dados dos contribuintes. Esse método é implementado pela função [TraitDao], já comentada (parágrafo “link”);
- linha 15: o método [saveResults] permite salvar os resultados de vários cálculos de imposto em um arquivo jSON. Nesse caso também, esse método é implementado pela função [TraitDao], já comentada (parágrafo com link);
- linhas 12, 18, 21, 27, 30: foi criado um método para cada uma das ações aceitas pelo serviço web;
23.14.2.2. Implementação
A interface [InterfaceClientDao] é implementada pela seguinte classe [ClientDao]:
<?php
namespace Application;
// dependências
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
class ClientDao implements InterfaceClientDao {
// uso de um Trait
use TraitDao;
// atributos
private $urlServer;
private $sessionCookie;
private $verbose;
// construtor
public function __construct(string $urlServer, bool $verbose = TRUE) {
$this->urlServer = $urlServer;
$this->verbose = $verbose;
}
…
}
Comentários
- linhas 18-21: o construtor recebe dois parâmetros:
- o URL [$urlServer] do serviço web jSON;
- um valor booleano [$verbose] que, em TRUE, indica que a classe deve exibir as respostas do servidor no console;
- linha 14: o cookie de sessão. Sua função foi descrita na versão 09 do cliente (parágrafo “link”);
- linha 11: a classe utiliza o traço [TraitDao], que implementa dois métodos da interface:
- [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array];
- [function calculerImpot(string $marié, int $enfants, int $salaire): Simulation];
23.14.2.2.1. Método [initSession]
O método [initSession] é implementado da seguinte forma:
public function initSession(string $type = 'json'): void {
// cria-se um cliente HTTP
$httpClient = HttpClient::create();
// envia-se a solicitação ao servidor sem autenticação
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "init-session",
"type" => $type
],
"verify_peer" => false
]);
// recupera-se a resposta
$this->getResponse($response);
// recupera-se 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];
}
}
}
}
Como a ação [init-session] deve ser a primeira ação solicitada ao serviço web, o método [initSession] será o primeiro método da camada [dao] a ser chamado.
Comentários
- linha 1: o tipo de sessão desejado é passado como parâmetro. Na ausência de parâmetro, será iniciada uma sessão jSON;
- linhas 5-11: é feita uma solicitação GET ao serviço web;
- linhas 7-8: os dois parâmetros da GET;
- linha 10: em caso de comunicações seguras (esquema https), o certificado de segurança enviado pelo serviço web não será verificado;
- linha 13: o método [getResponse] recupera a resposta do servidor. Ele a retorna na forma de uma matriz. Aqui, o resultado do método não é utilizado. O método [getResponse] lança uma exceção se o código HTTP da resposta do serviço web for diferente de 200 OK;
- linhas 14-25: como o método [initSession] é o primeiro método da camada [dao] a ser executado, recupera-se o cookie de sessão para que os métodos seguintes possam reenviá-lo ao serviço web. Esse código já foi comentado na versão 09;
23.14.2.2.2. O método [getResponse]
O método [getResponse] é responsável por processar a resposta do serviço web:
private function getResponse(CurlResponse $response) {
// a resposta é recuperada
$json = $response->getContent(false);
// logs
if ($this->verbose) {
print "$json\n";
}
// recuperando o status da resposta
$statusCode = $response->getStatusCode();
// erro?
if ($statusCode !== 200) {
// ocorreu um erro
throw new ExceptionImpots($json);
}
// enviando a resposta
$array = json_decode($json, true);
return $array["réponse"];
}
Comentários
- linha 1: o método é privado;
- linha 1: o parâmetro do método é a resposta do serviço web do tipo [Symfony\Component\HttpClient\Response\CurlResponse], o tipo de resposta do Symfony, quando [HttpClient] é implementado por [CurlClient], ou seja, pela biblioteca [curl];
- linha 3: recupera-se a resposta jSON do servidor. Vale lembrar que o parâmetro [false] existe para impedir que o Symfony lance uma exceção quando o status da resposta HTTP do servidor estiver no domínio [3xx, 4xx, 5xx];
- linhas 5-7: se estivermos no modo [$verbose], exibimos a resposta do servidor no console;
- linhas 9-14: se o status da resposta HTTP do servidor for diferente de 200, lança-se uma exceção com a resposta jSON do servidor como mensagem de erro;
- linha 16: a string jSON é decodificada em um array;
- linha 17: as informações úteis estão em [$array["réponse"]];
23.14.2.2.3. O método [authentifierUtilisateur]
O método [authentifierUtilisateur] é o seguinte:
public function authentifierUtilisateur(string $user, string $password): void {
// cria-se um cliente HTTP
$httpClient = HttpClient::create();
// enviando a solicitação ao servidor com autenticação
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "authentifier-utilisateur"
],
"body" => [
"user" => $user,
"password" => $password
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// recuperamos a resposta
$this->getResponse($response);
}
Comentários
- linha 5: a solicitação do cliente é um POST;
- linhas 6-8: parâmetros no URL;
- linhas 9-12: parâmetros do POST;
- linha 14: o cookie de sessão;
- linha 17: lemos a resposta. Sabemos que, em caso de erro (código HTTP diferente de 200), o método [getResponse] lança ele mesmo uma exceção;
23.14.2.2.4. O método [calculerImpot]
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
// cria-se um cliente HTTP
$httpClient = HttpClient::create();
// envia-se a solicitação ao servidor sem autenticação, mas com o cookie de sessão
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "calculer-impot"],
"body" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// recuperamos a resposta
$array = $this->getResponse($response);
return (new Simulation())->setFromArrayOfAttributes($array);
}
Comentários
- linhas 6-7: o único parâmetro do URL;
- linhas 8-12: os três parâmetros do POST (linha 5);
- linha 17: a resposta é processada;
- linha 18: se chegamos até aqui, é porque o método [getResponse] não lançou nenhuma exceção. Retornamos um objeto [Simulation] inicializado com a matriz retornada pelo [getResponse];
23.14.2.2.5. O método [listerSimulations]
public function listerSimulations(): array {
// cria-se um cliente HTTP
$httpClient = HttpClient::create();
// envia-se a solicitação ao servidor sem autenticação, mas com o cookie de sessão
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "lister-simulations"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// recuperamos a resposta
return $this->getSimulations($response);
}
Comentários
- linha 5: método GET;
- linhas 6-8: o único parâmetro do GET;
- linha 13: a recuperação das simulações é delegada ao método privado [getSimulations];
23.14.2.2.6. O método [getSimulations]
private function getSimulations(CurlResponse $response): array {
// recuperamos a resposta JSON
$array = $this->getResponse($response);
// temos uma matriz de objetos associativos
// vamos transformá-lo em um array de objetos de simulação
$simulations = [];
foreach ($array as $simulation) {
$simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
}
// retornamos a lista de objetos de simulação
return $simulations;
}
Comentários
- linha 3: recupera-se a matriz proveniente da resposta. Trata-se de uma matriz de matrizes, sendo que cada uma delas possui todos os atributos de um objeto [Simulation];
- linha 6: se chegamos até aqui, é porque o método [getResponse] não lançou nenhuma exceção;
- linhas 6-9: aproveita-se a resposta para construir um array de objetos [Simulation];
- linha 11: retorna-se essa matriz;
23.14.2.2.7. O método [SupprimerSimulation]
public function supprimerSimulation(int $numéro): array {
// criamos um cliente HTTP
$httpClient = HttpClient::create();
// enviamos a solicitação ao servidor sem autenticação, mas com o cookie de sessão
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "supprimer-simulation",
"numéro" => $numéro
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// recuperamos a resposta
return $this->getSimulations($response);
}
Comentários
- linha 5: é feita uma consulta GET;
- linhas 6-9: os dois parâmetros do URL;
- linha 14: após uma exclusão, o servidor retorna a nova tabela de simulações. Retornamos essa tabela;
23.14.2.2.8. O método [finSession]
Uma sessão de trabalho com o serviço web normalmente termina com a chamada ao método [finSession]:
public function finSession(): void {
// cria-se um cliente HTTP
$httpClient = HttpClient::create();
// envia-se a solicitação ao servidor sem autenticação, mas com o cookie de sessão
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "fin-session"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// recuperamos a resposta
$this->getResponse($response);
}
Comentários
- linha 5: é feita uma solicitação ao método GET;
- linhas 6-8: o único parâmetro do URL;
- linha 13: lemos a resposta. Será lançada uma exceção se o código HTTP da resposta for diferente de 200;
23.14.3. A camada [métier]

23.14.3.1. A interface
A interface da camada [métier] é a seguinte: [InterfaceClientMetier.php]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceClientMetier {
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
// autenticação
public function authentifierUtilisateur(String $user, string $password): void;
// lista de simulações
public function listerSimulations(): array;
// gravação dos resultados
public function saveResults(string $resultsFilename, array $simulations): void;
// excluir uma simulação
public function supprimerSimulation(int $numéro): array;
// início da sessão
public function initSession(string $type = 'json'): void;
// fim da sessão
public function finSession(): void;
}
Comentários
- apenas o método [executeBatchImpots] da linha 12 é específico da camada [métier]. Todos os demais pertencem à camada [dao], que os implementa;
23.14.3.2. A classe [ClientMetier]
A classe que implementa a camada [métier] é a seguinte:
<?php
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// atributo
private $clientDao;
// fabricante
public function __construct(InterfaceClientDao $clientDao) {
$this->clientDao = $clientDao;
}
// cálculo do imposto
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// permite que as exceções provenientes da camada [dao] sejam propagadas
// recuperação dos dados dos contribuintes
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela de resultados
$simulations = [];
// analisamos os resultados
foreach ($taxPayersData as $taxPayerData) {
// calcula-se o imposto
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// registro dos resultados
if ($resultsFileName !== NULL) {
$this->clientDao->saveResults($resultsFileName, $simulations);
}
}
public function authentifierUtilisateur(String $user, string $password): void {
$this->clientDao->authentifierUtilisateur($user, $password);
}
public function listerSimulations(): array {
return $this->clientDao->listerSimulations();
}
public function saveResults(string $resultsFilename, array $simulations): void {
$this->clientDao->saveResults($resultsFilename, $simulations);
}
public function supprimerSimulation(int $numéro): array {
return $this->clientDao->supprimerSimulation($numéro);
}
public function finSession(): void {
$this->clientDao->finSession();
}
public function initSession(string $type = 'json'): void {
$this->clientDao->initSession($type);
}
}
Comentários
- linhas 10-12: para ser criada, a camada [métier] precisa de uma referência à camada [dao];
- linhas 20-38: apenas o método [executeBatchImpots] é específico da camada [métier]. A implementação dos demais métodos delega o trabalho a ser realizado aos métodos com os mesmos nomes na camada [dao];
- linha 23: recorre-se à camada [dao] para obter, em uma tabela de objetos do tipo [TaxPayerData], os dados dos contribuintes;
- linha 25: as diferentes simulações calculadas serão acumuladas na tabela [$simulations];
- linhas 27-33: calcula-se o imposto de cada um dos contribuintes da tabela [$taxPayersData];
- linhas 35-37: os resultados obtidos na tabela [$simulations] são salvos em um arquivo jSON;
Observação: a camada [métier] praticamente não faz nada. Seria possível decidir excluí-la e reunir tudo na camada [dao].
23.14.4. O script principal

O script principal é configurado pelo seguinte arquivo [config.json]:
{
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
"dependencies": [
"/Entities/BaseEntity.php",
"/Entities/TaxPayerData.php",
"/Entities/Simulation.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Utilitaires.php",
"/Model/InterfaceClientDao.php",
"/Model/TraitDao.php",
"/Model/ClientDao.php",
"/Model/InterfaceClientMetier.php",
"/Model/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-12/main.php"
}
O script principal [main.php] é o seguinte:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare(strict_types = 1);
// espaço de nomes
namespace Application;
// gestão de erros por PHP
// ini_set("display_errors", "0");
//
// caminho do arquivo de configuração
define("CONFIG_FILENAME", "../Data/config.json");
// recupera-se a configuração
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// inclui-se as dependências necessárias para o script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definição das constantes
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// dependências do Symfony
use Symfony\Component\HttpClient\HttpClient;
// criação da camada [dao]
$clientDao = new ClientDao($config["urlServer"]);
// criação da camada [métier]
$clientMetier = new ClientMetier($clientDao);
// cálculo de impostos em modo batch
try {
// inicialização da sessão
$clientMetier->initSession('json');
// autenticação
$clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
// cálculo de impostos sem salvar os resultados
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// lista de simulações
$clientMetier->listerSimulations();
// exclusão de uma simulação
$simulations = $clientMetier->supprimerSimulation(1);
// salvamento dos resultados
$clientMetier->saveResults(RESULTS_FILENAME, $simulations);
// fim da sessão
$clientMetier->finSession();
// ação sem autenticação — deve causar falha
$clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
// exibe o erro
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// fim
print "Terminé\n";
exit();
Comentários
- linhas 12-16: processamento do arquivo de configuração [config.json];
- linhas 18-26: carregamento de todas as dependências;
- linhas 28-34: definição de constantes e aliases;
- linhas 36-39: construção das camadas [dao] e [métier];
- linha 44: inicialização de uma sessão jSON;
- linha 46: autenticação no servidor;
- linha 48: cálculo do imposto de uma série de contribuintes. Os resultados não são salvos (2º parâmetro NULL);
- linha 50: solicita-se os resultados de todos esses cálculos;
- linha 52: exclui-se a simulação nº 1 (a segunda da lista);
- linha 54: salvam-se as simulações restantes;
- linha 56: encerra-se a sessão. Isso significa que o cookie de sessão é eliminado;
- linha 58: solicita-se a lista de simulações. Como o cookie de sessão foi eliminado, a autenticação deve ser repetida. Portanto, deve ocorrer uma exceção informando que não estamos autenticados;
O arquivo [taxpayersdata.json] é o seguinte:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
Há 12 contribuintes, dos quais 1 está incorreto. Portanto, são 11 simulações no total. Uma delas será excluída. Devem restar 10.
Após a execução do script principal, o arquivo jSON [results.json] é o seguinte:
[
{
"marié": "oui",
"enfants": "2",
"salaire": "55555",
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "3",
"salaire": "50000",
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "2",
"salaire": "100000",
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": "3",
"salaire": "100000",
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "3",
"salaire": "100000",
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": "5",
"salaire": "100000",
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "0",
"salaire": "100000",
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "2",
"salaire": "30000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": "0",
"salaire": "200000",
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": "3",
"salaire": "20000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
Há, de fato, 10 simulações.
O arquivo jSON [errors.json] tem o seguinte conteúdo:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Os resultados no console são os seguintes (no modo detalhado, as respostas jSON do servidor são exibidas no console):
{"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé
23.14.5. Testes [Codeception]
Assim como nos clientes anteriores, o cliente da versão 12 pode ser submetido aos testes [Codeception]:

O código da classe de teste da camada [métier] do cliente é semelhante ao das classes de teste dos clientes anteriores:
<?php
// respeito estrito aos 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-12");
// caminho do arquivo de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config.json");
// recuperamos a configuração
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// inclui-se as dependências necessárias ao script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// dependências do Symfony
use Symfony\Component\HttpClient\HttpClient;
// classe de teste
class ClientDaoTest extends \Codeception\Test\Unit {
// camada DAO
private $clientDao;
public function __construct() {
parent::__construct();
// recuperamos a configuração
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// criação da camada [dao]
$clientDao = new ClientDao($config["urlServer"]);
// criação da camada [métier]
$this->métier = new ClientMetier($clientDao);
// inicialização da sessão
$this->métier->initSession("json");
// autenticação
$this->métier->authentifierUtilisateur("admin", "admin");
}
// testes
public function test1() {
$simulation = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
$this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
$this->assertEquals(0.14, $simulation->getTaux());
}
public function test2() {
….
}
…
public function test11() {
…
}
}
Comentários
- linhas 34-46: vale lembrar que o construtor da classe de teste é executado antes de cada teste;
- linhas 38-41: construção das camadas [dao] e [métier];
- linhas 42-45: os métodos de teste [test1…, test11] testam o método [calculerImpot]. Para que isso seja possível, é necessário primeiro inicializar uma sessão jSON e autenticar-se;
Os resultados do teste são os seguintes:

Muitos outros testes devem ser realizados:
- testar os diferentes métodos da camada [dao];
- testar os status retornados pelo servidor web. Esses status são importantes, pois seu valor determina qual página HTML deve ser exibida;