23. Exercício prático – versão 12
Neste capítulo, iremos escrever uma aplicação web que siga a arquitetura MVC (Modelo-Vista-Controlador). A aplicação poderá apresentar as suas respostas em três formatos: jSON, XML, HTML. Existe um salto de complexidade entre o que vamos fazer agora e o que foi feito anteriormente. Vamos reutilizar a maioria dos conceitos abordados até agora e detalhar todas as etapas que conduzem à aplicação final.
23.1. Arquitetura MVC
Vamos implementar o modelo de arquitetura denominado MVC (Modelo – Vista – Controlador) da seguinte forma:

O processamento de um pedido de um cliente decorrerá da seguinte forma:
- 1 - pedido
Os URL solicitados terão o seguinte formatohttp://machine:port/contexte/….?action=uneAction¶m1=v1¶m2=v2&… O [Contrôleur principal] utilizará um ficheiro de configuração para «encaminhar» o pedido para o controlador correto e para a ação correta dentro desse controlador. Para tal, utilizará o campo [action] do URL. O restante do URL [param1=v1¶m2=v2&…] é constituído por parâmetros opcionais que serão transmitidos à ação. O C de MVC é, neste caso, a cadeia [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. Estes 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 juntamente com o seu pedido;
- No processamento do pedido do utilizador, a ação pode necessitar da camada [métier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
- uma resposta de erro, caso a solicitação não tenha podido ser processada corretamente;
- uma resposta de confirmação, caso contrário;
- o [Contrôleur / Action] enviará a sua resposta [2c] ao controlador principal, juntamente com um código de estado. Estes códigos de estado representarão de forma única o estado em que se encontra a aplicação. 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. Estes podem provir de várias fontes:
- 3 - resposta
- dependendo de o cliente ter solicitado uma resposta jSON, XML ou HTML, o [Contrôleur principal] instanciará o [3a], o tipo de resposta adequado, e solicitará a este que envie a resposta ao cliente. O [Contrôleur principal] transmitirá a ele tanto a resposta como o código de estado fornecidos pelo [Contrôleur / Action] que foi executado;
- se a resposta pretendida for do tipo jSON ou XML, a resposta selecionada formatará a resposta do [Contrôleur / Action] que lhe foi fornecida e enviá-la-á para o [3c]. O cliente capaz de utilizar esta resposta pode ser um script de consola PHP ou um script JavaScript alojado numa página HTML;
- se a resposta pretendida for do tipo HTML, a resposta selecionada irá selecionar [3b] uma das vistas HTML [Vuei] utilizando o código de estado que lhe foi fornecido. É o V de MVC. A cada código de estado corresponde uma única vista. Esta vista V irá apresentar a resposta do [Contrôleur / Action] que foi executado. Esta vista apresenta os dados dessa resposta utilizando HTML, CSS e JavaScript. A estes dados chama-se o 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 der ao modelo, estes dois conceitos estão ou não relacionados. Consideremos uma aplicação web MVC de uma camada:

No exemplo acima, cada um dos [Contrôleur / Action] integra uma parte das camadas [métier] e [dao]. Na camada [web] existe, de facto, uma arquitetura MVC, mas a aplicação como um todo não possui uma arquitetura em camadas. Aqui, existe apenas uma camada que faz tudo.
Agora, consideremos uma arquitetura web multicamadas:

A camada [web] pode ser implementada sem seguir o modelo MVC. Temos, então, 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, obtendo-se assim uma arquitetura em camadas com uma camada [web] do tipo MVC. Feito isto, é possível substituir esta camada ASP.NET MVC por uma camada ASP.NET clássica (WebForms), mantendo o resto (função, DAO, Piloto) inalterado. Temos então uma arquitetura em camadas com uma camada [web] que já não é do tipo MVC.
Em MVC, afirmámos que o modelo M era o da vista V, c.a.d, ou seja, o conjunto de dados apresentados pela vista V. É fornecida outra definição do modelo M de MVC:

Muitos autores consideram que o que se encontra à direita da camada [web] constitui o modelo M do MVC. Para evitar ambiguidades, pode-se referir-se:
- do modelo do domínio, quando nos referimos a tudo o que está à direita da camada [web];
- do modelo da vista, quando nos referimos aos dados apresentados por uma vista V;
23.2. Estrutura do projeto NetBeans
Para o projeto NetBeans, adotaremos uma arquitetura que reflete 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 ficheiro URL, por exemplo, […/main.php?action=authentifier-utilisateur]. Com esta ação, o [Contrôleur principal] [main.php] irá selecionar um [Contrôleur secondaire], neste caso o [AuthentifierUtilisateurController], para processar a ação solicitada. Estes controladores também fazem parte do C de MVC;
- [5]: a pasta [Model] conterá as camadas [métier] e [dao] da aplicação. De acordo com os termos adotados anteriormente, estes 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. Existe uma classe por tipo de resposta pretendida:
- [JsonResponse]: para uma resposta jSON;
- [XmlResponse]: para uma resposta XML;
- [HtmlResponse]: para uma resposta HTML;
- [7]: o dossier [Views] contém as vistas HTML quando se pretende uma resposta HTML. Trata-se do V de MVC. Estas são ativadas pela classe [HtmlResponse], que lhes transmite os dados a apresentar. Estes dados constituem o modelo da vista. De acordo com a terminologia adotada para o M, estes dados podem ser o M de MVC;
- [8]: a pasta [Utilities] contém utilitários:
- [Logger]: a classe que permite registar logs num ficheiro de texto;
- [Sendmail]: a classe que permite enviar e-mails;
- [9]: a pasta [Logs] contém o ficheiro de registos [logs.txt];
- [10]: a pasta [Entities] contém classes utilizadas pelos diferentes controladores;
Com base nesta estrutura de diretórios, é possível descrever o percurso do processamento de uma ação solicitada por um cliente:
- [main.php] [3] recebe o pedido;
- após realizar algumas verificações preliminares (a ação faz parte das ações aceites?), transmite o pedido ao controlador secundário [4], responsável pelo processamento dessa ação;
- o controlador secundário executa as tarefas que lhe competem. No seu trabalho, pode necessitar das camadas [métier], [dao] e [5], bem como das entidades do dossier [10]. Envia a sua resposta ao controlador principal [main.php], que o ativou;
- dependendo do tipo de resposta [jSON, XML, HTML] pretendido pelo cliente, o controlador principal [main.php] ativa uma das respostas da pasta [Responses] [6];
- as respostas [JsonResponse, XmlResponse] enviam, respetivamente, a resposta jSON ou XML ao cliente;
- a resposta [HtmlResponse] utiliza uma das vistas do dossier [Views] [7] para enviar uma resposta HTML ao cliente;
- os diferentes controladores têm acesso à classe [Logger] da pasta [8] para gravar registos no ficheiro de registos da pasta [9]. São registados:
- a ação solicitada;
- a resposta do respetivo controlador. Esta é registada 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 utilizando a classe [SendMail] da pasta [8];
23.3. As ações da aplicação
O cliente transmite ao servidor web a ação a executar sob a forma de um parâmetro [action] no URL [/main.php?action=xxx]. As ações autorizadas estão listadas no ficheiro [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-9: um dicionário [action:contrôleur]. A cada ação está associado o controlador secundário responsável pelo seu processamento;
- linha 3: [init-session]: inicia uma sessão de simulações de cálculos de impostos. Esta ação indica o tipo de respostas pretendidas [jSON, XML, HTML];
- linha 4: uma vez definido o tipo de sessão, o cliente deverá autenticar-se com a ação [authentifier-utilisateur]. Enquanto não estiver identificado, todas as outras ações estão proibidas, com exceção de [init-session];
- linha 5: uma vez identificado, o cliente poderá efetuar uma série de cálculos de impostos com a ação [calculer-impot];
- linha 6: a qualquer momento, o cliente pode solicitar a visualização da lista das simulações que realizou com a ação [lister-simulations];
- linha 7: poderá eliminar algumas delas através da ação [supprimer-simulation];
- linha 8: o cliente termina a sua sessão de simulações com a ação [fin-session]. A partir desse momento, terá de se autenticar novamente se quiser utilizar a aplicação;
- linha 9: na aplicação 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 da aplicação web
A aplicação é configurada pelo seguinte ficheiro 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 ficheiro jSON que contém a configuração do acesso à base 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 utilizador autorizado a utilizar a aplicação;
- linhas 46-54: os endereços de e-mail do administrador da aplicação;
- linha 55: o caminho do ficheiro de registos;
- 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 vista [vue-erreurs] é apresentada numa sessão HTML sempre que ocorre um erro anómalo:
- uma aplicação jSON ou XML é normalmente consultada através de um cliente programado. Este envia ao servidor parâmetros que podem estar ausentes ou incorretos. Todos os controladores tratam estes casos e devolvem 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. Quando utilizada normalmente, a aplicação web utiliza apenas uma parte dos casos de utilização 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 introduzir 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, quando são necessários três, etc… O servidor jSON deve processar todos estes casos;
- Com uma aplicação 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 juntamente com uma ação POST e os três parâmetros [marié, enfants, salaire]. Alguns destes parâmetros poderão ter um valor incorreto, mas estarão presentes. No entanto, o utilizador pode reproduzir certos erros ao introduzir ele próprio os URL no navegador. Por motivos de segurança, é necessário gerir este caso;
- a vista [vue-erreurs] será apresentada sempre que um controlador secundário devolver um código de estado incompatível com a aplicação web, ou seja, um código de estado que não conste nas linhas 72-74 do ficheiro de configuração. Optamos por esta solução por motivos pedagógicos. Outra opção possível seria não fazer nada e limitar-nos a voltar a apresentar a vista atualmente exibida no navegador do cliente, para que o utilizador tenha a impressão de que o servidor não responde às suas URL criadas manualmente;
23.5. Instalação de ferramentas e bibliotecas
23.5.1. Postman
O [Postman] é a ferramenta que nos permitirá interrogar as diferentes URL da nossa aplicação web. Permite-nos:
- utilizar qualquer URL: estas são criadas manualmente;
- enviar pedidos ao servidor web através 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 no formato jSON, XML, HTML,
- ter acesso aos cabeçalhos HTTP da resposta. Assim, temos acesso à resposta completa HTTP do servidor;
Uma vez que criamos manualmente as URL consultadas, poderemos testar todos os casos de erro possíveis e ver como o servidor reage.
O [Postman] está disponível no URL [https://www.getpostman.com/downloads/]. A versão disponível em junho de 2019 é a 7.2. Esta versão apresenta uma anomalia: quando se efetuam pedidos sucessivos ao servidor web em questão, o cliente [Postman 7.2] não reenvia automaticamente os cookies que o servidor lhe envia, nomeadamente o cookie de sessão. Para manter a sessão, é necessário 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 equipa do [Postman] corrigiu-o numa versão alfa (que pode ser instável) denominada [Postman Canary], disponível no URL [https://www.getpostman.com/downloads/canary]. É esta versão que está a ser utilizada aqui. Vamos descrever a sua instalação. Se estiver disponível uma versão estável [Postman 7.3] ou posterior, pode descarregá-la: o bug provavelmente já terá sido corrigido.
Proceda à instalação da sua versão do [Postman]. Durante a instalação, ser-lhe-á pedido que crie uma conta: esta não será necessária neste caso. A conta [Postman] serve para sincronizar diferentes dispositivos, de modo a que a configuração de um seja replicada noutro. Nada disto é útil neste caso.
Uma vez instalado, o [Postman] apresenta a seguinte interface:

- no [2-3], tem-se acesso às definições do produto;

- no [6], a versão utilizada neste documento;
- se tiver criado uma conta, é efetuada uma sincronização entre o seu computador e um servidor remoto [Postman]. Isto é simbolizado pela roda [7] que gira sempre que efetua alterações no projeto [Postman]. Para interromper esta sincronização desnecessária, termine a sessão no [8-9];
23.5.2. A biblioteca Symfony / Serializer
Para serializar objetos em jSON e XML, vamos utilizar a biblioteca [Symfony / Serializer]. Esta apresenta aqui duas vantagens:
- é consistente na sua utilização para serializar em jSON ou XML: isto evita ter de aprender a utilizar duas bibliotecas com interfaces de programação de aplicações (API) diferentes;
- por si só, é capaz de serializar objetos em jSON ou XML, mesmo que os atributos desses objetos sejam privados. Recorde-se que, em jSON, para serializar um objeto, era necessário que a classe do mesmo implementasse a interface [\JsonSerializable]. O resultado obtido era então a cadeia jSON de um tabuleiro associativo com os atributos da classe como chaves. Ao deserializar essa cadeia jSON, recuperava-se o tabuleiro associativo primitivo, que tinha então de ser transformado num objeto da classe que tinha sido serializada. Com [Symfony / Serializer], a deserialização produz imediatamente um objeto da classe serializada. É mais simples;
A documentação da biblioteca [Symfony / Serializer] está disponível em URL: [https://symfony.com/doc/current/components/serializer.html] (junho de 2019).
Para instalar esta biblioteca, abra um terminal Laragon (ver parágrafo com o link) e introduza 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 da aplicação

As entidades [BaseEntity, Database, ExceptionImpots, TaxAdminData] foram utilizadas a partir da versão 08 do serviço web (ver parágrafo «ligação»).
A classe [Simulation] servirá para encapsular os elementos de uma simulação de cálculo de impostos:
<?php
namespace Application;
class Simulation extends BaseEntity {
// atributos de uma simulação de cálculo de impostos
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, por isso, herda os métodos:
- [setFromArrayOfAttributes($arrayOfAttributes)]: que permite inicializar os atributos da classe;
- [__toString]: que devolve a cadeia jSON do objeto;
- linhas 7-14: os atributos da simulação;
- linhas 16-47: os getters da classe;
23.7. Os utilitários da aplicação
![]()
A classe [Logger] permite registar eventos num ficheiro de texto. Esta classe foi descrita no parágrafo «ligação».
A classe [SendAdminMail] permite enviar um e-mail ao administrador da aplicação. Esta classe foi descrita no parágrafo «ligação».
23.8. As camadas [métier] e [dao]


As classes e interfaces das camadas [métier] e [dao] estão reunidas 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 «ligação». |
InterfaceServerDao | Interface implementada pela camada [dao] do servidor. Definida no parágrafo «ligação». |
ServerDao | Implementação da interface [InterfaceServerDao]. Implementa a camada [dao] do servidor. Definida no parágrafo «ligação». |
ServerDaoWithSession | Implementação da interface [InterfaceServerDao]. Implementa a camada [dao] do servidor. Definida no parágrafo «ligação». |
InterfaceServerMetier | Interface implementada pela camada [métier] do servidor. Definida no parágrafo «ligação». |
ServerMetier | Implementação da interface [InterfaceMetier]. Implementa a camada [metier] do servidor. Definida no parágrafo «ligação». |
A aplicação que está a ser desenvolvida 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 concentrar-nos na camada [web] da aplicação:

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

- [1-2]: o controlador principal [main.php] [1] é configurado pelo ficheiro [config.json] [2];
Recorde-se a posição do controlador principal na nossa arquitetura MVC:

No [1], o controlador principal [main.php] é o primeiro elemento da arquitetura MVC a processar o pedido do cliente. Tem várias funções:
- em primeiro lugar, realiza as verificações básicas:
- se o seu ficheiro de configuração existe e é válido;
- carrega todas as dependências do projeto. Isto equivale a carregar todos os elementos da arquitetura MVC;
- a ação solicitada foi especificada? Se sim, é válida?
- se a ação solicitada for válida, selecionar [2a] o controlador secundário que a irá processar e passar-lhe as informações de que necessita: a solicitação HTTP, a sessão, a configuração da aplicação;
- recuperar [2c] a resposta do controlador secundário. Dependendo do tipo (jSON, XML, HTML) da aplicação solicitada pelo cliente, selecionar [3a] a resposta (JsonResponse, XmlResponse, HtmlResponse) encarregada de enviar a resposta ao cliente e de lhe transmitir todas as informações de que necessita (a solicitação HTTP, a sessão, a configuração da aplicação, a resposta do controlador secundário);
- assim que essa resposta for enviada ([3c]), proceder à libertaçã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 rigoroso pelos 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);
// recuperar a configuração
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// erro?
if (!$fileContents) {
// regista-se o erro
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// recuperamos o código JSON do ficheiro de configuração numa tabela associativa
$config = \json_decode($fileContents, true);
// erro?
if (!$config) {
// regista-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 ficheiro 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 estado
$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 da aplicação web;
- [Response]: a resposta HTTP enviada ao cliente;
- linha 15: durante todo o desenvolvimento, esta linha será mantida como comentário: os erros PHP são, assim, integrados no fluxo de texto enviado ao cliente. Se esse cliente for um navegador, isto permite visualizar os erros encontrados pelo servidor. Trata-se de uma ajuda à depuração;
- linha 16: todos os erros são assinalados (E_ALL), exceto os avisos (! E_WARNING) e as informações não fatais (! E_NOTICE). Por exemplo, se um ficheiro não puder ser aberto, o PHP emite um erro do tipo [E_NOTICE]. Se a linha 15 permitir a exibição de erros, o erro de abertura do ficheiro aparece no navegador do cliente. Isso é bom se se tiver esquecido de testar o resultado da abertura do ficheiro, mas menos bom se tiver previsto o teste: uma linha de [notice] vem então poluir a resposta do servidor ao cliente. Na fase de desenvolvimento, a linha 16 também deve ser comentada: não quer perder nenhum erro;
- linha 19: o ficheiro de configuração é lido;
- linhas 22-27: se esta leitura não tiver corrido bem, regista-se o erro (linha 25), coloca-se a aplicação no estado [131] e prepara-se uma mensagem de erro;
- linha 30: descodifica-se a cadeia jSON do ficheiro de configuração;
- linhas 32-37: se esta descodificação falhar, regista-se o erro (linha 34), coloca-se a aplicação no estado [132] e prepara-se uma mensagem de erro;
- linhas 40-57: em caso de erro na leitura do ficheiro de configuração, não é possível prosseguir. Prepara-se, então, uma resposta jSON para o cliente:
- linha 44: como o ficheiro de configuração não foi lido, é necessário importar manualmente o ficheiro [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 pela aplicação web em análise terão três chaves:
- [action]: a ação solicitada pelo cliente;
- [état]: o estado da aplicação 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 ficheiro de configuração está ausente ou incorreto:

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

- nas coleções [4], aparece agora uma coleção denominada [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 pedido, neste caso um pedido [GET]. Em [19], os diferentes tipos de pedido disponíveis;
- em [13], introduz-se aqui o URL do servidor;
- em [14], introduzem-se aqui os parâmetros adicionados ao URL e que serão, portanto, parâmetros do GET. A vantagem de os colocar aqui, em vez de diretamente no URL, é que serão codificados em URL pelo [Postman]. Se os colocarem vocês próprios no URL, caberá a vocês codificá-los em URL;
- no [15], o [Authorization] serve para definir o utilizador que se vai ligar. Não teremos de utilizar esta possibilidade;
- no [16], os cabeçalhos HTTP que acompanharão o pedido. Alguns cabeçalhos são incluídos automaticamente no pedido. Aqui pode adicionar novos;
- Em [17], [Body] designa os parâmetros de uma operação [POST]. Teremos de utilizar esta opção;
Vamos realizar o seguinte teste:
- em [main.php], indicamos que o ficheiro de configuração é [config2.json], que não existe:

- a linha 16 do código deve ser descomentada;
- linha 18: o erro relativo ao nome do ficheiro de configuração;
Entremos no [Postman] [13, 20], o URL do servidor web de cálculo de impostos e executemos o [21]:

A resposta devolvida pelo servidor (é claro que o Laragon tem de estar ativo) é a seguinte:

- em [22], o servidor devolveu um código HTTP [500 Internal Server Error];
- em [23], [Body] designa o corpo da resposta, ou seja, o documento enviado pelo servidor a seguir aos cabeçalhos HTTP [28];
- em [26], verifica-se 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] apresenta então a página recebida;
- em [30], a resposta jSON do servidor. É mesmo essa que esperávamos;
Em [25], os cabeçalhos HTTP enviados na resposta do servidor são os seguintes:

- em [32], o tipo jSON da resposta;
Este primeiro teste permitiu-nos verificar que:
- é possível enviar qualquer tipo de pedido 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 ficheiro [config3.json] é um ficheiro jSON sintaticamente incorreto;
- em [4], o [main.php] está configurado para utilizar o [config3.json];
Adicionamos uma nova consulta no [Postman]:

- No [1-3], clicamos com o botão direito do rato no [2] e selecionamos a opção [duplicate] para duplicar a consulta [2];
- em [4], a nova consulta tem um nome predefinido que se altera para [5];

- em [6], a consulta renomeada;
- em [9-10], envia-se a mesma consulta GET que anteriormente;

- em [11], a resposta jSON do servidor;
Mostrámos aqui como iriam ser testadas as diferentes ações do serviço web de cálculo do imposto.
23.9.4. [main.php] – 2
Retomamos a análise do código do controlador principal [main.php]:
<?php
// respeito rigoroso dos 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);
// recupera-se 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 ficheiro de registos
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// não foi possível criar o ficheiro de registos - 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 ficheiro de configuração [config.json] que agora existe e está sintaticamente correto. Seria ainda necessário verificar se as chaves esperadas neste ficheiro estão efetivamente presentes. Consideraremos que isso faz parte do trabalho normal de depuração do programador. 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 deparámos com este código várias vezes;
- linhas 31-43: tentamos criar o objeto [Logger], que nos permitirá registar eventos no ficheiro [$config['logsFilename']]. Esta criação pode falhar;
- linhas 33-43: gestão do erro na criação do objeto [Logger];
- linha 35: define-se um número de estado;
- linhas 36-40: envia-se 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 {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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 da aplicação. No entanto, a resposta pode necessitar de outras informações. Por isso, são-lhe 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 necessita do [Logger], uma vez que irá registar a resposta enviada ao cliente;
A classe [JsonResponse] implementa a interface [InterfaceResponse] da seguinte forma:
<?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 {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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, inserir 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);
// registo
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 esta classe. É esta classe pai que envia a resposta ao cliente (linha 46). Foi precisamente porque este código era comum a todos os tipos de [Response] que foi fatorizado numa classe pai;
- linhas 33-40: instanciação do serializador [Symfony], que irá converter a resposta do servidor [$content] numa cadeia jSON (linha 42);
- linhas 34-36: o primeiro parâmetro do construtor de [Serializer] é um array. Nesta matriz, insere-se uma instância da classe [ObjectNormalizer], necessária para a serialização de objetos. Este 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 colocam-se todos os codificadores utilizados numa serialização (XML, jSON, CSV…);
- linha 39: aqui haverá apenas um codificador, do tipo [JsonEncoder]. O construtor sem parâmetros poderia ter sido suficiente. Aqui, passámos um parâmetro [JsonEncode] ao construtor, apenas para passar as opções de codificação jSON;
- linha 39: o parâmetro do construtor [JsonEncode] é um array de opções. Aqui, utiliza-se a opção [JSON_UNESCAPED_UNICODE] para solicitar que os caracteres UTF-8 da cadeia jSON sejam representados de forma nativa 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 indica ao cliente que lhe será enviado o jSON;
- linha 46: solicita-se à classe pai que envie a resposta ao cliente;
- linhas 48-50: regista-se 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 estado da resposta
// string $content: o corpo da resposta a enviar
// consoante o caso, trata-se de uma cadeia jSON, XML ou HTML
// matriz $headers: os cabeçalhos HTTP a adicionar à 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 estado
$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: note-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 estado 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;
Detalhámos todo o ciclo de uma resposta jSON. Não voltaremos a abordar este assunto mais adiante. Basta recordar a assinatura da interface [InterfaceResponse]:
interface InterfaceResponse {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// matriz $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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 esta assinatura sempre que solicitar o envio da resposta ao cliente.
23.9.5. Testes [Postman] – 2
Alteramos o ficheiro [config.json] da seguinte forma:

- no [1], indicamos que o ficheiro de registos é o [Logs], que é uma pasta [2]. A criação do ficheiro [Logs] deverá, portanto, falhar;
Criamos uma nova solicitação [Postman] [3], denominada [erreur-133]:

- [2-4]: definimos a mesma consulta que nos dois testes anteriores;
- [5-7]: obtemos, de facto, a resposta esperada jSON;
23.9.6. [main.php] – 3
Vamos continuar a análise do controlador principal [main.php]:
<?php
// respeito rigoroso pelos 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 pelo PHP
…
// criação do ficheiro de registos
…
// 1.º registo
$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;
// processamento da ação solicitada
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// a ação é guardada
$action = strtolower($request->query->get("action"));
}
// regista-se a ação
$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 determinadas 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 a enviar
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// a ação é executada através do respetivo controlador
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- envia-se a resposta
// caso de erro fatal HTTP_INTERNAL_SERVER_ERROR
// envia-se um e-mail ao administrador, se for 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 predefinição, a resposta será em jSON
$type = "json";
}
// adicionam-se as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] encarregado de 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 — libertam-se os recursos
$logger->close();
exit;
Comentários
- assim que as primeiras verificações foram efetuadas e ele sabe que pode funcionar, o controlador principal concentra-se na ação que lhe foi solicitada: esta deve cumprir determinadas condições;
- linha 21: registamos o facto de termos um novo pedido. Não era possível fazê-lo antes, pois não tínhamos a certeza de dispor de um ficheiro de registos 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, na qual recuperamos a sessão existente, caso exista;
- linha 27: a sessão é ativada;
- linha 29: um array de mensagens de erro;
- linha 30: um valor 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, então, parte dos parâmetros [$request→query];
- linhas 33-36: caso de ausência do parâmetro [action] no URL. O erro é registado e é-lhe atribuído um estado [101];
- linha 39: se o parâmetro [action] estiver presente no URL, é memorizado;
- linha 42: o tipo da ação é registado;
- linhas 45-49: se o parâmetro [action] estiver presente, 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 é registado e é-lhe atribuído o estado [102];
- linhas 52-56: trata-se de uma ação válida. Esta tem ainda de cumprir outras condições. A aplicação web fornece três tipos de resposta (jSON, XML, HTML). Este tipo é definido pela ação [init-session]. Esta ação coloca o tipo da sessão na chave [type];
- linha 52: fora da ação [init-session], qualquer outra ação deve decorrer com uma chave [type] na sessão;
- linhas 53-55: caso contrário, o erro é registado e é-lhe atribuído o estado [103];
- 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. Esta é efetuada através da ação [authentifier-utilisateur], que, caso a autenticação 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, então ocorre um erro;
- linhas 60-62: regista-se o erro e atribui-se-lhe o estado [104];
- linhas 66-71: verifica-se se a matriz [$erreurs] não está vazia. Se for esse o caso, então a ação solicitada ou o seu contexto de execução estão incorretos;
- linhas 68-70: prepara-se a resposta a enviar ao cliente, mas ainda não a envia;
- linha 68: código de estado HTTP;
- linha 69: corpo da resposta;
- linha 70: cabeçalhos a adicionar à 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 executar. [__NAMESPACE__] é o espaço de nomes em que nos encontramos, neste caso [Application] (linha 7);
- os nomes das classes do controlador secundário encontram-se no ficheiro [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: regista-se o nome do controlador secundário, para verificação durante o desenvolvimento;
- linha 76: o controlador secundário é executado. Voltaremos a abordar os controladores secundários um pouco mais adiante;
- linha 76: todos os controladores secundários devolvem o mesmo tipo de resultado, que é um tabuleiro:
- o primeiro elemento da matriz [$statusCode] é o código de estado HTTP da resposta a enviar;
- o segundo elemento, [$état], é o estado da aplicação após a execução do controlador;
- o terceiro elemento, [$content], é um tabuleiro associativo com a chave única [réponse], que corresponde ao corpo da resposta a enviar ao cliente;
- o quarto elemento [$headers] é um array de cabeçalhos HTTP a adicionar à 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 estado [500 Internal Server Error]. Se um controlador tiver definido este código de estado, significa que a aplicação não pode funcionar. É o caso, por exemplo, do cálculo do imposto, se o SGBD utilizado não tiver sido iniciado ou já não responder. Nesse caso, é enviado um e-mail ao administrador da aplicação para o alertar. Não iremos comentar este código em particular. A utilização da classe [SendAdminMail] já foi apresentada (parágrafo com link);
- linhas 89-95: determina-se o tipo [jSON, XML, HTML] da aplicação web. Se a ação [init-session] tiver sido executada com sucesso, este tipo encontra-se 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 enviar ao cliente. São-lhe adicionadas as chaves [action] e [état]. A chave [action] permitirá acompanhar melhor os registos do ficheiro [logs.txt]. A chave [état] terá duas funções:
- permitirá que os clientes jSON e XML saibam em que estado a ação executada colocou a aplicação web;
- no caso de uma resposta HTML, permitirá escolher a vista HTML que deve ser enviada ao navegador do cliente;
- linha 99: seleciona-se o tipo de classe [Response] a executar para enviar a resposta ao cliente;
Já apresentámos a classe [JsonResponse] no parágrafo «ligação». Esta 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 estas classes implementam a interface [InterfaceResponse], também apresentada no parágrafo «ligação»:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Pedido $request: pedido em processamento
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Esta interface possui um único método, [send], responsável por enviar a resposta ao cliente. Este método tem os 7 parâmetros descritos nas linhas 11-17. Todas as classes e interfaces da pasta [Responses] encontram-se no espaço de nomes [Application] (linha 3).
Voltemos ao código de [main.php]:
…
// adicionam-se as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] encarregado de enviar a resposta ao cliente
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// a resposta foi enviada — libertam-se os recursos
$logger->close();
exit;
- linha 5: instanciamos a classe [Response] adequada ao tipo de aplicação. Estas classes estão definidas no ficheiro [config.json] da seguinte forma:
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
- linha 5: o nome da classe é precedido pelo seu espaço de nomes;
- linha 6: a classe [Response] é instanciada e o seu método [send] é chamado com os 7 parâmetros que este espera. Estes parâmetros são os da interface [InterfaceResponse], que todas as classes de resposta implementam. Isto envia a resposta ao cliente;
- linha 9: fecha-se o ficheiro de registos;
- linha 10: o controlador principal concluiu o seu trabalho;
23.9.7. Testes [Postman] – 3
Vamos testar vários casos de erro do parâmetro [action] do URL.

- no [1]:
- [erreur-101]: caso do parâmetro [action] ausente no URL;
- [erreur-102]: caso do parâmetro [action] 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], um pedido 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 da aplicação acima, os controladores secundários encontram-se 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 da aplicação
// processamento de um pedido (Request)
// utiliza a sessão Session e pode alterá-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 através do método [execute] da linha 17. São passadas para este método as informações conhecidas do controlador principal:
- linha 18: [array $config], que encapsula a configuração da aplicação;
- linha 19: [Request $request], que corresponde à solicitação HTTP atualmente em processamento;
- linha 20: [Session $session], que corresponde à sessão atual da aplicação web;
- linha 21: [array $infos=NULL], que é um conjunto adicional de informações para o controlador, caso os três primeiros parâmetros do método não sejam suficientes. Nesta aplicação, este parâmetro nunca foi utilizado. Está presente por precaução;
- linha 21: o método [execute] devolve a matriz [$statusCode, $état, $content, $headers]
- [int $statusCode]: o código de estado da resposta HTTP;
- [int $état]: o estado em que se encontra a aplicação no final da execução;
- [array $content]: um tabuleiro associativo [réponse=>résultat], em que [résultat] é de qualquer tipo: trata-se do resultado produzido pelo controlador e que será enviado ao cliente, após esse resultado ter sido serializado sob a forma de uma cadeia de caracteres;
- [array $headers]: a lista de cabeçalhos HTTP a incorporar na resposta HTTP do servidor;
Cada controlador secundário é chamado pelo seguinte código do controlador principal:
// a ação é executada através do seu controlador
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
Na linha 3, verifica-se que o 4.º parâmetro [array $infos=NULL] do método [execute] não é utilizado.
23.11. As ações
Passamos agora a analisar 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 pretendidas | Solicitação GET main.php?action=init-session&type=x pode ser emitida a qualquer momento |
autenticar-utilizador | Autoriza ou não um utilizador a iniciar sessão | Pedido POST main.php?action=autenticar-utilizador A solicitação deve ter dois parâmetros enviados por POST [user, password] Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido |
calcular-imposto | Efetua uma simulação de cálculo de impostos | Pedido POST main.php?action=calculer-impot A solicitação deve ter três parâmetros enviados via POST: [marié, enfants, salaire] Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado |
lister-simulações | Solicita a visualização da lista de simulações realizadas desde o início da sessão | Pedido 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 utilizador estiver autenticado |
eliminar-simulação | Elimina uma simulação da lista de simulações | Pedido 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 utilizador estiver autenticado |
fim-sessão | Encerra a sessão de simulações. | Tecnicamente, a sessão web anterior é eliminada e é criada uma nova sessão Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o utilizador estiver autenticado |
Todos os controladores secundários procedem da mesma forma:
- verificam os seus parâmetros. Estes encontram-se no objeto [Request→query] para os parâmetros presentes no URL e no objeto [Request→request] para os que são enviados (pedido POST);
- um controlador assemelha-se a uma função ou método que verifica a validade dos 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 dos seus parâmetros. Se o parâmetro esperado for um número, é necessário verificar se a cadeia do parâmetro corresponde efetivamente a um número;
- uma vez verificado que os parâmetros esperados estão presentes e são sintaticamente corretos, é necessário verificar se são válidos no contexto de execução atual. Este 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 depois de o cliente estar autenticado. Geralmente, uma chave na sessão indica se essa autenticação ocorreu ou não;
- uma vez efetuadas as verificações anteriores, o controlador secundário pode começar a trabalhar. Este trabalho de verificação dos parâmetros é muito importante. Não se pode aceitar que um cliente nos envie qualquer coisa em qualquer momento do ciclo de vida da aplicação. É necessário controlar totalmente o ciclo de vida da mesma;
- assim que o seu trabalho estiver concluído, o controlador secundário devolve a tabela [$statusCode, $état, $content, $headers] esperada pelo controlador principal que o chamou;
Vamos agora analisar os diferentes controladores ou, o que dá no mesmo, as diferentes ações que marcam o ciclo de vida da aplicação web.
23.11.1. A ação [init-session]
A ação [init-session] é processada pelo seguinte controlador [InitSessionController]:
<?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 um pedido (Request)
// utiliza a sessão Session e pode alterá-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 conter 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], []];
}
// recuperam-se 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], []];
}
// define-se 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 um pedido [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 esse o caso, regista-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á efetivamente presente no URL. Se não for esse o caso, regista-se o erro;
- linha 40: regista-se o tipo da sessão;
- linhas 43-47: verifica-se se o tipo da sessão corresponde a um dos valores (json, xml, html). Caso contrário, regista-se o erro;
- linhas 49-51: se tiver ocorrido um erro, envia-se um resultado [$statusCode, $état, $content, $headers] ao controlador principal;
- linha 53: o tipo da sessão é inserido na sessão da aplicação web;
- linhas 55-57: o controlador concluiu o seu trabalho. Envia-se um resultado de sucesso [$statusCode, $état, $content, $headers] ao controlador principal;
Recorde-se o que o controlador principal faz com a resposta dos controladores secundários:
// erros?
if ($erreurs) {
// prepara-se a resposta sem a enviar
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// executa-se a ação utilizando o respetivo 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 está a ser enviada
// caso de erro fatal HTTP_INTERNAL_SERVER_ERROR
// envia-se um e-mail ao administrador, se for 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 predefinição, a resposta será em jSON
$type = "json";
}
// adicionam-se as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] encarregado de 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 — libertam-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, envia a resposta instanciando uma das classes [JsonResponse, XmlResponse, HtmlResponse], consoante o tipo (json, xml, html) da sessão em curso;
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] em falta;
- 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] seguinte:
<?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 da aplicação
// processamento de um pedido (Request)
// utiliza a sessão Session e pode alterá-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 conter 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]";
// envia-se o resultado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// recuperam-se os parâmetros do POST
$erreurs = [];
// utilizador
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// palavra-passe
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 utilizador
// o utilizador 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 {
// regista-se na sessão que o utilizador 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 um pedido [POST main.php?action=authentifier-utilisateur] com dois parâmetros enviados via POST [user, password];
- linhas 24-25: verifica-se se existe uma solicitação POST com um único parâmetro no URL;
- linhas 26-31: se houver algum erro, regista-se o mesmo e devolve-se um resultado [$statusCode, $état, $content, $headers] ao controlador principal;
- linhas 36-39: verifica-se a presença do parâmetro [user] nos valores enviados. Se não estiver presente, regista-se o erro;
- linhas 43-45: verifica-se a presença do parâmetro [password] nos valores lançados. Se não estiver presente, regista-se o erro;
- linhas 50-53: se algum dos valores lançados estiver em falta, é devolvido um resultado [$statusCode, $état, $content, $headers] ao controlador principal;
- linhas 56-62: verifica-se se o par [$user,$password] recuperado está presente na tabela [$config[‘users’]] do ficheiro de configuração;
- linhas 64-69: se não for esse o caso, o erro é registado. O código de estado 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. Regista-se isso na sessão, inserindo nela a chave [user]. É a presença desta chave que indica uma autenticação bem-sucedida;
- linhas 73-77: é devolvido um resultado de sucesso [$statusCode, $état, $content, $headers] ao controlador principal;
23.11.4. Testes [Postman]
Estamos a realizar os testes [Postman] do controlador [AuthentifierUtilisateurController] no modo jSON;

Acima:
- em [1-6], a ação [authentifier-utilisateur] com um GET [2], quando é necessário um POST;
- em [7-10], a resposta jSON do servidor;
Substituamos o GET por um POST [2] sem incluir 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 formas de enviar valores para o servidor. Optamos pelo 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, definamos os dois parâmetros enviados [user, password], mas com valores que fazem com que a autenticação falhe:

Acima:
- em [1-9], um pedido POST com parâmetros enviados [user, password] incorretos;
- em [10-13], a resposta jSON do servidor. Repare-se no código de estado [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. Note-se o código de estado 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 da aplicação
// processamento de um pedido Request
// utiliza a sessão Session e pode alterá-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 conter 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;
// envio 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é]";
}
}
// recuperar 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) {
// envio 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();
// ligamo-nos ao servidor para verificar se está disponível
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// correu mal
// envio 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 da base 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
// envio do resultado com erro para o 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 alcance [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);
// adiciona-se à sessão a simulação que acabou de ser realizada
$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 repostas na sessão
$session->set("simulations", $simulations);
// envio 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 em [marié, enfants, salaire]:
- [marié] deve ter o seu valor definido em [oui, non];
- [enfants, salaire] deve ser um número inteiro positivo ou nulo;
- linhas 26-27: verifica-se se existe efetivamente um POST com um único parâmetro no URL;
- linhas 28-34: se não for esse o caso, é enviado um resultado de erro ao controlador principal;
- linha 36: acumulam-se as mensagens de erro na tabela [$erreurs];
- linhas 39-41: verifica-se a presença do parâmetro [marié]. Se não estiver presente, o erro é registado;
- linhas 43-49: verifica-se se o valor de [marié] está presente em [oui, non]. Se não for o caso, o erro é registado;
- linhas 51-54: verifica-se a presença do parâmetro [enfants]. Se não estiver presente, o erro é registado;
- linhas 55-61: verifica-se se o valor do parâmetro [enfants] é um número positivo ou zero. Se não for esse o caso, o erro é registado;
- linhas 63-66: verifica-se a presença do parâmetro [salaire]. Se não estiver presente, o erro é registado;
- linhas 67-72: verifica-se se o valor do parâmetro [salaire] é um número positivo ou zero. Se não for esse o caso, o erro é registado;
- linhas 75-78: se a tabela [$erreurs] não estiver vazia, significa que ocorreram erros. Coloca-se a tabela de erros na resposta e devolve-se o resultado ao controlador principal;
- linha 80: os parâmetros são válidos. É possível calcular o imposto. Para tal, é necessário criar 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 for possível estabelecer ligação ao servidor [Redis], envia-se um código [500 Internal Server Error] ao cliente;
- linha 98: verifica-se se o servidor [Redis] possui a chave [taxAdminData]. Esta chave representa os dados da administração fiscal. Se a chave não estiver presente, então os dados fiscais devem ser consultados na base de dados;
- linha 101: construção da camada [dao] quando os dados fiscais têm de ser obtidos da base de dados. A classe [ServerDaoWithRedis] foi descrita no parágrafo «ligação»;
- linha 103: os dados recuperados da base de dados são armazenados na memória [Redis] com a chave [taxAdminData];
- linhas 104-110: se a pesquisa na base de dados falhar, regista-se o erro devolvido pela camada [dao] e integra-se no resultado devolvido ao controlador principal;
- linha 109: a mensagem de erro devolvida pela camada [PDO] está codificada em [iso-8859-1]. Codifica-se-a em [utf-8];
- linhas 111-117: se a chave [taxAdminData] existir na memória [Redis], então os dados fiscais são passados diretamente para o construtor da camada [dao];
- linha 119: é criada a camada [métier]. A classe [ServerMetier] foi descrita no parágrafo «ligação»;
- linhas 124-126: com o montante do imposto calculado, é criado um objeto [Simulation]. A classe [Simulation] encapsula os dados de uma simulação e foi descrita no parágrafo «ligação»;
- linhas 128-132: a simulação que acabou de ser criada deve ser adicionada à lista de simulações já calculadas. Esta lista encontra-se 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 esta é novamente colocada na sessão;
- linhas 137-139: o resultado é devolvido ao controlador principal;
23.11.6. Testes [Postman]
Procedemos aos testes [Postman] do controlador [CalculerImpotController] no modo jSON;

Acima:
- em [1-7], faz-se uma solicitação [GET] em vez de [POST];
- em [8-11], a resposta jSON do servidor;
Agora, vamos utilizar um método [POST], com ou sem parâmetros enviados, bem como com parâmetros enviados 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 destes parâmetros, desmarcando a respetiva caixa de seleção em [16]. Isto permitirá testar diferentes cenários. Na captura de ecrã 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], um pedido 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 da aplicação
// processamento de um pedido (Request)
// utiliza a sessão e pode alterá-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 ao 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
- pedido [GET main.php?action=lister-simulations];
- linhas 24-25: verifica-se se existe uma solicitação GET com um único parâmetro;
- linhas 26-31: se não for esse o caso, é devolvido um resultado com erro ao controlador principal;
- linhas 33-37: recupera-se a lista de simulações da sessão, caso esta exista (linha 36); caso contrário, essa lista está vazia (linha 34);
- linhas 39-40: devolve-se a lista de simulações ao controlador principal;
23.11.8. Testes [Postman]
Vamos criar dois testes: um de erro e outro bem-sucedido.

Acima:
- no [1-8], faz-se 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 um pedido válido:

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] tinha 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 da aplicação
// processamento de uma solicitação Request
// utiliza a sessão e pode alterá-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], []];
}
// elimina-se a simulação $numéro
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// recolocam-se as simulações na sessão
$session->set("simulations", $simulations);
// a lista de simulações é devolvida ao cliente
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Comentários
- pedido [GET main.php?action=supprimer-simulation&numéro=x];
- linhas 24-30: verifica-se se existe uma solicitação GET com dois parâmetros;
- linhas 32-38: verifica-se se o parâmetro [numéro] existe nos 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] existe efetivamente. Existem 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 eliminar não existe na lista de simulações;
- linhas 63-66: em caso de erro, é devolvido um resultado com erro ao controlador principal;
- linha 68: a simulação n.º [numéro] é eliminada;
- linha 69: a operação [unset] não altera os índices [0, n-1] da lista. Para os atualizar, solicitam-se os valores da tabela [$simulations] para eliminar a simulação em falta;
- linha 71: insere-se a nova tabela de simulações na sessão;
- linhas 73-74: devolve-se ao controlador principal a nova lista de simulações;
23.11.10. Testes [Postman]
Vamos realizar testes de erro e de sucesso:

Acima:
- em [1-6], um pedido 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 eliminar a simulação n.º 0 da lista, ou seja, a primeira simulação. Em primeiro lugar, vamos solicitar novamente esta lista com a consulta [lister-simulations-500]:

- No [1], existem atualmente 2 simulações;
Eliminamos a primeira simulação (número 0):

Acima:
- no [1-5], elimina-se a simulação n.º 0 [5];
- em [6-9], a resposta jSON do servidor. Vemos que a simulação n.º 0 foi eliminada;
Repitamos esta operação:

Acima:
- em [1], já não restam 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 da aplicação
// processamento de um pedido (Request)
// utiliza a sessão Session e pode alterá-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], []];
}
// memoriza-se o tipo de sessão
$type = $session->get("type");
// invalida-se a sessão atual
$session->invalidate();
// reintroduz-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
- pedido [GET main.php?action=fin-session];
- linhas 25-33: verifica-se se a ação é uma GET com o parâmetro único [fin-action];
- linha 38: invalida-se a sessão atual. Isto elimina os dados registados nessa sessão e é iniciada uma nova sessão;
- linha 36: antes do fim da sessão, guarda-se o tipo [json, xml, html] da mesma;
- linha 40: o tipo da sessão anterior é reposto na nova sessão. Por fim, recomeça-se com uma nova sessão com a chave única [type];
- linhas 44-45: o resultado é devolvido ao controlador principal;
23.11.12. Testes [Postman]
Vamos realizar um teste de erro e um teste de sucesso:

Acima:
- em [1-5], solicita-se o fim 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 na sua resposta:

Acima:
- em [3-4], o cookie de sessão não consta na resposta do servidor. Isso é normal. O servidor só o envia uma vez: no início de uma nova sessão 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;
Vejamos 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 web está a iniciar;
23.12. Tipos de resposta do servidor
23.12.1. Introdução
Voltemos à arquitetura geral da aplicação:

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

Já apresentámos a classe [JsonResponse] no parágrafo sobre links. Esta implementa a interface [InterfaceResponse] e estende a classe [ParentResponse]. O mesmo se aplica às outras duas classes, [XmlResponse] e [HtmlResponse].
Recorde-se 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 {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// matriz $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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 da aplicação. No entanto, a resposta pode necessitar de outras informações. Por isso, são-lhe fornecidos os três primeiros parâmetros (linhas 20-22), que lhe dão acesso a todas as informações relativas ao pedido, à sessão e à configuração;
- linha 26: a resposta necessita do [Logger], uma vez que irá registar a resposta enviada ao cliente;
Recordemos agora o código da classe [ParentResponse], classe-pai dos três tipos de resposta que agrupa 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 estado da resposta
// string $content: o corpo da resposta a enviar
// consoante o caso, trata-se de uma cadeia jSON, XML ou HTML
// matriz $headers: os cabeçalhos HTTP a adicionar à 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 estado
$response->setStatusCode($statusCode);
// cabeçalhos
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// envia-se a resposta
$response->setContent($content);
$response->send();
}
}
Comentários
- linhas 10-13: o significado dos três parâmetros do método [send];
- linha 17: note-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 estado 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, recorde-se o código do controlador principal que solicita o envio da resposta ao cliente:
// adicionam-se as chaves [action, état] à resposta do controlador
$content = ["action" => $action, "état" => $état] + $content;
// instancia-se o objeto [Response] encarregado de 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 — libertam-se os recursos
$logger->close();
exit;
- linha 4: define-se o nome da classe [Response] a instanciar;
- linha 5: instanciamos a classe e enviamos a resposta ao cliente através 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]
Já foi apresentada no parágrafo anterior. No entanto, voltamos a apresentar o seu código para melhor realçar a homogeneidade das três classes de resposta:
A classe [JsonResponse] implementa a interface [InterfaceResponse] da seguinte forma:
<?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 {
// Pedido $request: pedido em processamento
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// array $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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, inserir 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);
// registo
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 esta classe. É esta classe pai que envia a resposta ao cliente (linha 46). Como este código era comum a todos os tipos de [Response], foi fatorizado numa classe pai;
- linhas 33-40: instanciação do serializador [Symfony], que irá converter a resposta do servidor [$content] numa cadeia jSON (linha 42);
- linhas 34-36: o primeiro parâmetro do construtor de [Serializer] é um array. Nesta, insere-se uma instância da classe [ObjectNormalizer], necessária para a serialização de objetos. Este 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 colocam-se todos os codificadores utilizados numa serialização (XML, jSON, CSV…);
- linha 39: aqui haverá apenas um codificador, do tipo [JsonEncoder]. O construtor sem parâmetros poderia ter sido suficiente. Aqui, passámos um parâmetro [JsonEncode] ao construtor, apenas para passar as opções de codificação jSON;
- linha 39: o parâmetro do construtor [JsonEncode] é um array de opções. Aqui, utiliza-se a opção [JSON_UNESCAPED_UNICODE] para solicitar que os caracteres UTF-8 da cadeia jSON sejam representados de forma nativa 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 lhe será enviado jSON;
- linha 46: solicita-se à classe pai que envie a resposta ao cliente;
- linhas 48-50: regista-se a resposta jSON;
23.12.3. A classe [XmlResponse]
A classe [XmlResponse] implementa a interface [InterfaceResponse] da seguinte forma:
<?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 {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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);
// registo
if ($logger !== NULL) {
// registo 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 matriz;
- linha 36: o primeiro array contém uma instância do tipo [ObjectNormalizer], que intervém na serialização de objetos;
- linhas 37-47: o segundo array contém os codificadores utilizados para a serialização. É possível prever vários 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 registar a resposta no ficheiro [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 lhe está a ser enviado um documento XML;
- linha 54: envio efetivo da resposta ao cliente pela classe pai;
- linhas 56-60: registo da resposta em jSON;
23.12.4. Testes [Postman]
Já realizámos todos os testes de erros possíveis no jSON. Não há mais nada a fazer no XML. Apresentamos dois exemplos de resposta XML:

Acima:
- em [1-3], o pedido 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 as alterar e teremos, para cada uma delas, uma resposta XML. Façamos, por exemplo, uma autenticação bem-sucedida:

Acima:
- em [1-3], um pedido de autenticação válido;
- em [4-7], a resposta XML do servidor;
23.12.5. A resposta [HtmlResponse]
Quando o tipo da sessão é [html], é instanciado um objeto do tipo [HtmlResponse] para enviar a resposta ao cliente. Este irá enviar ao cliente um fluxo HTML que depende do código de estado devolvido pelo controlador secundário que processou a ação. Esta correspondência [état=>vue] está registada no ficheiro de configuração [config.json] da seguinte forma:
"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"
Esta 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 devolvido um estado da tabela [700, 221, 400], então deve ser apresentada a vista [vue-authentification.php];
- linha 3: se o controlador secundário tiver devolvido um estado da tabela [200, 300, 341, 350, 800], então deve ser apresentada a vista [vue-calcul-impot.php];
- linha 4: se o controlador secundário tiver devolvido um estado da tabela [500, 600], então deve ser apresentada a vista [vue-liste-simulations.php];
- linha 6: se o controlador secundário tiver devolvido um estado que não conste em nenhuma das tabelas anteriores, então deve ser apresentada a vista [vue-erreurs.php];
As vistas 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 {
// Pedido $request: pedido a ser processado
// Sessão $session: a sessão da aplicação web
// array $config: a configuração da aplicação
// int statusCode: o código de estado da resposta HTTP
// array $content: a resposta do servidor
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
// Logger $logger: o logger para gravar registos
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 registo da resposta
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// a resposta HTML depende do código de estado devolvido pelo controlador
$état = $content["état"];
// a cada estado corresponde uma vista — esta é procurada na configuração da aplicação
// a lista de vistas
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// percorre-se a lista de vistas
while (!$trouvé && $i < count($vues)) {
// estados associados à vista n.º i
$états = $config["vues"][$vues[$i]];
// o relatório procurado encontra-se entre os relatórios associados à vista n.º I?
if (in_array($état, $états)) {
// a vista apresentada será a vista n.º i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// próxima vista
$i++;
}
// Encontrado?
if (!$trouvé) {
// se não existir nenhuma vista para o estado atual da aplicação
// é apresentada a vista de erros
$vueRéponse = $config["vue-erreurs"];
}
// recupera-se a vista HTML para apresentar numa cadeia de caracteres
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// indica-se nos cabeçalhos que se vai enviar HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// a classe pai encarrega-se do envio efetivo da resposta
parent::sendResponse($statusCode, $html, $headers);
// registo em jSON da resposta sem o HTML
if ($logger !== NULL) {
// registo 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 Symfony. Este é necessário para o registo 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 apresentada. Esta depende do código de estado devolvido pelo controlador que processou a ação. Este código encontra-se em [$content[‘état’]] (linha 43);
- linhas 42-61: procura-se a vista que corresponde a esse estado;
- linhas 62-67: se não tiver sido encontrada nenhuma vista, então estamos perante uma situação de código de estado anormal para a aplicação HTML. Este conceito de estados anormais será explicado mais adiante. Neste caso, é apresentada uma vista de erro;
- linhas 68-70: interpreta-se o código PHP da vista selecionada e coloca-se o resultado na variável [$html] (linha 71);
- este código merece algumas explicações. Imaginemos que a vista selecionada seja [vue-authentification.php], que apresenta um formulário web de autenticação:
- linha 69: a função [ob_start] inicia o que a documentação denomina um atraso de saída. Tudo o que é escrito por operações print, require… e que normalmente é enviado imediatamente para o cliente, vai para um buffer de saída (ob=output buffer) sem ser enviado para o cliente;
- linha 70: carrega-se a vista [vue-authentification.php], que é uma vista dinâmica HTML contendo código PHP. Acontecem então duas coisas:
- o código PHP da vista [vue-authentification.php] é carregado e interpretado. O resultado é uma vista a que chamaremos [vue-authentification.html], que contém apenas código HTML, ou mesmo CSS e JavaScript, mas já não contém PHP;
- este código HTML é normalmente enviado ao cliente. Na verdade, é o que acontece com todo o texto encontrado pelo interpretador PHP que não seja código PHP. Devido ao atraso na saída, este código HTML é colocado no buffer de saída sem ser enviado ao cliente;
- linha 71: a função [ob_get_clean] faz duas coisas:
- coloca na variável [$html] o conteúdo do buffer de saída, ou seja, a página [vue-authentification.html] que lá foi colocada;
- esvazia o buffer de saída. Para este, tudo decorre como se nada tivesse acontecido. Além disso, o cliente continua sem ter recebido nada;
- linha 70: estamos aqui durante a execução da classe [HtmlResponse], que se encontra na pasta [Responses]. Para encontrar a vista, é necessário, portanto, subir um nível para [..] e, em seguida, passar para 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: adiciona-se aos cabeçalhos HTTP recebidos como parâmetro (linha 29) o cabeçalho que indica ao cliente que lhe será enviado HTML;
- linha 75: solicita-se à classe pai que proceda ao envio efetivo da resposta ao cliente;
- linhas 77-81: regista-se em jSON a resposta [$content] fornecida pelo controlador secundário que processou a ação em curso;
23.12.6. Testes [Postman]
Para testar efetivamente o modo HTML da sessão, teríamos de analisar todas as vistas. Faremos isso mais tarde. Vamos realizar o seguinte teste:
Vejamos a lista de vistas no ficheiro 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 esta pode ser do tipo XML ou HTML. É este último caso que vai ser testado. De acordo com o ficheiro de configuração, é a vista [vue-authentification.php] que constitui a resposta HTML. Vamos verificar.

Acima:
- em [1-3], inicia-se uma sessão HTML. Espera-se, portanto, uma resposta HTML;
- em [4-8], a resposta HTML do servidor;
- o separador [8] permite obter uma pré-visualização do código HTML recebido;

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

A vista de cálculo do imposto:

A vista da lista de simulações:

A vista dos erros inesperados:

Vamos descrever estas vistas uma a uma.
23.13.2. A vista de autenticação
23.13.2.1. Apresentação da vista
A vista de autenticação é a seguinte:

A vista é composta por dois elementos a que chamaremos fragmentos:
- o fragmento [1] é gerado por um script [v-bandeau.php];
- o fragmento [2] é gerado por um script [v-authentification.php];
A vista 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 - é exibido um aviso 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 baliza <meta charset> indica que o documento está codificado em UTF-8;
- linha 12: a baliza <meta name=’viewport’> define a exibição inicial da vista: em toda a largura do ecrã que a exibe (width) à sua dimensão inicial (initial-scale), sem redimensionamento para se adaptar a um ecrã de dimensão menor (shrink-to-fit);
- linha 14: a baliza <link rel=’stylesheet’> define o ficheiro CSS que controla a aparência da vista. Aqui, utilizamos o framework CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- linha 15: a baliza <title> define o título da página:

- linhas 17-43: o corpo da página web está encapsulado nas tags <body></body>;
- linhas 18-42: a baliza <div> delimita uma secção da página apresentada. Os atributos [class] utilizados na visualização referem-se todos ao framework CSS Bootstrap. A baliza <div class=’container’> delimita um contentor Bootstrap;
- linha 20: inclui-se o script [v-bandeau.php]. Este script gera o banner [1] da página. Iremos descrevê-lo em breve;
- linhas 22-26: a baliza <div class=’row’> delimita uma linha do Bootstrap. Estas linhas são compostas por 12 colunas;
- linha 23: a baliza <div class=’col-md-9’> delimita uma secção de 9 colunas;
- linha 24: inclui-se o script [v-authentification.php] que apresenta o formulário de autenticação [2] da página. Iremos descrevê-lo em breve;
- linha 27: a baliza <?php insere o código PHP no interior da página HTML. Este código é executado antes da exibição da página HTML e pode alterá-la;
- linha 29: todos os dados dinâmicos da vista apresentada serão encapsulados num objeto [$modèle] do tipo [stdClass]. Trata-se de uma escolha arbitrária. Ter-se-ia podido optar por um tabuleiro associativo em vez disso, para obter o mesmo resultado;
- linha 29: a autenticação falha se o utilizador introduzir credenciais incorretas. Neste caso, a vista de autenticação é exibida novamente com uma mensagem de erro. O atributo [$modèle→error] indica se esta mensagem de erro deve ser exibida;
- linhas 30-39: esta sintaxe escreve todo o texto colocado entre os símbolos PHP <<<EOT (linha 30 – pode colocar-se 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 utilizado na linha 30). O símbolo deve ser escrito na 1.ª coluna da linha 39. As variáveis PHP situadas 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 baliza HTML <ul> (lista não ordenada) apresenta uma lista com marcadores. Cada elemento da lista deve ter a sintaxe <li>elemento</li>;
Destacamos neste código os elementos dinâmicos a definir:
- [$modèle→error]: para apresentar 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] apresenta a barra superior em todas as vistas da aplicação 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 barra superior está encapsulada numa secção Bootstrap do tipo Jumbotron [<div class="jumbotron">]. Esta classe Bootstrap aplica um estilo específico ao conteúdo apresentado para o destacar;
- linhas 3-12: uma linha Bootstrap;
- linhas 4-6: uma imagem [img] é colocada nas quatro primeiras colunas da linha;
- linha 5: a sintaxe [<?= $logo ?>] é equivalente à sintaxe [<?php print $logo ?>]. Por outras palavras, o valor do atributo [src] será o valor da variável PHP [$logo];
- linhas 7-11: as outras 8 colunas da linha (recorde-se 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] apresenta o formulário de autenticação da aplicação web:

O código do fragmento [v-authentification.php] é o seguinte:
<!-- formulário HTML — os valores são enviados através da 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">
<!-- descrição -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- campo de introdução 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">
<!-- designação -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- área de introdução 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] numa 3.ª 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 baliza <form> delimita um formulário HTML. Este apresenta, em geral, as seguintes características:
- define campos de introdução de dados (etiquetas <input> nas linhas 17 e 27;
- possui um botão do tipo [submit] (linha 34) que envia os valores introduzidos para o URL indicado no atributo [action] da baliza [form] (linha 2). O método HTTP utilizado para consultar este URL é especificado no atributo [method] da baliza [form] (linha 2);
- neste caso, quando o utilizador clicar no botão [Valider] (linha 34), o navegador irá enviar (linha 2) os valores introduzidos no formulário para o URL [main.php?action=authentifier-utilisateur] (linha 2);
- os valores enviados são os valores introduzidos pelo utilizador nos campos de introdução das linhas 17 e 27. Serão enviados sob a forma [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 secção Bootstrap para apresentar um título num fundo azul:

- linhas 10-37: um formulário Bootstrap. Todos os elementos do formulário serão então estilizados de uma determinada forma;
- 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 baliza [label] associa o rótulo ao atributo [id] do campo de introdução de dados da linha 17;
- linhas 15-19: coloca a área de introdução de dados num conjunto de quatro colunas;
- linha 17: a baliza HTML [input] descreve um campo de introdução de dados. Possui vários parâmetros:
- [type=’text’]: trata-se de um campo de introdução de texto. É possível introduzir qualquer coisa neste campo;
- [class=’form-control’]: estilo Bootstrap para a área de introdução de dados;
- [id=’user’]: identificador do campo de entrada. Este identificador é geralmente utilizado pelo CSS e pelo código JavaScript;
- [name=’user’]: nome do campo de introdução de texto. É com este nome que o valor introduzido pelo utilizador será enviado pelo navegador [user=xx];
- [placeholder=’invite’]: o texto exibido no campo de introdução de dados quando o utilizador ainda não digitou nada;
![]()
- [value=’valeur’]: o texto «valor» será exibido no campo de introdução assim que este for apresentado, ou seja, antes de o utilizador introduzir qualquer outra coisa. Este mecanismo é utilizado em caso de erro para exibir a entrada que provocou o erro. Neste caso, este valor será o valor da variável PHP [$modèle→login];
- linhas 21-30: um código semelhante para a introdução da palavra-passe;
- linha 27: [type=’password’] faz com que haja um campo de introdução de texto (pode-se digitar qualquer coisa), mas os caracteres digitados ficam ocultos:
![]()
- linhas 32-36: uma terceira linha para o botão [Valider];
- linha 34: uma vez que possui o atributo [type=submit], ao clicar neste botão, o navegador envia para o servidor os valores introduzidos, tal como foi explicado anteriormente. O atributo CSS [class="btn btn-primary"] apresenta 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 (não começa por http://machine:port/chemin). No nosso exemplo, todos os URL da aplicação têm o formato [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. A vista de autenticação será obtida com vários 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]
Estes URL referem-se a um documento [main.php] no caminho [http://localhost/php7/scripts-web/impots/version-12]. Este será o caso de todos os URL desta aplicação. O parâmetro [action="main.php?action=authentifier-utilisateur"] será precedido por este caminho no momento do envio dos valores introduzidos. Estes serão, portanto, 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 das vistas muito antes da sua integração na aplicação. Trata-se, neste caso, de testar o seu aspeto visual. Iremos reunir todas as vistas de teste na pasta [Tests] do projeto:

Para testar a vista [vue-authentification.php], temos de criar o modelo de dados que ela irá apresentar:
<?php
// dados de teste da página
//
// calcula-se o modelo da vista
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulamos os dados da página em $modèle
$modèle = new \stdClass();
// identificador do utilizador
$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";
// criamos 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 tem partes dinâmicas controladas pelo objeto [$modèle]. A este objeto chama-se modelo da vista. De acordo com uma das duas definições dadas para a sigla MVC, trata-se aqui do M do MVC;
- linha 5: o modelo da vista é calculado pela função [getModelForThisView];
- linha 9: o modelo da vista será encapsulado num tipo [stdClass];
- linhas 10-22: definem-se valores de teste para os elementos dinâmicos da vista de autenticação;
O teste visual pode ser realizado a partir do NetBeans:

Continuamos estes testes visuais até ficarmos satisfeitos com o resultado.
23.13.2.5. Cálculo do modelo da vista
Uma vez determinado o aspeto visual da vista, pode-se proceder ao cálculo do modelo da vista em condições reais. Recorde-se os códigos de estado que conduzem a esta vista. Encontram-se no ficheiro 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"
São, portanto, os códigos de estado [700, 221, 400] que fazem com que a vista de autenticação seja apresentada. Para descobrir o significado destes códigos, pode-se recorrer aos testes [Postman] realizados na aplicação jSON:
- [init-session-json-700]: 700 é o código de estado após uma ação [init-session] bem-sucedida: é então apresentado o formulário de autenticação em branco;
- [authentifier-utilisateur-221]: 221 é o código de estado após uma ação [authentifier-utilisateur] que falhou (dados de identificação não reconhecidos): é então apresentado o formulário de autenticação para que seja corrigido;
- [fin-session-400]: 400 é o código de estado após uma ação [fin-session] bem-sucedida: é então apresentado o formulário de autenticação em branco;
Agora que sabemos em que momentos o formulário de autenticação deve ser apresentado, podemos calcular o seu modelo em [vue-authentification.php]:

O código de cálculo do modelo da vista [vue-authentification.php] é o seguinte:
<?php
// herdam-se as seguintes variáveis
// Pedido $request: o pedido em curso
// Sessão $session: a sessão da aplicação
// matriz $config: a configuração da aplicação
// 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 vista
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulamos 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 apresentar
$modèle->error = FALSE;
break;
case 221:
// autenticação incorreta
// é apresentado novamente o utilizador introduzido inicialmente
$modèle->login = $request->request->get("user");
// há um erro a apresentar
$modèle->error = TRUE;
// lista HTML de mensagens de erro — neste caso, 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 Symfony utilizadas no código da vista;
- linhas 15-40: a função [getModelForThisView] é responsável por calcular o modelo da vista;
- linha 19: recupera-se o código de estado devolvido pelo controlador que processou a ação em curso;
- linhas 21-37: o modelo depende deste código de estado;
- linhas 22-28: caso em que é necessário apresentar um formulário de autenticação em branco;
- linhas 29-37: caso de autenticação incorreta: exibe-se o identificador introduzido pelo utilizador e apresenta-se uma mensagem de erro. O utilizador pode então tentar novamente a autenticação;
Foi criado um modelo específico para o banner [v-bandeau.php]:
<?php
// logótipo
$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 logótipo da faixa. Em vez de calcular esta variável quatro vezes para as quatro vistas da aplicação, este cálculo é fatorizado no fragmento [v-bandeau.php];
- as linhas 1-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á criámos pedidos que geram os códigos [700, 221, 400], os quais apresentam a página de autenticação. Recapitulemos:
- [init-session-html-700]: 700 é o código de estado após uma ação [init-session] bem-sucedida: é então apresentado o formulário de autenticação em branco;
- [authentifier-utilisateur-221]: 221 é o código de estado após uma ação [authentifier-utilisateur] que falhou (dados de identificação não reconhecidos): é então apresentado o formulário de autenticação para que seja corrigido;
- [fin-session-400]: 400 é o código de estado após uma ação [fin-session] bem-sucedida: é então apresentado o formulário de autenticação em branco;
Basta reutilizá-los e verificar se apresentam corretamente a página de autenticação. Aqui, apresentaremos apenas dois testes:
- [init-session-html-700]: início de uma sessão HTML;

- [authentifier-utilisateur-221]: autenticação do utilizador [x, x];

Acima:
- a solicitação enviou a cadeia [user=x&password=x];
- em [4], é apresentada uma mensagem de erro;
- em [3], o utilizador incorreto foi apresentado novamente;
23.13.2.7. Conclusão
Conseguimos testar a vista [vue-authentification.php] sem ter escrito as outras vistas. Isto foi possível porque:
- todos os controladores estão implementados;
- o [Postman] permite-nos enviar pedidos ao servidor sem precisarmos das vistas. Ao escrever os controladores, é preciso ter em conta que qualquer pessoa pode fazê-lo. Por isso, é preciso estar preparado para lidar com pedidos que nenhuma vista permitiria. Estes são criados manualmente em [Postman]. Nunca se deve assumir a priori que «este pedido é impossível». É preciso verificar;
23.13.3. A vista de cálculo do imposto
23.13.3.1. Apresentação da vista
A vista de cálculo do imposto é a seguinte:

A vista 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 vista de cálculo do imposto é gerada pelo seguinte script [vue-calcul-impot.php]:

<?php
// herdam-se as seguintes variáveis
// Request $request: o pedido em curso
// Sessão $session: a sessão da aplicação
// matriz $config: a configuração da aplicação
// 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 vista
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulam-se os dados da página em $modèle
$modèle = new \stdClass();
…
// renderiza-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"; ?>
<!-- linha com 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
- apenas comentamos novidades que ainda não foram abordadas;
- linha 37: inclusão da barra superior da vista na primeira linha Bootstrap da vista;
- linhas 41-43: inclusão do menu que ocupará três colunas da segunda linha Bootstrap da vista;
- linhas 45-47: inclusão do formulário de cálculo de impostos, que ocupará nove colunas da segunda linha Bootstrap da vista;
- linhas 51-69: se o cálculo do imposto for bem-sucedido ([$modèle→success=TRUE]), então o resultado do cálculo do imposto é apresentado numa caixa verde (linhas 59-65). Este quadro encontra-se na terceira linha Bootstrap da vista (linha 54) e ocupa nove colunas (linha 58) à direita de três colunas vazias (linhas 55-57). Este quadro ficará, portanto, imediatamente abaixo do formulário de cálculo do imposto;
- linhas 71-87: se o cálculo do imposto falhar ([$modèle→error=TRUE]), é exibida uma mensagem de erro num quadro cor-de-rosa (linhas 80-83). Este quadro encontra-se na terceira linha Bootstrap da vista (linha 75) e ocupa nove colunas (linha 79) à direita de três colunas vazias (linhas 76-78). Este quadro ficará, portanto, 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] apresenta o formulário de autenticação da aplicação 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 sobre 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 introdução numérica do 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">
<!-- legenda em 4 colunas -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- campo de introdução numérica do 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 introdução:
- o valor do botão de opção selecionado, na forma:
- [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 introdução numérica da linha 37 na forma [enfants=xx], em que [enfants] é o valor do atributo [name] da linha 37, e [xx] o valor introduzido pelo utilizador através do teclado;
- o valor do campo de introdução numérica da linha 46 na forma [salaire=xx], em que [salaire] é o valor do atributo [name] da linha 46, e [xx] o valor introduzido pelo utilizador através do teclado;
- o valor do botão de opção selecionado, na forma:
Por fim, o valor lançado terá o formato [marié=xx&enfants=yy&salaire=zz].
- os valores introduzidos serão enviados quando o utilizador 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 têm o mesmo atributo [name] (linhas 18, 24). O navegador garante que, num grupo de botões de opção, apenas um esteja selecionado de cada vez. Assim, clicar num deles desativa aquele que estava selecionado anteriormente;
- são botões de opção devido ao atributo [type="radio"] (linhas 18, 24);
- ao apresentar o formulário (antes do preenchimento), um dos botões de opção deverá estar selecionado: para tal, basta adicionar o atributo [checked=’checked’] à baliza <input type="radio"> em questão. Isto é feito com variáveis dinâmicas:
- [<?= $modèle->checkedOui ?>] na linha 18;
- [<?= $modèle->checkedNon ?>] na linha 24;
Estas variáveis farão parte do modelo da vista.
- linha 37: um campo de introdução numérica [type="number"] com um valor mínimo de 0 [min="0"]. Nos navegadores mais recentes, isto significa que o utilizador só poderá introduzir um número >=0. Nesses mesmos navegadores mais recentes, a introdução pode ser feita através de um controlador deslizante em que se pode clicar para aumentar ou diminuir o valor. O atributo [step="1"] da linha 37 indica que o controlador funcionará com incrementos de 1 unidade. Isto implica que o controlador só aceitará valores inteiros que variem de 0 a n, com um incremento de 1. Para a introdução manual, isto significa que os números com vírgula não serão aceites;
![]()
- linha 37: em certas visualizações, o campo de introdução de dados relativos aos filhos deverá ser pré-preenchido com a última entrada efetuada nesse campo. Para tal, utiliza-se o atributo [value], que define o valor a apresentar no campo de introdução de dados. Este valor será dinâmico e gerado pela variável [$modèle→enfants];
- linha 46: as explicações para a introdução do salário são as mesmas que para a introdução dos filhos;
- linha 53: o botão do tipo [submit] que aciona o POST com os valores introduzidos no URL e no [main.php?action=calculer-impot];

23.13.3.3. O fragmento [v-menu.php]
Este fragmento apresenta 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 baliza HTML [nav] enquadra uma parte do documento HTML que apresenta ligações de navegação para outros documentos;
- linha 7: a baliza 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 é 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 Bootstrap que definem a aparência do menu;
23.13.3.4. Teste visual
Reunimos estes diferentes elementos na pasta [Tests] e criamos um modelo de teste para a vista [vue-calcul-impot.php]:

O modelo de dados da vista [vue-calcul-impot] será o seguinte:
<?php
// dados de teste da página
//
// calcula-se o modelo da vista
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulamos 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"];
// constrói-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-sessão'];
// imagem do banner
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// apresenta o 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 vista [vue-calcul-impot.php] e dos fragmentos [v-calcul-impot.php] e [v-menu.php];
Testa-se a vista [vue-calcul-impot.php]:

Obtém-se o seguinte resultado:

Trabalhamos nesta vista até que o resultado obtido visualmente nos satisfaça. Podemos então passar à integração da vista na aplicação web que estamos a desenvolver.
23.13.3.5. Cálculo do modelo da vista

Uma vez definido o aspeto visual da vista, podemos proceder ao cálculo do modelo da vista em condições reais. Recorde-se os códigos de estado que conduzem a esta vista. Encontram-se no ficheiro 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"
São, portanto, os códigos de estado [200, 300, 341, 350, 800] que fazem com que seja apresentada a página de autenticação. Para conhecer o significado destes códigos, pode-se recorrer aos testes [Postman] realizados na aplicação jSON:
- [authentifier-utilisateur-200]: 200 é o código de estado após uma ação [authentifier-itilisateur] bem-sucedida: é então apresentado o formulário de cálculo de impostos em branco;
- [calculer-impot-300]: 300 é o código de estado após uma ação [calculer-impot] bem-sucedida. É então apresentado o formulário de cálculo com os dados que nele foram introduzidos e o montante do imposto. O utilizador pode então efetuar outro cálculo;
- [fin-session-400]: 400 é o código de estado após o sucesso de uma ação [fin-session]: é então apresentado o formulário de autenticação em branco;
- o código de estado [341] é o obtido para um cálculo de imposto válido, mas a ausência de ligação ao SGBD provoca um erro;
- o código de estado [350] é aquele obtido para um cálculo de imposto válido, mas a ausência de ligação ao servidor [Redis] provoca um erro;
- o código de estado [800] será apresentado posteriormente. Ainda não o encontrámos;
- partimos aqui do pressuposto de que o utilizador utiliza um navegador recente. Assim, com o formulário em análise, não é possível introduzir números negativos, cadeias de caracteres não numéricos ou números com vírgula nos campos de introdução de dados [enfants, salaire]. Com navegadores mais antigos, isso seria possível. Trataremos estes erros como erros inesperados e, nesse caso, exibiremos a vista [vue-erreurs];
Agora que sabemos em que momentos deve ser apresentado o formulário de cálculo do imposto, podemos definir o seu modelo em [vue-calcul-impot.php]:
<?php
// herdam-se as seguintes variáveis
// Request $request: o pedido em curso
// Sessão $session: a sessão da aplicação
// matriz $config: a configuração da aplicação
// 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 vista
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulam-se 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 reposto com os valores introduzidos
$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:
// base de dados HS
case 350:
// servidor Redis HS
// formulário repovoado com os valores introduzidos
$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"];
// o modelo é apresentado
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. Voltam a ser apresentados os valores introduzidos, bem como o montante 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] permite-nos obter o código de estado 300. Este corresponde a um cálculo de imposto bem-sucedido:

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

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

A vista gerada pelo script [vue-liste-simulations] tem três partes:
- 1: a barra 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 vista
$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();
…
// renderiza-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"; ?>
<!-- linha com 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 da aplicação [1];
- linha 33: inclusão do menu [2]. Será apresentado em três colunas por baixo do banner;
- linha 37: inclusão da tabela de simulações [3]. Será apresentada em nove colunas, por baixo do banner e à direita do menu;
Já comentámos dois dos três fragmentos desta vista:
O fragmento [v-liste-simulations.php] é o seguinte:
<!-- mensagem sobre 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 apresentados) -->
<tbody>
<?php
$i = 0;
// cada simulação é apresentada ao percorrer a tabela de simulações
foreach ($modèle->simulations as $simulation) {
// exibição de uma linha da tabela com 6 colunas - baliza <tr>
// coluna 1: cabeçalho da linha (n.º da simulação) - baliza <th scope='row'>
// coluna 2: valor do parâmetro [marié] - baliza <td>
// coluna 3: valor do parâmetro [enfants] - baliza <td>
// coluna 4: valor do parâmetro [salaire] - baliza <td>
// coluna 5: valor do parâmetro [impôt] (do imposto) - baliza <td>
// coluna 6: valor do parâmetro [surcôte] - baliza <td>
// coluna 7: valor do parâmetro [décôte] - baliza <td>
// coluna 8: valor do parâmetro [réduction] - baliza <td>
// coluna 9: valor do parâmetro [taux] (do imposto) - baliza <td>
// coluna 10: link para eliminar a simulação - baliza <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 baliza <table> (linhas 6 e 58);
- os cabeçalhos das colunas da tabela são definidos dentro de uma baliza <thead> (cabeçalho da tabela, linhas 8 e 21). A baliza <tr> (linha da tabela, linhas 9 e 20) delimita uma linha. Nas linhas 10 a 15, a baliza <th> (cabeçalho da tabela) define um cabeçalho de coluna. Existem, portanto, dez. [scope="col"] indica que o cabeçalho se aplica à coluna. [scope="row"] indica que o cabeçalho se aplica à linha;
- linhas 23-57: a baliza <tbody> enquadra os dados apresentados pela tabela;
- linhas 40-51: a baliza <tr> delimita uma linha da tabela;
- linha 41: a baliza <th scope=’row’> define o cabeçalho da linha;
- linhas 42-50: cada baliza <td> define uma coluna da linha;
- linha 27: a lista de simulações encontra-se no modelo [$modèle→simulations], que é uma tabela associativa;
- linha 50: um link para eliminar a simulação. O modelo URL retoma o número apresentado na 1.ª coluna da tabela (linha 41);
23.13.4.2. Teste visual
Reunimos estes diferentes elementos na pasta [Tests] e criamos um modelo de teste para a vista [vue-liste-simulations.php]:

O modelo de dados da vista [vue-liste-simulations] será o seguinte:
<?php
// calcula-se o modelo da vista
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulam-se os dados da página em $modèle
$modèle = new \stdClass();
// colocam-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 apresentada pela tabela HTML;
- linhas 32-34: a tabela das opções do menu;
Vamos apresentar esta vista:

Obtém-se o seguinte resultado:

Trabalhamos nesta vista até que o resultado visual nos satisfaça. Podemos então passar à integração da vista na aplicação web que estamos a desenvolver.
23.13.4.3. Cálculo do modelo da vista

Uma vez definido o aspeto visual da vista, podemos proceder ao cálculo do modelo da vista em condições reais. Recorde-se os códigos de estado que conduzem a esta vista. Encontram-se no ficheiro 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"
São, portanto, os códigos de estado [500, 600] que fazem com que a vista das simulações seja apresentada. Para descobrir o significado destes códigos, pode-se recorrer aos testes [Postman] realizados na aplicação jSON:
- [lister-simulations-500]: 500 é o código de estado após uma ação [lister-simulations] bem-sucedida: é então apresentada a lista das simulações realizadas pelo utilizador;
- [supprimer-simulation-600]: 600 é o código de estado após uma ação [supprimer-simulation] bem-sucedida. É então apresentada a nova lista de simulações obtida após essa eliminação;
Agora que sabemos em que momentos a lista de simulações deve ser apresentada, podemos calcular o seu modelo em [vue-liste-simulations.php]:
<?php
// herdam-se as seguintes variáveis
// Request $request: o pedido em curso
// Sessão $session: a sessão da aplicação
// matriz $config: a configuração da aplicação
// 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 vista
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulamos os dados da página em $modèle
$modèle = new \stdClass();
// colocam-se as simulações no formato esperado pela página
// estas encontram-se na resposta do controlador que executou a ação
// sob a forma de uma matriz de objetos do tipo [Simulation]
$objetsSimulation = $content["réponse"];
// cada objeto [Simulation] será transformado numa 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"];
// criamos 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] permite-nos obter o código de estado 500. Corresponde a um pedido para visualizar as simulações:

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

23.13.5. A visualização de erros inesperados
Denomina-se aqui «erro inesperado» um erro que não deveria ter ocorrido no âmbito da utilização normal da aplicação web.
Tomemos como exemplo o teste [Postman] [calculer-impot-3xx], definido da seguinte forma:

- em [1-3], um pedido POST com a ação [calculer-impot];
- em [4-6]: aqui é possível definir o que se quiser para os três parâmetros do POST:
- [4]: falta o parâmetro [marié];
- [5-6]: os parâmetros [enfants, salaire] estão presentes, mas são inválidos;
- no [9], estes três erros são assinalados com o código de estado 338;
No entanto, no formulário HTML da aplicação web, esta situação 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];
- num navegador recente, os atributos <input type=’number’ min=’0’ step=’1’ …> fazem com que os valores introduzidos para os filhos e o salário sejam obrigatoriamente números inteiros >=0;
No entanto, nada impede um utilizador de selecionar [Postman] e enviar ao nosso servidor o teste [calcul-impot-3xx] acima referido. Vimos que a nossa aplicação web sabia responder corretamente a esta solicitação. Chamaremos de «erro inesperado» um erro que não deveria ocorrer no contexto da aplicação HTML. Se ocorrer, é provável que alguém esteja a tentar «hackear» a aplicação. Por motivos pedagógicos, decidimos apresentar uma página de erros para estes casos. Na realidade, poderíamos voltar a apresentar a última página enviada ao cliente. Para tal, basta registar na sessão a última resposta HTML enviada. Em caso de erro inesperado, reenviamos essa resposta. Assim, o utilizador terá a impressão de que o servidor não responde aos seus erros, uma vez que a página apresentada não muda.
23.13.5.1. Apresentação da vista
A vista que apresenta os erros inesperados é a seguinte:

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

<?php
// calcula-se o modelo da vista
$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 com 12 colunas -->
<?php require "v-bandeau.php"; ?>
<!-- linha com duas colunas -->
<div class="row">
<!-- menu de 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 da aplicação [1];
- linha 32: inclusão do menu [2]. Será apresentado em três colunas por baixo do banner;
- linhas 34-44: exibição da área de erros em nove colunas;
- linhas 37-44: a operação [print] que apresenta os erros inesperados;
- linha 38: esta apresentação será feita num quadro Bootstrap com fundo rosa;
- linha 39: um texto de apresentação;
- linha 40: a baliza <ul> enquadra uma lista com marcadores. Esta lista com marcadores é fornecida pelo modelo [$modèle->erreurs];
Já comentámos os dois fragmentos desta vista:
23.13.5.2. Teste visual
Reunimos estes diferentes elementos na pasta [Tests] e criamos um modelo de teste para a vista [vue-erreurs.php]:

O modelo de dados da vista [vue-erreurs.php] será o seguinte:
<?php
// calcula-se o modelo da vista
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsula os dados da página em $modèle
$modèle = new \stdClass();
// a tabela de erros inesperados
$erreurs = ["erreur1", "erreur2"];
// constrói-se 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";
// retorna-se 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 apresentar esta vista:

Obtém-se o seguinte resultado:

Trabalhamos nesta vista até que o resultado visual nos satisfaça. Podemos então passar à integração da vista na aplicação web que estamos a desenvolver.
23.13.5.3. Cálculo do modelo da vista

Uma vez definido o aspeto visual da vista, podemos proceder ao cálculo do modelo da vista em condições reais. Recorde-se os códigos de estado que conduzem a esta vista. Encontram-se no ficheiro 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"
São, portanto, os códigos de estado que não constam nas linhas [2-4] que fazem com que seja apresentada a vista de erros inesperados.
O código de cálculo do modelo da vista [vue-erreurs.php] é o seguinte:
<?php
// herdam-se as seguintes variáveis
// Request $request: o pedido em curso
// Sessão $session: a sessão da aplicação
// matriz $config: a configuração da aplicação
// 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 vista
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulamos 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;
}
// constrói-se 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 vista [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] permite-nos 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 da aplicação
Vamos abordar aqui a implementação das ações do menu. Recorde-se o significado dos links que encontrámos
Vista | 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 vista 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] | Mostrar a vista do cálculo do imposto |
[Liste des simulations] | [main.php?action=lister-simulations] | ||
[Fin de session] | [main.php?action=fin-session] |
É importante recordar que um clique num link provoca 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 através de um POST, já não é possível utilizar um link, a menos que este 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 vistas: o servidor jSON ou XML não tem qualquer motivo para a implementar, uma vez que não possui o conceito de vista. É o servidor HTML que introduz este conceito.
Temos, portanto, de implementar a ação [afficher-calcul-impot]. Isto permitir-nos-á rever o procedimento de implementação de uma ação no servidor.
Em primeiro lugar, temos de adicionar um novo controlador secundário. Vamos chamá-lo de [AfficherCalculImpotController]:

Este controlador deve ser adicionado ao ficheiro 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 o seu controlador;
- linha 35: o novo controlador irá devolver o código de estado 800. Numa mudança de vista, não pode haver erros;
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 da aplicação
// processamento de um pedido Request
// utiliza a sessão Session e pode alterá-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 vista — basta definir um código de estado
return [Response::HTTP_OK, 800, ["réponse" => ""], []];
}
}
Comentários
- linha 10: tal como os outros controladores secundários, o novo controlador implementa a interface [InterfaceController];
- as alterações de vista são simples de implementar: basta definir o código de estado associado à vista de destino, neste caso o código 800, tal como visto anteriormente;
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 vistas em situação real. Precisamos de uma forma de inicializar a sessão HTML. Sabemos que é necessário enviar ao servidor os parâmetros [action=init-session&type=html]. Para evitar ter de os digitar na barra de endereços do navegador, vamos adicionar o script [index.php] à nossa aplicação:

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 ao navegador do cliente que se redirecione para o destino URL indicado em [Location]. O script [index.php] é solicitado juntamente 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], irá 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 arranque pode ser simplificado para [http://localhost/php7/scripts-web/impots/version-12/]. Caso não seja especificada nenhuma página no URL, as páginas [index.html, index.php] são utilizadas por predefinição. Neste caso, será utilizado o script [index.php];
Vamos lá: apresentamos agora algumas sequências de vistas.
No nosso navegador, ativamos o acompanhamento das solicitações (F12 no Firefox) e solicitamos a URL de inicialização [https://localhost/php7/scripts-web/impots/version-12/]:

- em [4], a primeira resposta do servidor é um redirecionamento 302:
- em [5], é efetuada uma nova solicitação para o URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html];
Vamos analisar 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á esta redireção com um novo pedido GET:

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

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


Vamos solicitar a lista de simulações:

Vamos eliminar a primeira simulação:

Terminemos a sessão:

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

Vamos agora analisar o cliente jSON [A] do serviço web [B]. O cliente [A], tal como o serviço web [B], possui uma estrutura em camadas:

Esta arquitetura reflete-se 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;
// registo 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;
// eliminar uma simulação
public function supprimerSimulation(int $numéro): array;
// início da sessão
public function initSession(string $type = 'json'): void;
// fim de sessão
public function finSession(): void;
}
Comentários
- linha 9: o método [getTaxPayersData] permite processar o ficheiro jSON com os dados dos contribuintes. Este método é implementado pela função [TraitDao], já comentada (parágrafo «ligação»);
- linha 15: o método [saveResults] permite guardar os resultados de vários cálculos de impostos num ficheiro jSON. Também neste caso, este 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 aceites 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 {
// utilização 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 apresentar as respostas do servidor na consola;
- linha 14: o cookie de sessão. A função deste foi descrita na versão 09 do cliente (parágrafo «ligação»);
- 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];
}
}
}
}
Uma vez que 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 pretendido é passado como parâmetro. Na ausência de parâmetro, será iniciada uma sessão jSON;
- linhas 5-11: é efetuada uma solicitação GET ao serviço web;
- linhas 7-8: os dois parâmetros da GET;
- linha 10: no 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. Apresenta-a sob a forma de uma tabela. 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. Este 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) {
// recupera-se a resposta
$json = $response->getContent(false);
// registos
if ($this->verbose) {
print "$json\n";
}
// recuperar o estado da resposta
$statusCode = $response->getStatusCode();
// erro?
if ($statusCode !== 200) {
// ocorre um erro
throw new ExceptionImpots($json);
}
// envia-se 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. Recorde-se que o parâmetro [false] existe para impedir que o Symfony lance uma exceção quando o estado da resposta HTTP do servidor se encontra no domínio [3xx, 4xx, 5xx];
- linhas 5-7: se estivermos no modo [$verbose], então exibimos a resposta do servidor na consola;
- linhas 9-14: se o estado da resposta HTTP do servidor for diferente de 200, então é lançada uma exceção com a resposta jSON do servidor como mensagem de erro;
- linha 16: a cadeia jSON é descodificada numa matriz;
- linha 17: as informações úteis encontram-se 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();
// envia-se o pedido 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: o pedido 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: lê-se a resposta. Sabe-se que, em caso de erro (código HTTP diferente de 200), o método [getResponse] lança ele próprio 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 chegarmos até aqui, significa que o método [getResponse] não lançou nenhuma exceção. Devolvemos um objeto [Simulation] inicializado com o array devolvido 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]
]);
// obtém-se 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 é confiada 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 um array de objetos associativos
// vamos transformá-lo num array de objetos «Simulation»
$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 resultante 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 chegarmos até aqui, significa que 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: devolve-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 o pedido 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: é efetuada uma consulta GET;
- linhas 6-9: os dois parâmetros do URL;
- linha 14: após uma eliminação, o servidor devolve a nova tabela de simulações. Devolvemos esta tabela;
23.14.2.2.8. O método [finSession]
Uma sessão de trabalho com o serviço web termina normalmente 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: é efetuada uma solicitação GET;
- linhas 6-8: o único parâmetro do URL;
- linha 13: lê-se 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 dos 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;
// registo dos resultados
public function saveResults(string $resultsFilename, array $simulations): void;
// eliminar uma simulação
public function supprimerSimulation(int $numéro): array;
// início de 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 outros 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 {
// deixa-se que as exceções provenientes da camada [dao] sejam reenviadas
// recuperam-se os dados dos contribuintes
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela de resultados
$simulations = [];
// analisam-se os resultados
foreach ($taxPayersData as $taxPayerData) {
// calcula-se o imposto
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// registo 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] necessita de uma referência à camada [dao];
- linhas 20-38: apenas o método [executeBatchImpots] é específico da camada [métier]. A implementação dos outros métodos delega o trabalho a realizar aos métodos com os mesmos nomes na camada [dao];
- linha 23: recorre-se à camada [dao] para obter, numa tabela de objetos do tipo [TaxPayerData], os dados dos contribuintes;
- linha 25: acumulam-se na tabela [$simulations] as diferentes simulações calculadas;
- 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 guardados num ficheiro jSON;
Nota: A camada [métier] praticamente não faz nada. Poder-se-ia decidir eliminá-la e reunir tudo na camada [dao].
23.14.4. O script principal

O script principal é configurado pelo seguinte ficheiro [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 rigoroso dos 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 ficheiro 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 dos 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 gravação dos resultados
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// lista de simulações
$clientMetier->listerSimulations();
// eliminação de uma simulação
$simulations = $clientMetier->supprimerSimulation(1);
// gravação 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) {
// é exibido o erro
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// fim
print "Terminé\n";
exit();
Comentários
- linhas 12-16: utilização do ficheiro 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 guardados (2.º parâmetro NULL);
- linha 50: solicitam-se os resultados de todos estes cálculos;
- linha 52: elimina-se a simulação n.º 1 (a segunda da lista);
- linha 54: guardam-se as simulações restantes;
- linha 56: encerra-se a sessão. Isto 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 tem de ser repetida. Por isso, deve ocorrer uma exceção a indicar que não estamos autenticados;
O ficheiro [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
}
]
Existem 12 contribuintes, dos quais 1 está incorreto. Isso perfaz, portanto, um total de 11 simulações. Uma delas será eliminada. Devem restar 10.
Após a execução do script principal, o ficheiro 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
}
]
Existem, de facto, 10 simulações.
O ficheiro jSON [errors.json] tem o seguinte conteúdo:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Os resultados da consola são os seguintes (no modo detalhado, as respostas jSON do servidor são apresentadas na consola):
{"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]
Tal como aconteceu com os 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 rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// definição das constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12");
// caminho do ficheiro de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config.json");
// recuperar 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";
}
// 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: recorde-se 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, previamente, inicializar uma sessão jSON e autenticar-se;
Os resultados do teste são os seguintes:

Devem ser realizados muitos outros testes:
- testar os diferentes métodos da camada [dao];
- testar os estados devolvidos pelo servidor web. Estes estados são importantes, uma vez que o seu valor determina a página HTML a apresentar;