Skip to content

17. Serviços web

Nota: por “serviço web”, entende-se aqui qualquer aplicativo web que forneça dados brutos consumidos por um cliente — um script de console, nos exemplos a seguir. Não estamos interessados em uma tecnologia específica, como REST (REpresentational State Transfer) ou SOAP (Simple Object Access Protocol), por exemplo, que fornecem dados mais ou menos brutos em um formato bem definido. O REST fornece jSON, enquanto que, para o SOAP, o resultado é XML. Cada uma dessas tecnologias descreve com precisão a maneira como o cliente deve consultar o servidor e o formato que a resposta deste deve assumir. Neste curso, seremos muito mais flexíveis quanto à natureza da solicitação do cliente e da resposta do servidor. No entanto, os scripts escritos e as ferramentas utilizadas são semelhantes aos da tecnologia REST.

17.1. Introduction

Como os programas PHP podem ser executados por um servidor WEB, tal programa se torna um programa servidor capaz de atender a vários clientes. Do ponto de vista do cliente, chamar um serviço web equivale a solicitar o URL desse serviço. O cliente pode ser escrito em qualquer linguagem, inclusive em PHP. Nesse último caso, utilizam-se as funções de rede que acabamos de ver. Além disso, precisamos saber “conversar” com um serviço web, ou seja, compreender o protocolo http de comunicação entre um servidor WEB e seus clientes. Esse era o objetivo do parágrafo anterior.

O cliente web descrito no parágrafo “link” nos permitiu descobrir uma parte do protocolo HTTP.

Image

Em sua versão mais simples, as trocas entre cliente e servidor são as seguintes:

  • o cliente abre uma conexão com a porta 80 do servidor web;
  • ele faz uma solicitação referente a um documento;
  • o servidor web envia o documento solicitado e encerra a conexão;
  • o cliente, por sua vez, encerra a conexão;

O documento pode ser de diversos tipos: um texto no formato HTML, uma imagem, um vídeo… Pode ser um documento existente (documento estático) ou um documento gerado dinamicamente por um script (documento dinâmico). Neste último caso, fala-se em programação web. O script de geração dinâmica de documentos pode ser escrito em diversas linguagens: PHP, Python, Perl, Java, Ruby, C#, VB.net…

A seguir, utilizaremos scripts PHP para gerar dinamicamente documentos de texto.

Image

  • no [1], o cliente estabelece uma conexão com o servidor, solicita um script PHP e pode ou não enviar parâmetros para esse script;
  • no [2], o servidor web faz com que o script PHP seja executado pelo interpretador PHP. O script gera um documento que é enviado ao cliente [3];
  • o servidor encerra a conexão. O cliente faz o mesmo;

O servidor web pode atender a vários clientes ao mesmo tempo.

Com o pacote de software [Laragon], o servidor web é um servidor Apache, um servidor de código aberto da Apache Foundation (http://www.apache.org/). Nas aplicações a seguir, o [Laragon] deve ser iniciado:

Image

Isso inicia o servidor web Apache, bem como os processos SGBD e MySQL.

Os scripts executados pelo servidor web serão escritos com a ferramenta NetBeans. Até agora, escrevemos scripts PHP executados em um contexto de console:

Image

O usuário utiliza o console para solicitar a execução de um script PHP e receber os resultados.

Nas aplicações cliente/servidor que se seguirão:

  • o script do cliente é executado em um contexto de console;
  • o script do servidor é executado em um contexto web;

Image

O script PHP do servidor não pode estar em qualquer lugar no sistema de arquivos. Na verdade, o servidor web procura, nos locais especificados pela configuração, os documentos estáticos e dinâmicos que lhe são solicitados. A configuração padrão do Laragon faz com que os documentos sejam procurados na pasta <Laragon>/www, onde <Laragon> é a pasta de instalação do Laragon. Assim, se um cliente da web solicitar um documento D com o caminho URL [http://localhost/D], o servidor web fornecerá o documento D do caminho [<Laragon>/www/D].

Nos exemplos a seguir, colocaremos os scripts do servidor na pasta [www/php7/scripts-web]. Se um script do servidor se chamar S.php, ele será solicitado ao servidor web com o URL [http://localhost/php7/scripts-web/S.php]. O documento [<Laragon>/www/php7/scripts-web/S.php] será então fornecido.

Image

  • em [1], a pasta [<laragon>/www];
  • em [2], a pasta [php7/scripts-web];

Para criar scripts de servidor com o NetBeans, procederemos da seguinte maneira:

Image

  • em [1-2], criamos um novo projeto
  • em [3-4], selecionamos a categoria [PHP] e o projeto [PHP Application]

Image

  • em [5], o nome do projeto;
  • em [6], a pasta do projeto no sistema de arquivos. Observe que esta está na pasta [<laragon>/www], onde deve estar;
  • em [7-8], aceite os valores padrão propostos;
  • em [9-10], aceite os valores padrão propostos. Em [10], observe que o URL dos scripts que colocaremos neste projeto começará com o caminho [http://localhost/php7/scripts-web/];

Image

  • em [11], são oferecidos frameworks web escritos em PHP. Esses frameworks são indispensáveis assim que a aplicação web ganha um pouco de magnitude;
  • em [12], é possível adicionar bibliotecas PHP usando a ferramenta [Composer]. Utilizamos essa ferramenta duas vezes em uma janela [Terminal] do Laragon:
    • para instalar a biblioteca [SwiftMailer], que permite enviar e-mails;
    • para instalar a biblioteca [php-mime-mail-parser], que permite ler e-mails;
  • no [13], após a validação do assistente de criação do projeto, este aparece no [13] na aba de projetos;

17.2. Criação de uma página estática

Observação: Para prosseguir, é necessário que o [Laragon] esteja em execução.

Mostraremos como criar uma página estática HTML (HyperText Markup Language) usando o NetBeans:

Image

  • no [1-5], criamos uma pasta chamada [01];

Image

Image

  • em [6-12], criamos um arquivo HTML [exemple-01.html];

O arquivo [exemple-01.html] é gerado pré-preenchido da seguinte forma (maio de 2019):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

Vamos alterar seu conteúdo da seguinte forma:


<!DOCTYPE html>
<html>
    <head>
        <title>PHP7 par l'exemple</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b>Ceci est un exemple de page statique</b></div>
    </body>
</html>

Alteramos o título da página (linha 4) e também seu conteúdo (linha 9).

Agora, vamos fazer com que o servidor Apache do Laragon exiba essa página HTML:

Image

  • em [1-2], exibimos a página pelo servidor Apache do Laragon;
  • em [3], o URL da página exibida;
  • em [4], o título que modificamos;
  • em [5], o conteúdo que modificamos;

A página exibida é uma página estática: é possível carregá-la quantas vezes quisermos no navegador (F5), e sempre é exibido o mesmo conteúdo.

A maioria dos navegadores permite acessar os dados trocados entre o cliente e o servidor, aqueles descritos no parágrafo sobre links. No navegador Firefox (maio de 2019), é preciso digitar F12 para acessar esses dados:

Image

Conforme indicado em [1], vamos recarregar a página (F5):

Image

  • em [2], o documento carregado pelo navegador: vamos selecioná-lo;

Image

  • em [5], o documento a ser analisado está selecionado;
  • em [3-4], solicitamos a exibição das trocas cliente/servidor;
  • em [6], essas trocas;

Image

  • em [7], seleciona-se a aba de cabeçalhos;
  • em [8], o URL solicitado pelo navegador;
  • em [9], o comando enviado ao servidor é [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1];
  • em [10], os cabeçalhos HTTP enviados em seguida pelo navegador (o cliente);
  • em [11], os cabeçalhos HTTP da resposta do servidor;

Image

  • em [12-14], a resposta do servidor enviada após os cabeçalhos HTTP;
  • em [14], vemos que o navegador do cliente recebeu a página HTML que criamos. Em seguida, ele interpretou esse código para exibir o seguinte:

Image

17.3. Criação de uma página dinâmica em PHP

Agora vamos escrever uma página dinâmica em PHP:

Image

Image

  • no [1-8], criamos uma página [exemple-01.php];

O arquivo [exemple-01.php] é gerado pré-preenchido da seguinte forma (maio de 2019):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <?php
        // coloque seu código aqui
        ?>
    </body>
</html>

Alteramos o código acima da seguinte forma:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // tempo: número de milissegundos entre o momento atual e 01/01/1970
        // formato de exibição de data e hora
        // d: dia com 2 dígitos
        // m: mês com 2 dígitos
        // y: ano com 2 dígitos
        // H: hora 0,23
        // I: minutos
        // s: segundos
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

Comentários

  • linha 5: alteramos o título da página;
  • linha 17: exibe a data e a hora atuais;

Basicamente, o script PHP acima exibe a hora atual no console. No entanto, quando executado por um servidor web, o fluxo de saída da instrução [print] — que normalmente é direcionado para o console de execução do script — é redirecionado, neste caso, para a conexão que liga o servidor ao seu cliente. Portanto, em um contexto web, o script acima envia a hora atual na forma de texto para o cliente, neste caso, um navegador.

Vamos executar o script [exemple-01.php]:

Image

  • em [3], o URL solicitado ao servidor web Apache;
  • em [4], o título da página que alteramos;
  • em [5], o conteúdo gerado pela instrução [print];

Temos aqui uma página dinâmica, pois, se recarregarmos a página várias vezes no navegador (F5), seu conteúdo muda (a hora muda).

O navegador recebeu um fluxo HTML. Para conhecê-lo, é preciso exibir o código-fonte da página no navegador:

Image

  • para acessar o menu [1], clique com o botão direito do mouse na página no navegador;
  • em [2], o URL da página [exemple-01.php], precedido pelos prefixos [view-source :] e [3];
  • em [4], o conteúdo HTML que o navegador exibiu;

Portanto, é preciso lembrar que um script PHP destinado a ser executado por um servidor web deve gerar um fluxo HTML.

Vejamos agora (F12) os cabeçalhos HTTP enviados pelo servidor ao navegador do cliente:

Image

  • em [3], um cabeçalho HTTP que não estava presente quando solicitamos a página estática. Esse cabeçalho mostra que a resposta do servidor foi gerada por um script PHP;

Vimos que a resposta (o fluxo HTML neste caso) do servidor poderia ser gerada por um script PHP. O script também pode gerar os cabeçalhos HTTP e praticamente todos os elementos da resposta do servidor.

17.4. Noções básicas da linguagem HTML

Este capítulo não se aprofundará na programação em WEB. Uma aplicação web MVC é desenvolvida no parágrafo com o link. Este capítulo se concentra, ao contrário, nos serviços web: páginas PHP que fornecem, por meio de um servidor web, dados destinados a outros clientes PHP. No entanto, consideramos útil apresentar ao leitor alguns conceitos básicos de HTML.

Um navegador da web pode exibir diversos documentos, sendo o mais comum o documento HTML (HyperText Markup Language). Trata-se de um texto formatado com tags do tipo <balise>texte</balise>. Assim, o texto <b>important</b> exibirá o texto important em negrito. Existem tags isoladas, como a tag <hr/>, que exibe uma linha horizontal. Não abordaremos as tags que podem ser encontradas em um texto HTML. Existem diversos softwares WYSIWYG que permitem criar uma página WEB sem escrever uma única linha de código HTML. Essas ferramentas geram automaticamente o código HTML a partir de um layout criado com o mouse e controles predefinidos. Assim, é possível inserir (com o mouse) uma tabela na página e, em seguida, consultar o código HTML gerado pelo software para descobrir as tags a serem utilizadas para definir uma tabela em uma página WEB. Não é mais complicado do que isso. Além disso, o conhecimento da linguagem HTML é indispensável, uma vez que as aplicações web dinâmicas devem gerar elas mesmas o código HTML a ser enviado aos clientes WEB. Esse código é gerado por programa e, obviamente, é preciso saber o que deve ser gerado para que o cliente tenha a página da web que deseja.

Resumindo, não é necessário conhecer toda a linguagem HTML para começar a programar na web. No entanto, esse conhecimento é necessário e pode ser adquirido por meio do uso de softwares WYSIWYG para a criação de páginas WEB, como o DreamWeaver e dezenas de outros. Outra maneira de descobrir as sutilezas da linguagem HTML é navegar pela web e visualizar o código-fonte das páginas que apresentam características interessantes e ainda desconhecidas para você.

Consideremos o exemplo a seguir, que apresenta alguns elementos que podem ser encontrados em um documento WEB, tais como:

  • uma tabela;
  • uma imagem;
  • um link.

Image

Um documento HTML tem o seguinte formato geral:

<html> <head> <title>Um título</title> ... </head> <atributos do corpo> ... </body></html>

Todo o documento é delimitado pelas tags <html>…</html>. Ele é composto por duas partes:

  1. <head>…</head>: essa é a parte não exibível do documento. Ela fornece informações ao navegador que irá exibir o documento. Nela, costuma-se encontrar a tag <title>…</title>, que define o texto a ser exibido na barra de título do navegador. Também podem ser encontradas outras tags, notadamente aquelas que definem as palavras-chave do documento, palavras-chave posteriormente utilizadas pelos mecanismos de busca. Também é possível encontrar nessa parte scripts, geralmente escritos em JavaScript ou VBScript, que serão executados pelo navegador.
  1. <body atributos>…</body>: esta é a parte que será exibida pelo navegador. As tags HTML contidas nesta parte indicam ao navegador a forma visual “desejada” para o documento. Cada navegador interpretará essas tags à sua maneira. Dois navegadores podem, portanto, exibir um mesmo documento da web de maneiras diferentes. Isso costuma ser um dos desafios dos web designers.

O código HTML do nosso documento de exemplo é o seguinte:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Quelques balises HTML</title>
    </head>

    <body style="background-image: url(images/standard.jpg)">
        <h1 style="text-align: left">Quelques balises HTML</h1>
        <hr />

        <table border="1">
            <thead>
                <tr>
                    <th>Colonne 1</th>
                    <th>Colonne 2</th>
                    <th>Colonne 3</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>cellule(1,1)</td>
                    <td style="text-align: center;">cellule(1,2)</td>
                    <td>cellule(1,3)</td>
                </tr>
                <tr>
                    <td>cellule(2,1)</td>
                    <td>cellule(2,2)</td>
                    <td>cellule(2,3</td>
                </tr>
            </tbody>
        </table>
        <br/><br/>
        <table border="0">
            <tr>
                <td>Une image</td>
                <td>
                    <img border="0" src="images/cerisier.jpg"/></td>
            </tr>
            <tr>
                <td>Le site de Polytech'Angers</td>
                <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
            </tr>
        </table>
    </body>
</html>
Elément
tags e exemplos HTML
titre du document
<title>Algumas tags HTML</title> (linha 5)
o texto [Quelques balises HTML] aparecerá na barra de título do navegador que exibirá o documento
barre horizontale
<hr />: exibe uma linha horizontal (linha 10)
tableau
<table atributos>….</table>: para definir a tabela (linhas 12, 32)
<thead>…</thead>: para definir os cabeçalhos das colunas (linhas 13, 19)
<tbody>…</tbody>: para definir o conteúdo da tabela (linhas 20, 31)
<tr atributos>…</tr>: para definir uma linha (linhas 21, 25)
<td atributos>…</td>: para definir uma célula (linha 22)
exemplos:
<table border="1">…</table>: o atributo border define a espessura da borda da tabela
<td style="text-align: center;">célula(1,2)</td> (linha 23): define uma célula cujo conteúdo será célula(1,2). Esse conteúdo será centralizado horizontalmente (text-align: center).
image
<img border="0" src="images/cerisier.jpg"/> (linha 38): define uma imagem sem borda (border="0") cujo arquivo de origem é [images/cerisier.jpg] no servidor web (src="images/cerisier.jpg"). Esse link está em um documento da web gerado com o URL http://localhost/php7/scripts-web/01/balises.html. Assim, o navegador solicitará o URL http://localhost/php7/scripts-web/01/images/cerisier.jpg para obter a imagem referenciada aqui.
lien
<a href="http://www.polytech-angers.fr/fr/index.html">aqui</a> (linha 42): faz com que o texto ici funcione como um link para o URL http://www.polytech-angers.fr/fr/index.html.
fond de page
<body style="background-image: url(images/standard.jpg)"> (linha 8): indica que a imagem que deve servir como plano de fundo da página está localizada no endereço URL [images/standard.jpg] do servidor WEB. No contexto do nosso exemplo, o navegador solicitará o endereço http://localhost/php7/scripts-web/01/images/standard.jpg para obter essa imagem de fundo.

Vemos neste exemplo simples que, para construir o documento na íntegra, o navegador precisa fazer três solicitações ao servidor:

  1. http://localhost/php7/scripts-web/01/images/balises.html para obter o código-fonte HTML do documento
  2. http://localhost/php7/scripts-web/01/images/cerisier.jpg para obter a imagem cerisier.jpg
  3. http://localhost/php7/scripts-web/01/images/standard.jpg para obter a imagem de fundo standard.jpg

É o que mostram as trocas de dados entre o cliente e o servidor (F12 no navegador):

Image

  • em [3-5], vemos as três solicitações feitas pelo navegador;

17.5. Tornando uma página estática dinâmica

Vamos mostrar como podemos tornar dinâmica a página HTML [exemple-01.html]. Copiemos o conteúdo

Image

Copiamos o conteúdo de [exemple-01.html] para o arquivo [page-01.php]. Se executarmos o script da web [2], obtemos o seguinte resultado no navegador:

Image

  • em [3], o URL solicitado;
  • em [4], o título da página;
  • em [5], o conteúdo da página;

Se exibirmos o código recebido pelo navegador, encontraremos o seguinte:

Image

  • em [7], temos o código HTML inserido no script [exemple-01.php]

O interpretador PHP interpretou o script [page-01.php] e gerou o mesmo fluxo HTML que a página estática [exemple-01.html]. No script [page-01.php], não havia PHP, apenas HTML. Com isso, aprendemos uma coisa: quando o interpretador PHP encontra HTML em um script PHP, ele não altera nada e o envia tal como está para o cliente.

Agora, vamos inserir algumas instruções PHP no script [page-01.php] para que o interpretador PHP tenha algo para fazer:


<!DOCTYPE html>
<html>
    <head>
        <title><?php print $page->title ?></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b><?php print $page->contents ?></b></div>
    </body>
</html>

Nas linhas 4 e 9, inserimos o código PHP para gerar dinamicamente o título e o conteúdo da página. Partimos aqui da hipótese de que a variável [$page] é um objeto que contém os dados a serem exibidos.

Se executarmos esse novo código, obtemos o seguinte resultado no navegador:

Image

  • em [1], o URL solicitado;
  • em [2], o título da página não pôde ser exibido porque a variável [$page] não estava definida;
  • em [3], o mesmo ocorreu com o conteúdo;

Agora, vamos escrever o seguinte script da web [exemple-02.php]:

Image

O script [exemple-02.php] será o seguinte:


<?php

// definimos os elementos da página a serem exibidos
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// exibe-se [page-01]
require_once "page-01.php";
  • linhas 4-6: definimos o objeto [$page];
  • linha 8: inclui-se o script [page-01.php]. O código desse script será interpretado por sua vez:
    • a variável [$page] agora está definida e o interpretador PHP irá utilizá-la;
    • o código HTML de [page-01.php] será enviado tal como está para o cliente;
    • os resultados das operações PHP e [print] serão incluídos no fluxo de texto enviado ao cliente;

Agora, se executarmos o script da web [exemple-02.php], obteremos o seguinte no navegador:

Image

Se visualizarmos o conteúdo de texto recebido pelo navegador:

Image

  • os códigos PHP, que eram [2] e [3], foram substituídos pelos resultados dos dois comandos [print];

Desse exemplo, podemos destacar duas coisas:

  • as páginas HTML destinadas ao navegador podem ser isoladas em scripts PHP que contenham apenas esse código HTML e algumas partes dinâmicas geradas pelo código PHP. Deve haver o mínimo possível de PHP nessas páginas;
  • toda a lógica que gera os dados dinâmicos incluídos nas páginas HTML deve ser isolada em scripts PHP puros, sem nenhum código de apresentação das páginas (HTML, CSS, JavaScript…);

Isso permite uma separação de tarefas:

  • a tarefa de criação das páginas da web a serem exibidas (HTML, CSS, JavaScript…);
  • a tarefa da lógica da aplicação web que estamos construindo. Essa lógica poderá ser implementada com uma arquitetura de três camadas, exatamente como fizemos com os scripts de console;

Posteriormente, vamos criar scripts da web específicos;

  • eles enviarão apenas dados ao cliente e nenhuma formatação (HTML, CSS, Javascript). Serão, portanto, servidores de dados, e não páginas da web;
  • os clientes desses scripts da web serão scripts de console que se encarregarão de recuperar os dados enviados pelo servidor e processá-los;

17.6. Aplicação cliente/servidor de data/hora

Agora, estamos na seguinte configuração:

Image

Vamos escrever:

  • um script web [1] que envia ao seu cliente a data e a hora do momento atual;
  • um script de console [2] que atuará como cliente do script web: ele irá recuperar a data e a hora enviadas pelo script web e exibi-las no console;

Image

  • no [1], o script da web [date-time-server.php];
  • em [2], o script de console [date-time-client], cliente do script da web;

17.6.1. O script do servidor

Já escrevemos um script web que gera a data e a hora do momento atual, conforme descrito no parágrafo com o link. Era o seguinte script [exemple-01.php]:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time: número de milissegundos desde 01/01/1970
        // formato de exibição de data e hora
        // d: dia com 2 dígitos
        // m: mês com 2 dígitos
        // y: ano com 2 dígitos
        // H: hora 0,23
        // i: minutos
        // s: segundos
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

Dissemos que iríamos criar servidores de dados: dados brutos sem formatação HTML. O script de servidor [date-time-server.php] será, então, o seguinte:


<?php

// define-se o cabeçalho HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// envia-se a data e a hora
// tempo: número de milissegundos desde 01/01/1970
// formato de exibição da data e hora
// d: dia com 2 dígitos
// m: mês com 2 dígitos
// y: ano com 2 dígitos
// H: hora 0,23
// i: minutos
// s: segundos
print date("d/m/y H:i:s", time());
  • linha 4: definimos o cabeçalho HTTP [Content-Type], que informa ao cliente a natureza do documento que ele receberá. Até agora, o [Content-Type] era: [Content-Type: text/html; charset=UTF-8]. Aqui, informamos ao cliente que o documento é um texto sem formatação: HTML. Isso não é importante para o nosso cliente de console, que não utilizará esse cabeçalho. É mais importante para os navegadores dos clientes, que, por sua vez, utilizam esse cabeçalho;

Vamos executar este script de servidor:

Image

Se examinarmos no navegador a resposta do servidor (F12), vemos em [5] o cabeçalho HTTP que o script do servidor definiu e, em [8], o documento de texto recebido;

Image

17.6.2. O script do cliente

No parágrafo sobre links, desenvolvemos vários clientes HTTP. Poderíamos usá-los para recuperar o documento de texto enviado pelo script do servidor [date-time-server.php]. Mas não faremos isso. Assim como fizemos para os protocolos SMTP e IMAP, vamos utilizar uma biblioteca de terceiros, a saber, o componente [HttpClient] do framework Symfony [https://symfony.com/doc/master/components/http_client.html].

Assim como nas duas bibliotecas anteriores, usamos a ferramenta [Composer] para instalar o componente [HttpClient] do Symfony. Em uma janela do Laragon (ver parágrafo com o link), digitamos o seguinte comando:

Image

  • no [3], verifique se você está na pasta [<laragon>/www/], onde <laragon> é a pasta de instalação do Laragon;
  • em [4], o comando [composer], que instala a biblioteca [HttpClient] do Symfony;
  • em [5], nada é instalado, pois a biblioteca [HttpClient] já havia sido instalada neste computador;
  • no [6-7], novas pastas aparecem no [<laragon>/www/vendor/symfony];

No lugar de [5], você deve ter algo semelhante ao seguinte:


C:\myprograms\laragon-lite\www
? composer require symfony/http-client
Using version ^4.3 for symfony/http-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-php73 (v1.11.0): Downloading (100%)
  - Installing symfony/http-client-contracts (v1.1.1): Downloading (100%)
  - Installing psr/log (1.1.0): Loading from cache
  - Installing symfony/http-client (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

Certifique-se de que a pasta [<laragon>/www/vendor] faça parte do ramo [Include Path] do seu projeto (veja o parágrafo com o link):

Image

Feito isso, podemos escrever o script de console [date-time-client.php]:

Image

O script de console [date-time-client.php] utilizará o seguinte arquivo jSON [config-date-time-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server.php"
}
  • linha 2: o URL do script do servidor;

O script do cliente [date-time-client.php] será o seguinte:


<?php

// cliente do serviço de data/hora
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-date-time-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();

try {
  // enviando a solicitação
  $response = $httpClient->request('GET', $config['url']);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperação dos cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // recupera-se o corpo da resposta
  $content = $response->getContent();
  // exibindo
  print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
  // é exibido o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
  exit;
}

Comentários

  • linha 10: assim como fizemos nas bibliotecas anteriores, carregamos o arquivo [<laragon>/www/vendor/autoload.php];
  • linha 11: declaramos a classe [HttpClient] que vamos utilizar;
  • linhas 13-24: recuperamos a configuração do script no dicionário [$config];
  • linha 27: criamos um objeto do tipo [HttpClient];
  • linha 31: solicitamos o URL do script do servidor por meio de um comando GET: [GET URL HTTTP/1.1]. Essa operação é assíncrona. A execução continua na linha 33 sem aguardar o recebimento da resposta;
  • linha 33: solicita-se o status da resposta. Esse status está no primeiro cabeçalho HTTP retornado pelo servidor. Assim, se esse cabeçalho for [HTTP/1.1 200 OK], o status da resposta é 200. Essa operação é bloqueante: só se retorna quando o cliente tiver recebido toda a resposta do servidor;
  • linha 37: solicitam-se os cabeçalhos HTTP da resposta;
  • linha 42: solicita-se o documento enviado pelo servidor: sabe-se que, neste caso, esse documento é um texto.
  • linhas 45-49: em caso de erro, exibe-se a mensagem de erro;

Ao executar o script do cliente (é necessário que o Laragon esteja em execução para que o script do servidor possa ser acessado), obtém-se o seguinte resultado no console:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Thu, 30 May 2019 14:42:03 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
content-length: 17
content-type: text/plain; charset=UTF-8
---Réponse du serveur : [30/05/19 14:42:03]

A data e a hora do momento atual são recuperadas corretamente na linha 8.

Talvez surja a curiosidade de saber o que o script do cliente enviou ao servidor. Para isso, vamos usar nosso servidor genérico TCP (ver parágrafo com o link):

Image

  • no [1], a pasta de utilitários;
  • em [2], o servidor TCP é iniciado na porta 100;
  • em [3], aguarda um comando digitado no teclado;

Alteramos o arquivo de configuração do script [date-time-client.php]:


{
    "url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}

Desta vez, o cliente entra em contato com o servidor [localhost] na porta 100. Portanto, será nosso servidor genérico TCP que será solicitado. Quando executamos o script de console [date-time-client.php], o console do servidor genérico TCP apresenta as seguintes alterações:

Image

  • em [3], o comando HTTP GET criado pelo script do cliente;
  • em [4], a assinatura do script de console;
  • em [5], a resposta do servidor ao script do cliente. Observe-se que esta não é uma resposta HTTP válida:
    • deveria haver cabeçalhos HTTP;
    • em seguida, uma linha em branco;
    • em seguida, o documento de texto enviado ao cliente;
  • em [6], encerra-se a comunicação com o script do cliente para que este detecte que recebeu a resposta completa;

No lado do script do cliente, temos a seguinte exibição no console:

Image

  • no [7], o que o cliente Symfony recebeu;

17.6.3. O script do servidor – versão 2

Por padrão, as funções PHP para escrever um script web não são orientadas a objetos. No lado do servidor, somos levados a misturar classes e funções PHP clássicas. Para obter uma sintaxe mais homogênea, utilizaremos a biblioteca [HttpFoundation] do framework Symfony. Ela encapsulou todas as funções PHP clássicas para um serviço web em um sistema de classes e interfaces. A documentação da biblioteca está disponível no URL [https://symfony.com/doc/current/components/http_foundation.html] (maio de 2019).

Para instalar a biblioteca, procedemos da seguinte maneira em um terminal Laragon (ver parágrafo com o link):

Image

  • [2-3]: certifique-se de estar na pasta [<laragon>/www];
  • [4]: o comando [composer], que instalará a biblioteca [HttpFoundation];
  • [5]: neste exemplo, a biblioteca já estava instalada;

Na primeira instalação, você deverá ter registros de console semelhantes a estes:


C:\myprograms\laragon-lite\www
? composer require symfony/http-foundation
Using version ^4.3 for symfony/http-foundation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/mime (v4.3.0): Downloading (100%)
  - Installing symfony/http-foundation (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

A segunda versão do servidor web [date-time-server-2.php] é a seguinte:


<?php

// uso das bibliotecas do Symfony

// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;

// definimos o cabeçalho Content-Type
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");

// definimos o conteúdo da resposta
//
// envio da data e da hora
// time: número de milissegundos desde 01/01/1970
// formato de exibição da data e hora
// d: dia com 2 dígitos
// m: mês com 2 dígitos
// y: ano com 2 dígitos
// H: hora 0,23
// i: minutos
// s: segundos
$response->setContent(date("d/m/y H:i:s", time()));

// envia-se a resposta
$response->send();

Comentários

  • linha 7: a classe [Response] da biblioteca [HttpFoundation] do Symfony gerencia toda a resposta aos clientes do serviço web;
  • linha 10: criação de uma instância da classe [Response];
  • linha 11: indica-se que a resposta é do tipo [text/plain];
  • linha 12: a resposta é o texto UTF-8;
  • linha 25: define-se o documento da resposta, conforme solicitado pelo cliente;
  • linha 28: envia-se a resposta ao cliente;

17.6.4. O script do cliente – versão 2

O script do cliente não sofre alterações. Altera-se apenas seu arquivo de configuração [config-date-time-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server-2.php"
}

Os resultados são os mesmos da versão 1.

17.7. Um servidor de dados jSON

A resposta de um script da web pode ser composta por vários dados que podem ser agrupados em tabelas e objetos. O script pode, então, enviar esses diversos elementos dentro de uma string jSON que o cliente irá decodificar.

Image

17.7.1. O script do servidor

O script [json-server.php] utiliza a seguinte classe [Personne]:


<?php

namespace Modèles;

class Personne implements \JsonSerializable {
  // atributos
  private $nom;
  private $prénom;
  private $âge;

  // conversão de uma tabela associativa para um objeto [Personne]
  public function setFromArray(array $assoc): Personne {
    // inicializa-se o objeto atual com o array associativo
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // resultado
    return $this;
  }

  // getters e setters
  public function getNom() {
    return $this->nom;
  }

  public function getPrénom() {
    return $this->prénom;
  }

  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }

  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }

  public function getÂge() {
    return $this->âge;
  }

  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }

  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }

  // implementa a interface JsonSerializable
  public function jsonSerialize(): array {
    // retorna um array associativo cujas chaves são os atributos do objeto
    // esse array poderá então ser codificado em jSON
    return get_object_vars($this);
  }

  // conversão de um jSON para um objeto [Personne]
  public static function jsonUnserialize(string $json): Personne {
    // cria-se uma pessoa a partir da sequência jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }

}

Comentários

  • linha 5: a classe implementa a interface PHP [JsonSerializable]. Isso a obriga a implementar o método [jsonSerialize] das linhas 55 a 59. O método deve retornar um array associativo que deverá ser serializado em jSON. Ao utilizar a expressão [json_encode($personne)], a função [json_encode] verifica se a classe [Personne] implementa a interface [JsonSerializable]. Se sim, a expressão passa a ser [json_encode($personne→serialize())];
  • linhas 12-19: a classe não possui um construtor, mas sim um inicializador. A classe [Personne] pode, então, ser instanciada pela expressão [(new Personne())→setFromArray($array)]. É possível ter diversos tipos de inicializadores, enquanto só é possível ter um construtor. Esses inicializadores permitem diversos modos de instanciação do tipo [(new Personne())→initialiseuri(…)];
  • linhas 62-65: a função estática [jsonUnserialize] permite criar um objeto [Personne] a partir de sua string jSON;

O script [json-server.php] será o seguinte:


<?php

// dependências
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;

// define-se o cabeçalho Content-Type e a biblioteca de caracteres utilizada
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// cria-se um objeto Pessoa
$personne = (new Personne())->setFromArray([
  "nom" => "de la Hûche",
  "prénom" => "jean-paul",
  "âge" => 27]);
// um array associativo
$assoc = ["attr1" => "value1",
  "attr2" => [
    "prenom" => "Jean-Paul",
    "nom" => "de la Hûche"
  ]
];
// o conteúdo da resposta é do tipo jSON
$response->setContent(json_encode([$personne, $assoc]));

// envio da resposta
$response->send();

Comentários

  • linhas 4-5: importa-se a classe [Personne];
  • linha 11: indica-se que o documento será do tipo [application/json]. Ao receber esse cabeçalho, os navegadores exibirão a formatação da sequência jSON em vez de exibir texto bruto;
  • linha 12: a sequência jSON conterá os caracteres UTF-8;
  • linhas 15-18: cria-se um objeto [Personne];
  • linhas 20-25: cria-se um array associativo de dois níveis;
  • linha 27: envia-se ao cliente a string jSON de um array:
    • o elemento [$personne] será serializado como jSON por meio de seu método [jsonSerialize];
    • o elemento [$assoc] será serializado nativamente como jSON;

Se executarmos esse script no servidor (o Laragon deve estar em execução), obtemos a seguinte resposta no navegador:

Image

Image

Comentários

  • em [2], a resposta jSON formatada;
  • em [4], a resposta jSON sem formatação. Observe a codificação dos caracteres acentuados;
  • em [6], foi o tipo de conteúdo [application/json] enviado pelo servidor que levou o navegador a fazer essa formatação;

17.7.2. O cliente

Image

O cliente [json-client.php] é configurado pelo seguinte arquivo jSON [config-json-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/03/json-server.php"
}

O script [json-client.php] é o seguinte:


<?php

// cliente de um serviço jSON
//
// gestão de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-json-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();

try {
  // envia-se a solicitação
  $response = $httpClient->request('GET', $config['url']);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperando os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // recupera-se o corpo jSON da resposta
  list($personne, $assoc) = json_decode($response->getContent(), true);
  // instancia-se uma pessoa a partir da tabela de seus atributos
  $personne = (new Personne())->setFromArray($personne);
  // exibe-se a resposta do servidor
  print "---Réponse du serveur\n";
  print "$personne\n";
  print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
  // exibe-se o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}


Comentários

  • linhas 12-13: importação da classe [Personne];
  • linha 30: criação do cliente HTTP;
  • linha 44: decodifica-se a string jSON enviada pelo servidor. Sabe-se que o que foi codificado é um array de dois elementos, composto por dois tabuletos associativos;
  • linha 46: cria-se um objeto [Personne] para exibi-lo posteriormente na linha 49;
  • linha 50: exibe-se o segundo array associativo. A instrução [print] não sabe exibir arrays. Por isso, transforma-se esse array na string jSON. Para que os caracteres acentuados sejam exibidos corretamente, é preciso definir o segundo parâmetro como [JSON_UNESCAPED_UNICODE]. Vimos que, de fato, os caracteres acentuados estão codificados na sequência jSON;

A execução do script do cliente produz os seguintes resultados:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 09:56:29 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 143
connection: close
content-type: application/json
---Réponse du serveur
Personne [jean-paul, de la Hûche, 27]
tableau={"attr1":"value1","attr2":{"prenom":"Jean-Paul","nom":"de la Hûche"}}

Nas linhas 11 e 12, os caracteres acentuados foram recuperados corretamente.

17.8. Recuperação das variáveis de ambiente do serviço web

Um script de servidor é executado em um ambiente web que ele pode reconhecer. Esse ambiente está armazenado no dicionário $_SERVER, uma variável global de PHP. Se utilizarmos a biblioteca [HttpFoundation], esse ambiente será encontrado no campo [Request→server], onde [Request] é a solicitação HTTP processada pelo script da web.

17.8.1. O script do servidor

Estamos escrevendo um aplicativo de servidor que envia seu ambiente de execução aos clientes.

Image

O script da web [env-server.php] é o seguinte:


<?php

// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

// recupera-se a solicitação
$request = Request::createFromGlobals();
// elabora-se a resposta
$response = new Response();
// o conteúdo da resposta é JSON UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// define-se o conteúdo jSON da resposta
$response->setContent(json_encode($request->server->all()));
// envio da resposta
$response->send();
  • linha 9: recuperamos o objeto do tipo [Request], que encapsula todas as informações disponíveis sobre a solicitação HTTP recebida pelo script da web, bem como sobre o ambiente de execução deste;
  • linhas 13-14: enviaremos texto simples com caracteres UTF-8 ao cliente;
  • linha 16: a informação enviada ao cliente será uma cadeia de caracteres obtida por serialização jSON do objeto [$request→server→all()]: [$request→server] representa o ambiente de execução do script da web. Trata-se de um objeto do tipo [ServerBag], uma espécie de dicionário. [$request→server→all()], por sua vez, é um dicionário propriamente dito, o do conteúdo do [ServerBag];
  • linha 18: enviamos a informação;

Se executarmos esse script a partir do NetBeans, o navegador exibe a seguinte página:

Image

  • em [2], as diferentes chaves do dicionário do ambiente;
  • em [3], os valores dessas chaves;

17.8.2. O script do cliente

Image

O script do cliente [env-client.php] é configurado pelo seguinte arquivo jSON [config-env-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/04/env-server.php"
}

O script do cliente [env-client.php] é o seguinte:


<?php

// ambiente de um script de servidor
//
// gestão de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-env-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();
try {
  // envia-se a solicitação ao servidor
  $response = $httpClient->request('GET', $config['url']);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperando os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibe-se a resposta do servidor
  print "---Réponse du serveur\n";
  $env = json_decode($response->getContent());
  foreach ($env as $key => $value) {
    print "[$key]=>$value\n";
  }
} catch (TypeError | RuntimeException $ex) {
  // exibe-se o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linha 42: deserializa-se a resposta jSON do servidor. Obtém-se um array associativo;
  • linhas 43-45: exibimos todos os valores desse array associativo;

Obtém-se o seguinte resultado no console:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 17:35:50 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 1505
connection: close
content-type: application/json
---Réponse du serveur
[HTTP_HOST]=>localhost
[HTTP_USER_AGENT]=>Symfony HttpClient/Curl
[HTTP_ACCEPT_ENCODING]=>deflate, gzip
[PATH]=>C:\Program Files (x86)\Mail Enable\BIN;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files (x86)\Mail Enable\BIN64;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\myprograms\Microsoft VS Code\bin
[SystemRoot]=>C:\windows
[COMSPEC]=>C:\windows\system32\cmd.exe
[PATHEXT]=>.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
[WINDIR]=>C:\windows
[SERVER_SIGNATURE]=>
[SERVER_SOFTWARE]=>Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
[SERVER_NAME]=>localhost
[SERVER_ADDR]=>::1
[SERVER_PORT]=>80
[REMOTE_ADDR]=>::1
[DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[REQUEST_SCHEME]=>http
[CONTEXT_PREFIX]=>
[CONTEXT_DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[SERVER_ADMIN]=>admin@example.com
[SCRIPT_FILENAME]=>C:/myprograms/laragon-lite/www/php7/scripts-web/04/env-server.php
[REMOTE_PORT]=>63744
[GATEWAY_INTERFACE]=>CGI/1.1
[SERVER_PROTOCOL]=>HTTP/1.1
[REQUEST_METHOD]=>GET
[QUERY_STRING]=>
[REQUEST_URI]=>/php7/scripts-web/04/env-server.php
[SCRIPT_NAME]=>/php7/scripts-web/04/env-server.php
[PHP_SELF]=>/php7/scripts-web/04/env-server.php
[REQUEST_TIME_FLOAT]=>1559496950.644
[REQUEST_TIME]=>1559496950

Aqui está o significado de algumas das variáveis (para Windows. No Linux, elas seriam diferentes):

HTTP_HOST
o valor xxx do cabeçalho HTTP [Host: xxx] enviado pelo cliente
HTTP_USER_AGENT
o valor xxx do cabeçalho HTTP [User_Agent: xxx] enviado pelo cliente
HTTP_ACCEPT_ENCODING
o valor xxx do cabeçalho HTTP [Accept-Encoding: xxx] enviado pelo cliente
PATH
o caminho dos executáveis na máquina na qual o script do servidor está sendo executado
COMSPEC
o caminho do interpretador de comandos DOS
PATHEXT
as extensões dos arquivos executáveis
WINDIR
a pasta de instalação do Windows
SERVER_SIGNATURE
a assinatura do servidor web. Aqui não há nada.
SERVER_SOFTWARE
o tipo do servidor web
SERVER_NAME
o nome de Internet da máquina do servidor web
SERVER_PORT
a porta de escuta do servidor web
SERVER_ADDR
o endereço IP do servidor web, neste caso 127:0:0:1
REMOTE_ADDR
o endereço IP do cliente. Neste caso, o cliente estava na mesma máquina que o servidor.
REMOTE_PORT
a porta de comunicação do cliente
DOCUMENT_ROOT
a raiz da árvore de documentos servidos pelo servidor web
REQUEST_SCHEME
o protocolo TCP da solicitação de URL http://localhost/php7/…
SERVER_ADMIN
o endereço de e-mail do administrador do servidor web
SCRIPT_FILENAME
o caminho completo do script do servidor
REMOTE_PORT
a porta a partir da qual o cliente fez sua solicitação
SERVER_PROTOCOL
a versão do protocolo HTTP utilizada pelo servidor web
REQUEST_METHOD
a ordem HTTP utilizada pelo cliente. Existem quatro: GET, POST, PUT, DELETE
QUERY_STRING
os parâmetros enviados com uma ordem GET /url?parâmetros
REQUEST_URI
o URL solicitado pelo cliente. Se o navegador solicitar o URL http://machine[:port]/uri, teremos REQUEST_URI=uri
SCRIPT_NAME
$_SERVER['SCRIPT_FILENAME']=$_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME']

17.9. Recuperação pelo servidor de parâmetros enviados por um cliente

17.9.1. Introdução

No protocolo HTTP, um cliente dispõe de dois métodos para passar parâmetros ao servidor WEB:

  • ele solicita o serviço URL na forma

GET url?param1=val1&param2=val2&param3=val3… HTTP/1.0

onde os valores vali devem ser previamente codificados para que certos caracteres reservados sejam substituídos por seus valores hexadecimais;

  • ele solicita o URL do serviço na forma

POST url HTTP/1.0

e, em seguida, entre os cabeçalhos HTTP enviados ao servidor, insere o seguinte cabeçalho:


Content-length=N

O restante dos cabeçalhos enviados pelo cliente termina com uma linha vazia. Ele pode então enviar seus dados na forma


val1&param2=val2&param3=val3…

onde os valores vali devem, assim como no método GET, ser previamente codificados. O número de caracteres enviados ao servidor deve ser N, sendo que N é o valor declarado no cabeçalho


Content-length=N

O script PHP do serviço web, que recupera os parâmetros parami anteriores enviados pelo cliente, obtém seus valores na tabela:

  • $_GET["parami"] para um pedido GET;
  • $_POST["parami"] para um comando POST;

isso se refere às funções básicas de PHP. Se for utilizada a biblioteca [HttpFoundation], esses parâmetros serão encontrados em:

  • [Request]->query->get(‘parami’) para um comando GET;
  • [Request]->request->get(‘parami’) para um comando POST;

onde [Request] representa todas as informações sobre a solicitação recebida pelo script da web;

17.9.2. O cliente GET – versão 1

Image

Os scripts de cliente são configurados pelo seguinte arquivo jSON [config-parameters-client.json]:

1
2
3
4
{
    "url-get": "http://localhost/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • linha 1: o URL do script da web de destino dos clientes GET;
  • linha 2: o URL do script da web de destino do cliente POST;

Os clientes GET enviam três parâmetros [nom, prenom, age] ao servidor. O cliente [parameters-get-client.php] é o seguinte:


<?php

// cliente GET de um servidor web
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-parameters-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// criando um cliente HTTP
$httpClient = HttpClient::create();

try {
  // prepara-se os parâmetros
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// codificação das informações
  $parameters = "prenom=" . urlencode($prenom) .
    "&nom=" . urlencode($nom) .
    "&age=$age”;
  // envia-se a solicitação
  $response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperando os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibindo a resposta do servidor
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // é exibido o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linhas 33-35: codificação dos parâmetros enviados ao servidor. Os parâmetros [$prenom, $nom], que podem conter caracteres UTF-8, são codificados com a função [urlencode]. Todos os caracteres não alfanuméricos (no sentido das expressões relacionais) são substituídos por %xx, onde xx é o valor hexadecimal do caractere. Os espaços, por sua vez, são substituídos pelo sinal +;
  • linha 37: o URL solicitado é $URL?$parameters, onde $parameters tem a forma nom=val1&prenom=val2&age=val3;
  • linha 48: o cliente se limitará a exibir a resposta do servidor;

Podemos ter curiosidade em ver o que o servidor recebe durante uma solicitação GET configurada. Para isso, iniciamos nosso servidor genérico [RawTcpServer] na porta 100 da máquina local a partir de um terminal Laragon (ver parágrafo com o link):

Image

Verifique se, no [4], você está realmente na pasta de utilitários.

Alteramos o arquivo jSON [parameters-get-client.json], que configura os clientes GET e POST:


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • linha 2: alteramos a porta do servidor web. Assim, será o [RawTcpServer] que será contatado;

Executamos o cliente. Na janela do [RawTcpServer], obtemos as seguintes informações:

Image

  • no [1], o comando GET configurado e enviado pelo cliente. É possível ver claramente a codificação de alguns caracteres;

17.9.3. O servidor GET / POST

Image

O script do servidor [parameters-server.php] é o seguinte:


<?php

// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

// recupera-se a consulta
$request = Request::createFromGlobals();
// recuperam-se os parâmetros da solicitação
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();

// elabora-se a resposta
$response = new Response();
// o conteúdo da resposta é texto UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// conteúdo da resposta — uma tabela codificada em jSON
$response->setContent(json_encode([
  "method" => $request->getMethod(),
  "uri" => $request->getRequestUri(),
  "getParameters" => $getParameters,
  "bodyParameters" => $bodyParameters
    ], JSON_UNESCAPED_UNICODE));
// envio da resposta
$response->send();

Comentários

  • linha 9: criação do objeto [Request] do script da web. Esse objeto encapsula todas as informações que o script da web recebeu do cliente;
  • linha 11: o objeto [Request→query] é do tipo [ParameterBag] e reúne os parâmetros de uma eventual operação GET de um cliente. A expressão [Request→query→get(«X»)] permite obter o parâmetro denominado X entre os parâmetros do GET [nom=val1&prenom=val2&age=val3]. A expressão [Request→query→all()] permite obter o dicionário de parâmetros da operação GET;
  • linha 12: o objeto [Request→request] é do tipo [ParameterBag] e reúne os parâmetros enviados como documento do cliente para o servidor. Também se diz que esses parâmetros são “enviados” porque pertencem a um documento que o cliente envia ao servidor. A expressão [Request→request→get(«X»)] permite obter o parâmetro denominado X entre os parâmetros enviados como upload [nom=val1&prenom=val2&age=val3]. A expressão [Request→request→all()] permite obter o dicionário dos parâmetros enviados como upload;
  • linhas 17-18: informa-se ao cliente que será enviado a ele o jSON codificado em UTF-8;
  • linhas 20-25: o servidor retorna ao cliente todos os parâmetros que recebeu, bem como o tipo de operação [GET / POST / …] realizada pelo cliente e o URI solicitado. Esse método é obtido pela expressão [$request→getMethod()]. O documento enviado ao cliente é a sequência jSON de uma tabela associativa, cujos valores, por sua vez, são tabelas associativas. O parâmetro [JSON_UNESCAPED_UNICODE] determina que os caracteres Unicode (como os caracteres acentuados, por exemplo) sejam enviados tal como estão, sem codificação;
  • linha 27: a resposta é enviada ao cliente;

A execução do script do cliente produz os seguintes resultados:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 10:08:45 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 207
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom=jean-paul&nom=de+la+h%C3%BBche&age=45","getParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"},"bodyParameters":[]}]
  • linha 10:
    • [method]: o método é GET;
    • [uri]: observam-se os parâmetros codificados por URL da solicitação GET na solicitação URI;
    • [getParameters]: a tabela de parâmetros do GET;
    • [bodyParameters]: a tabela de parâmetros enviados: ela está vazia;

17.9.4. O cliente GET – versão 2

Na versão anterior do script cliente, nós mesmos codificamos por URL os parâmetros enviados ao servidor, com fins didáticos. O objeto [HttpClient] é capaz de realizar essa tarefa sozinho. Trata-se do seguinte script [parameters-get-client-2.php]:


<?php

// cliente GET de um servidor web
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-parameters-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// criando um cliente HTTP
$httpClient = HttpClient::create();
try {
  // preparando os parâmetros
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // envia-se a solicitação ao servidor
  $response = $httpClient->request('GET', $config['url-get'],
    ["query" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperam-se os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibindo a resposta do servidor
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // exibe o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linhas 33-37: adição de parâmetros à solicitação GET da linha 32. O objeto [HttpClient] se encarregará sozinho da codificação do URL;

17.9.5. O cliente POST

Um cliente HTTP envia ao servidor web a seguinte sequência de texto: cabeçalhos HTTP, linha vazia, documento. No cliente anterior, essa sequência era a seguinte:

1
2
3
GET /url?paramètres HTTP/1.1
… autres entêtes HTTP
ligne vide

Não havia documento. Existe outra maneira de transmitir parâmetros, o método denominado POST. Nesse caso, a sequência de texto enviada ao servidor web é a seguinte:

1
2
3
4
POST /url HTTP/1.1
… autres entêtes HTTP
ligne vide
paramètres

Desta vez, os parâmetros que, no cliente GET, estavam incluídos nos cabeçalhos HTTP, fazem parte, no cliente POST, do documento enviado após os cabeçalhos.

O script do cliente POST [parameters-postclient.php] é o seguinte:


<?php

// cliente POST de um servidor web
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-parameters-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();
try {
  // prepara-se os parâmetros
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // envia-se a solicitação ao servidor
  $response = $httpClient->request('POST', $config['url-post'],
    ["body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperam-se os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibindo a resposta do servidor
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // exibe o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
  • linha 32: agora temos uma solicitação HTTP do tipo POST;
  • linhas 33-37: os parâmetros de POST são chamados de corpo (body) da solicitação POST: trata-se do documento enviado pelo cliente ao servidor. Aqui, três parâmetros são enviados: [nom, prenom, age];
  • linha 48: exibe-se a resposta jSON do servidor;

Os resultados da execução do script do cliente são os seguintes:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 11:43:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 163
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • linha 10: o método é [Post] e os parâmetros são do tipo [bodyParameters]. Não há parâmetros [getParameters], conforme mostra o [uri];

Podemos ficar curiosos para saber o que o servidor recebe durante uma solicitação POST. Para isso, iniciamos nosso servidor genérico [RawTcpServer] na porta 100 da máquina local a partir de um terminal Laragon (ver parágrafo com o link):

Image

Verifique se, no [4], você está realmente na pasta de utilitários.

Alteramos o arquivo jSON [config-parameters-client.json], que configura o cliente POST:


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost:100/php7/scripts-web/05/parameters-server.php"
}
  • linha 3: alteramos a porta do servidor web. Assim, será o [RawTcpServer] que será contatado;

Executamos o cliente. Na janela do [RawTcpServer], obtemos as seguintes informações:

Image

  • em [6], o comando POST;
  • em [7]: o cabeçalho HTTP [Content-Length] indica o número de bytes do documento que o cliente enviará ao servidor. O cabeçalho HTTP [Content-Type] indica a natureza desse documento. O tipo [application/x-www-form-urlencoded] designa um texto codificado por URL;
  • em [8], a linha vazia que anuncia o fim dos cabeçalhos HTTP e o início do documento de 44 bytes. O que a captura de tela não mostra é o próprio documento. Trata-se da sequência codificada por URL dos parâmetros: [prenom=jean-paul&nom=de+la+h%C3%BBche&age=45]. O leitor poderá verificar se ela realmente tem 44 caracteres;

17.9.6. Um cliente POST misto

Em um POST, é possível combinar os parâmetros codificados no URL com aqueles codificados no documento enviado pelo cliente após os cabeçalhos HTTP. Aqui está um exemplo [parameters-mixte-postclient.php]:


<?php

// cliente POST de um servidor web
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-parameters-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();
try {
  // prepara-se os parâmetros
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // envia-se a solicitação ao servidor
  $response = $httpClient->request('POST', $config['url-post'],
    [
      // parâmetros do documento (corpo)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parâmetros da URL (consulta)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperam-se os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibe-se a resposta do servidor
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // exibindo o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linha 32: uma solicitação POST;
  • linhas 40-45: os parâmetros codificados por URL no URL;
  • linhas 35-39: os parâmetros codificados como URL no corpo (body, documento) da solicitação;

Ao executar, obtêm-se os seguintes resultados no console:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:34:23 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 270
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • linha 10: percebe-se que o servidor conseguiu recuperar os dois tipos de parâmetros;

17.9.7. Um cliente misto GET

Tentamos fazer o mesmo que anteriormente com uma solicitação GET. O script [parameters-mixte-get-client.php] é o seguinte:


<?php

// cliente POST de um servidor web
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-parameters-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// cria-se um cliente HTTP
$httpClient = HttpClient::create();
try {
  // prepara-se os parâmetros
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // envia-se a solicitação ao servidor
  $response = $httpClient->request('GET', $config['url-post'],
    [
      // parâmetros do documento (corpo)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parâmetros do URL (consulta)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperam-se os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibindo a resposta do servidor
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // exibindo o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linha 32: uma solicitação POST;
  • linhas 40-45: os parâmetros codificados por URL na solicitação URL;
  • linhas 35-39: os parâmetros codificados como URL no corpo (body, documento) da solicitação;

Ao executar, obtêm-se os seguintes resultados no console:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:41:19 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 217
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":[]}]
  • linha 10: verifica-se que o servidor não recebeu parâmetros codificados como URL no documento enviado pelo cliente. Ao analisar os cabeçalhos HTTP enviados por ele, percebe-se que ele realmente enviou um documento de 44 caracteres, mas o servidor não o processou;

Afinal, qual método escolher para enviar informações ao servidor?

  • o método [GET URL?param1=val1&param2=val2&…] utiliza um URL configurado que pode servir como link. Essa é sua principal vantagem: o usuário pode adicionar esses links aos seus favoritos;
  • em outras aplicações, pode-se preferir não exibir em um URL os parâmetros enviados ao servidor. Por motivos de segurança, por exemplo. Nesse caso, utilizar-se-á um método [POST] e os parâmetros codificados por URL serão incluídos em um documento enviado ao servidor;

17.10. Gerenciamento de sessões na web

Nos exemplos cliente/servidor anteriores, o funcionamento era o seguinte:

  • o cliente estabelece uma conexão com a porta 80 do servidor do serviço web;
  • ele envia a sequência de texto: cabeçalhos HTTP, linha vazia, [document];
  • em resposta, o servidor envia uma sequência do mesmo tipo;
  • o servidor encerra a conexão com o cliente;
  • o cliente encerra a conexão com o servidor;

Se o mesmo cliente fizer, pouco depois, uma nova solicitação ao servidor web, uma nova conexão é criada entre o cliente e o servidor. O servidor não tem como saber se o cliente que está se conectando já esteve lá antes ou se esta é a primeira solicitação. Entre duas conexões, o servidor “esquece” seu cliente. Por esse motivo, diz-se que o protocolo HTTP é um protocolo sem estado. No entanto, é útil que o servidor se lembre de seus clientes. Assim, se um aplicativo for seguro, o cliente enviará ao servidor um nome de usuário e uma senha para se identificar. Se o servidor “esquecer” seu cliente entre duas conexões, este terá que se identificar a cada nova conexão, o que não é viável.

Para acompanhar um cliente, o servidor procede da seguinte maneira: durante a primeira solicitação de um cliente, ele inclui em sua resposta um identificador que o cliente deve, então, reenviar a cada nova solicitação. Graças a esse identificador, diferente para cada cliente, o servidor pode reconhecer um cliente. Ele pode, então, gerenciar uma memória para esse cliente na forma de uma memória associada de maneira única ao identificador do cliente.

Tecnicamente, isso ocorre da seguinte forma:

  • na resposta a um novo cliente, o servidor inclui o cabeçalho HTTP Set-Cookie: MotClé=Identificador. Ele faz isso apenas na primeira solicitação;
  • nas solicitações seguintes, o cliente reenviará seu identificador por meio do cabeçalho HTTP Cookie: MotClé=Identificador para que o servidor o reconheça;

Pode-se questionar como o servidor sabe que está lidando com um novo cliente, em vez de um cliente que já visitou o site anteriormente. É a presença do cabeçalho HTTP Cookie nos cabeçalhos HTTP do cliente que indica isso ao servidor. Para um novo cliente, esse cabeçalho está ausente.

O conjunto de conexões de um determinado cliente é chamado de sessão.

17.10.1. O arquivo de configuração [php.ini]

Para que o gerenciamento de sessões funcione corretamente com o PHP, é necessário verificar se ele está configurado corretamente. No Windows, seu arquivo de configuração é o php.ini. Dependendo do contexto de execução (console, web), o arquivo de configuração [php.ini] deve ser procurado em pastas diferentes. Para identificá-las, utilizar-se-á o seguinte script:

1
2
3
4
<?php

// informações PHP
phpinfo();

Na linha 4, a função phpinfo fornece informações sobre o interpretador PHP que executa o script. Ela fornece, em particular, o caminho do arquivo de configuração [php.ini] utilizado.

Já utilizamos esse script em um ambiente de console (ver parágrafo com link). Em um ambiente web, obtém-se o seguinte resultado:

Image

  • no [1-2], o arquivo [php.ini] que configura o interpretador de scripts da web. Nesse arquivo, encontramos uma seção de sessão:
[Session]
session.save_handler = files
session.save_path = "C:/myprograms/laragon-lite/tmp"
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 36000
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
  • linha 2: os dados de uma sessão do cliente são salvos em um arquivo;
  • linha 3: a pasta de armazenamento dos dados da sessão. Se essa pasta não existir, nenhum erro será sinalizado e o gerenciamento de sessões não funcionará;
  • linhas 4-6: indicam que o identificador de sessão é gerenciado pelos cabeçalhos HTTP, Set-Cookie e Cookie;
  • linha 7: o cabeçalho Set-Cookie terá o formato Set-Cookie: PHPSESSID=identifiant_de_session;
  • linha 8: uma sessão do cliente não é iniciada automaticamente. O script do servidor deve solicitá-la explicitamente por meio da instrução session_start();
  • linha 9: o cookie de sessão é válido enquanto o navegador do cliente não for fechado;
  • linha 10: o caminho para o qual o cookie de sessão deve ser reenviado. Se for [session.cookie_path = /xxx], então, sempre que o navegador solicitar um URL do tipo [/xxx/yyy/zzz], ele deverá reenviar o cookie. Aqui, o caminho [/] indica que o cookie deve ser reenviado para qualquer URL do site;
  • linha 13: alguns objetos da sessão devem ser serializados para poderem ser armazenados em um arquivo. É o PHP que realiza essa serialização/desserialização com as funções [serialize / unserialize];
  • linha 16: tempo de vida após o qual os objetos de sessão armazenados no arquivo de backup são considerados obsoletos;
  • linha 19: tempo de vida de uma sessão. Após esse período, uma nova sessão é criada e os objetos salvos na sessão anterior são perdidos;

17.10.2. Exemplo 1

17.10.2.1. O servidor

Image

O gerenciamento do identificador de sessão é transparente para um serviço web. Esse identificador é gerenciado pelo servidor web. Um serviço web tem acesso à sessão do cliente por meio da instrução session_start(). A partir desse momento, o serviço web pode ler/gravar dados na sessão do cliente por meio do dicionário $_SESSION. Se for utilizada a biblioteca [HttpFoundation], a sessão fica disponível por meio da expressão [Request→getSession].

O código a seguir, [session-server.php], mostra o gerenciamento em sessão de três contadores. A cada nova solicitação, o script da web incrementa esses contadores e os armazena na sessão para que possam ser recuperados na próxima solicitação.


<?php
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;

//
// recupera-se a solicitação
$request = Request::createFromGlobals();
// sessão
$session = new Session();
$session->start();
// recuperando três contadores na sessão
if ($session->has("N1")) {
  // incremento do contador N1
  $session->set("N1", (int) $session->get("N1") + 1);
} else {
  // o contador N1 não está na sessão — ele é criado
  $session->set("N1", 0);
}
if ($session->has("N2")) {
  // incremento do contador N2
  $session->set("N2", (int) $session->get("N2") + 1);
} else {
  // o contador N2 não está em sessão — ele é criado
  $session->set("N2", 10);
}
if ($session->has("N3")) {
  // incremento do contador N3
  $session->set("N3", (int) $session->get("N3") + 1);
} else {
  // o contador N3 não está em sessão — ele é criado
  $session->set("N3", 100);
}
// elaboramos a resposta
$response = new Response();
// o conteúdo da resposta é texto UTF-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// a resposta será jSON, uma tabela contendo os três contadores
$response->setContent(json_encode([
  "N1" => $session->get("N1"),
  "N2" => $session->get("N2"),
  "N3" => $session->get("N3")]));

// envio da resposta
$response->send();
  • linha 10: o objeto [$request] encapsula todas as informações sobre a solicitação recebida pelo script da web;
  • linhas 12-13: cria-se uma sessão e ela é ativada. O objeto [Session] encapsula os dados da sessão correspondentes ao cookie de sessão enviado pelo cliente. Se o cliente não tiver enviado tal cookie, não haverá dados armazenados em [Session]. O script da web incluirá em sua primeira resposta o cabeçalho HTTP [Set-Cookie : PHPSESSID=xxx]. Em suas solicitações subsequentes, o cliente enviará o cabeçalho HTTP [Cookie : PHPSESSID=xxx] para indicar a sessão cujo conteúdo deseja utilizar. Uma sessão é a memória de um cliente;
  • linha 15: verifica-se se a sessão possui uma chave chamada [N1]. Esse será o nome do nosso primeiro contador. Se não for o caso (linha 20), atribui-se a ele o valor 0 e ele é inserido na sessão. Se for o caso (linha 23), então:
    • o recuperamos da sessão;
    • incrementamos seu valor em 1;
    • o colocamos de volta na sessão;
  • linhas 22-35: fazemos o mesmo para os outros dois contadores N2 e N3;
  • linhas 36-40: prepara-se uma resposta do tipo [application/json];
  • linhas 42-45: a resposta será a string jSON de uma matriz contendo os três contadores;
  • linha 48: envia-se a resposta ao cliente;

Na relação cliente/servidor, o gerenciamento da sessão do cliente no servidor depende dos dois participantes, o cliente e o servidor:

  • o servidor é responsável por enviar um identificador ao cliente na primeira solicitação
  • o cliente é responsável por reenviar esse identificador a cada nova solicitação. Se não o fizer, o servidor considerará que se trata de um novo cliente e gerará um novo identificador para uma nova sessão.

Resultados

Usamos como cliente um navegador da web. Por padrão (na verdade, por configuração), ele reenvia ao servidor os identificadores de sessão que este lhe envia. À medida que as solicitações são feitas, o navegador receberá os três contadores enviados pelo servidor e verá seus valores serem incrementados.

Image

  • Em [2], a primeira solicitação ao serviço web;
  • em [4], a quarta solicitação mostra que os contadores foram incrementados corretamente. Os valores dos contadores são, de fato, memorizados ao longo das solicitações;

Vamos usar o modo de desenvolvimento para ver os cabeçalhos HTTP trocados entre o servidor e o cliente. Fechamos o Firefox para encerrar a sessão atual com o servidor, reabrimos o navegador e ativamos o modo de desenvolvimento (F12). Isso excluirá a sessão atual do navegador, que, portanto, iniciará uma nova. Solicitamos o serviço [session-server.php]:

Image

Em [5], vemos o identificador de sessão enviado pelo servidor em sua resposta à primeira solicitação do cliente. Ele utiliza o cabeçalho HTTP Set-Cookie.

Vamos fazer uma nova solicitação atualizando (F5) a página no navegador da web:

Image

Acima, observamos duas coisas:

  • em [11], o navegador retorna o identificador de sessão com o cabeçalho HTTP Cookie.
  • Em [12], na resposta, o serviço web não inclui mais esse identificador. Agora, cabe ao cliente enviá-lo em cada uma de suas solicitações.

17.10.2.2. O cliente

Agora, vamos escrever um script de cliente baseado no script de servidor anterior. Em sua gestão da sessão, ele deve se comportar como um navegador da web:

  • Na resposta do servidor à sua primeira solicitação, ele deve encontrar o identificador de sessão que o servidor lhe envia. Ele sabe que o encontrará no cabeçalho HTTP Set-Cookie.
  • Em cada uma de suas solicitações subsequentes, ele deve reenviar ao servidor o identificador que recebeu. Ele fará isso com o cabeçalho HTTP Cookie.

Image

O cliente [session-client] é configurado pelo seguinte arquivo jSON [config-session-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/06/session-server.php"
}

O código do cliente [session-client] é o seguinte:


<?php

// gerenciamento de uma sessão
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-session-client.json";

// recupera-se a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// criando um cliente HTTP
$httpClient = HttpClient::create();
try {
  // vamos fazer 10 consultas
  for ($i = 0; $i < 10; $i++) {
    // enviando a solicitação ao servidor
    if (!isset($sessionCookie)) {
      // sem sessão
      $response = $httpClient->request('GET', $config['url']);
    } else {
      // com sessão
      $response = $httpClient->request('GET', $config['url'],
        ["headers" => ["Cookie" => $sessionCookie]]);
    }
    // status da resposta
    $statusCode = $response->getStatusCode();
    print "---Réponse avec statut : $statusCode\n";
    // recuperamos os cabeçalhos
    print "---Entêtes de la réponse\n";
    $headers = $response->getHeaders();
    foreach ($headers as $type => $value) {
      print "$type: " . $value[0] . "\n";
    }
    // recuperamos o cookie de sessão, se houver
    if (isset($headers["set-cookie"])) {
      // cookie de sessão?
      foreach ($headers["set-cookie"] as $cookie) {
        $match = [];
        $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
        if ($match) {
          $sessionCookie = "PHPSESSID=" . $champs[1];
        }
      }
    }
  }
  // exibindo a resposta jSON do servidor
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // exibe o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}


Comentários

  • linha 27: criação do cliente HTTP;
  • linha 30: faremos 10 vezes a mesma consulta ao servidor [session-server.php];
  • linha 32: a variável [$sessionCookie] terá como valor o valor do cabeçalho HTTP [Set-Cookie] recebido pelo cliente;
  • linhas 32-34: se essa variável não existir, significa que a sessão ainda não foi iniciada. Envia-se o comando [GET] sem o cabeçalho [Cookie];
  • linhas 35-38: caso contrário, a sessão já foi iniciada e enviamos o comando [GET] com o cabeçalho [Cookie]. O valor desse cabeçalho será [$sessionCookie];
  • linha 50: se o cabeçalho [Set-Cookie] estiver entre os cabeçalhos HTTP recebidos, então procura-se o cookie de sessão;
  • linha 52: o servidor web pode enviar vários cabeçalhos [Set-Cookie]. O cookie de sessão é apenas um deles. No nosso exemplo, ele tem a particularidade de ter o formato [PHPSESSID=xxx;];
  • linhas 53-57: utiliza-se uma expressão regular para localizar o cookie de sessão;
  • linha 62: após as 10 solicitações serem feitas, exibe-se a última resposta jSON do servidor;

Resultados

A execução do script do cliente gera a seguinte exibição no console do NetBeans:


"C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\clients web\06\session-client.php"
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=1cerjgsgdlc35e1mkenvtltmh8; path=/
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
…………………………………………………………
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse du serveur : {"N1":9,"N2":19,"N3":109}
  • linha 8: em sua primeira resposta, o servidor envia o identificador de sessão. Nas respostas seguintes, ele não o envia mais;
  • linha 41: os três contadores [N1, N2, N3] foram incrementados 9 vezes. Na consulta nº 1, eles foram zerados;

O exemplo a seguir mostra que também é possível salvar os valores de um array ou de um objeto na sessão.

17.10.3. Exemplo 2

17.10.3.1. O servidor

Image

Vamos inserir um objeto [Personne] na sessão. A definição dessa classe é a seguinte:


<?php

namespace Modèles;

class Personne implements \JsonSerializable {
  // atributos
  private $nom;
  private $prénom;
  private $âge;

  // conversão de um array associativo para um objeto [Personne]
  public function setFromArray(array $assoc): Personne {
    // inicializa-se o objeto atual com o array associativo
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // resultado
    return $this;
  }

  // getters e setters
  public function getNom() {
    return $this->nom;
  }

  public function getPrénom() {
    return $this->prénom;
  }

  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }

  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }

  public function getÂge() {
    return $this->âge;
  }

  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }

  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }

  // implementa a interface JsonSerializable
  public function jsonSerialize(): array {
    // retorna um array associativo cujas chaves são os atributos do objeto
    // esse array poderá então ser codificado em jSON
    return get_object_vars($this);
  }

  // conversão de um jSON para um objeto [Personne]
  public static function jsonUnserialize(string $json): Personne {
    // cria-se uma pessoa a partir da sequência jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }

}

O script do servidor será o seguinte:


<?php

// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;

//
// recupera-se a consulta atual
$request = Request::createFromGlobals();

// sessão
$session = new Session();
$session->start();

// recupera-se diversos dados da sessão
// matriz
if ($session->has("tableau")) {
  // a matriz está na sessão — incrementamos todos os seus valores
  $tableau = $session->get("tableau");
  for ($i = 0; $i < count($tableau); $i++) {
    $tableau[$i] += 1;
  }
  // colocamos a matriz de volta na sessão
  $session->set("tableau", $tableau);
} else {
  // a matriz não está na sessão — ela é criada
  $tableau = [0, 10, 100];
  // colocamos na sessão
  $session->set("tableau", $tableau);
}
// dicionário
if ($session->has("assoc")) {
  // [assoc] está na sessão — incrementam-se todos os seus elementos
  $assoc = $session->get("assoc");
  foreach ($assoc as $key => $value) {
    $assoc[$key] = $value + 1;
  }
  // colocamos $assoc na sessão
  $session->set("assoc", $assoc);
} else {
  // [assoc] não está na sessão — ele é criado
  $assoc = ["un" => 0, "deux" => 10, "trois" => 100];
  // colocamos $assoc na sessão
  $session->set("assoc", $assoc);
}
// objeto Pessoa
if ($session->has("personne")) {
  // [personne] está na sessão — incrementamos sua idade
  $personne = $session->get("personne");
  $personne->setÂge($personne->getÂge() + 1);
} else {
  // [personne] não está na sessão — ele é criado
  $personne = (new Personne())->setFromArray(
    ["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
  // colocamos $personne na sessão
  $session->set("personne", $personne);
}
// elabora-se a resposta
$response = new Response();
// o conteúdo da resposta está no formato jSON utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
  "tableau" => $tableau,
  "assoc" => $assoc,
  "personne" => $personne], JSON_UNESCAPED_UNICODE));

// envio da resposta
$response->send();

Comentários

  • linhas 16-17: recuperamos a sessão atual e a ativamos;
  • linhas 21-34: gerencia-se um array [tableau] associado à sessão. A cada nova solicitação, seus elementos são incrementados em 1;
  • linhas 36-49: gerencia-se um array associativo [assoc] colocado na sessão. A cada nova consulta, seus elementos são incrementados em 1;
  • linhas 51-61: gerencia-se um objeto [Personne] armazenado na sessão. A cada nova solicitação, a idade dessa pessoa é incrementada em 1;
  • linhas 62-73: enviamos uma resposta jSON ao cliente: a string jSON de um array associativo;

Vamos executar esse script a partir do NetBeans. As duas primeiras solicitações geram os seguintes resultados (F5 no navegador para a segunda):

Image

  • vemos que, em [6-8], todos os contadores foram incrementados;

17.10.3.2. O cliente

Image

O cliente é o mesmo do exemplo 1 (parágrafo “link”). Modifica-se apenas seu arquivo de configuração [config-session-client]:


{
    "url": "http://localhost/php7/scripts-web/07/session-server.php"
}

A execução produz os seguintes resultados:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=qbfrj8clr20mod3eriur71mao6; path=/
content-length: 119
connection: close
content-type: application/json
---Réponse avec statut : 200
………….……………………………………………………….
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 119
connection: close
content-type: application/json
---Réponse du serveur : {"tableau":[9,19,109],"assoc":{"un":9,"deux":19,"trois":109},"personne":{"nom":"Hûche","prénom":"Léonard","âge":9}}
  • na linha [22], percebe-se que todos os contadores foram incrementados;

17.11. Authentification

Agora, vamos nos concentrar nos serviços web destinados apenas a determinados usuários. Nesse caso, o cliente precisa se autenticar no serviço web antes de receber a resposta.

17.11.1. O cliente

Image

O código do cliente [auth-client.php] é o seguinte:


<?php

// gerenciamento de uma sessão
//
// gerenciamento de erros
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;

// a configuração do cliente
const CONFIG_FILE_NAME = "config-auth-client.json";

// recuperamos a configuração
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}

// criando um cliente HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    // "verify_peer" => false,
    // “verify_host” => false
  ]);


try {
  // a solicitação é enviada ao servidor
  $response = $httpClient->request('GET', $config['url']);
  // status da resposta
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // recuperam-se os cabeçalhos
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // exibe-se a resposta jSON do servidor
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // exibe-se o erro
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Comentários

  • linhas 27-31: foi passado um parâmetro para o método estático [HttpClient::create], um array associativo;
  • linha 28: a chave [auth_basic] tem como valor um array com dois elementos, [user, password]. É com esses elementos que o cliente se autenticará no serviço web. A chave [auth_basic] designa um tipo de autenticação chamado [Autorization Basic], cujo nome corresponde ao cabeçalho HTTP que será emitido pelo cliente. Existem outros tipos de autenticação;
  • exceto por esse código, o cliente é idêntico aos anteriores;

Para visualizar os cabeçalhos HTTP enviados pelo cliente, vamos conectá-lo ao servidor genérico TCP, [RawTcpServer], como já fizemos várias vezes:

Image

Iniciamos o cliente com a seguinte configuração:


{
    "url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}

O servidor [RawTcpServer] recebe então as seguintes linhas:

Image

  • em [5], vemos o cabeçalho [Autorization : Basic XXX] enviado pelo cliente. A sequência XXX é a sequência [user:password] codificada em Base64;

Para ter certeza, você pode decodificar a sequência recebida no site [https://www.base64decode.org/]:

Image

17.11.2. O servidor

Image

O servidor [auth-server.php] é o seguinte:


<?php

// dependências
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

// usuários autorizados
$users = ["admin" => "admin"];
//
// recupera-se a solicitação atual
$request = Request::createFromGlobals();
// autenticação
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// o usuário existe?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// preparação da resposta
$response = new Response();
// definimos o código de status da resposta
if (!$trouvé) {
  // não encontrado – código 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
  // encontrado – código 200
  $response->setStatusCode(Response::HTTP_OK);
}
// a resposta não tem conteúdo, apenas cabeçalhos HTTP
$response->send();

Comentários

  • linha 9: os usuários autorizados; neste caso, apenas um, com login [admin] e senha [admin];
  • linha 14: o identificador do usuário é recuperado no cabeçalho [PHP-AUTH-USER]. Não se trata de um cabeçalho enviado pelo cliente, mas de um cabeçalho criado pelo servidor PHP;
  • linha 15: a senha do usuário é recuperada no cabeçalho [PHP-AUTH-PW], um cabeçalho criado pelo PHP;
  • linha 17: procura-se o usuário que deseja se conectar na lista de usuários autorizados;
  • linhas 23-24: se o usuário não for reconhecido, é enviado ao cliente
    • linha 23: o código [401 Unauthorized];
    • linha 24: um cabeçalho [WWW-Authenticate: Basic realm=”quelque chose”]. A maioria dos navegadores reconhece esse cabeçalho e exibirá uma janela de autenticação solicitando que o usuário se autentique. Os cabeçalhos HTTP devem ser codificados em ISO 8859-1. Os textos do NetBeans, por sua vez, são codificados em UTF-8. A função [utf8_decode] realiza a conversão de UTF-8 para ISO 8859-1. Nesse caso, ela não foi necessária, pois os caracteres da sequência [PHP7 par l’exemple] são os mesmos em UTF-8 e ISO 8859-1. A função serve apenas como um lembrete da codificação utilizada pelos cabeçalhos HTTP;
  • linha 25: se o usuário foi reconhecido, envia-se ao cliente o código [200 OK];

Vamos solicitar o URL [auth-server.php] usando um navegador:

Image

Vemos que o navegador exibe uma janela de autenticação. Em [2], vemos o valor do cabeçalho [WWW-Authenticate] enviado pelo servidor. Se observarmos os cabeçalhos HTTP recebidos pelo navegador, encontramos o seguinte:

1
2
3
4
5
6
7
8
9
HTTP/1.0 401 Unauthorized
Date: Fri, 07 Jun 2019 09:11:23 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
WWW-Authenticate: Basic realm="PHP7 par l'exemple"
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • linha 1: o código [401 Unauthorized] da resposta;
  • linha 6: o cabeçalho HTTP [WWW-Authenticate];
  • linha 7: o corpo da resposta está vazio;

Se, em [3-4], digitar [admin] duas vezes, a resposta do servidor será a seguinte:

1
2
3
4
5
6
7
8
HTTP/1.0 200 OK
Date: Fri, 07 Jun 2019 09:21:00 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • linha 1: o código 200 OK da resposta;
  • linha 6: o corpo da resposta está vazio;

Se, em [3-4], forem digitadas credenciais incorretas, o navegador [Firefox] utilizado para os testes exibe indefinidamente a janela de autenticação até que as credenciais corretas sejam digitadas. A cada vez que ocorre uma troca de dados com o servidor, a mesma resposta é retornada, o que aciona a janela de autenticação do navegador.

Vamos executar o cliente [auth-client.php] com um usuário não autorizado. A resposta do servidor é a seguinte:


---Réponse avec statut : 401
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
  • No [1], o cliente recebeu corretamente um código 401;
  • em [3], uma exceção foi lançada no cliente. Foi o cliente Symfony [HttpClient] que a lançou: ele lança uma exceção quando o código de status da resposta HTTP indica que houve um erro no lado do servidor e o cliente tenta ler os cabeçalhos ou o conteúdo da resposta do servidor. A mensagem na linha 3 nos permite ver que o servidor respondeu com [HTTP/1.0 401 Unauthorized] para indicar que o usuário não foi reconhecido;

Vamos agora executar o cliente [auth-client.php] com o usuário autorizado [‘admin’,’admin’]. A resposta do servidor é então a seguinte:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:11:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :

  • linha 1: o servidor respondeu [HTTP/1. 200 OK];
  • linha 7: a resposta não tem conteúdo (0 byte);

17.11.3. Proteger a conexão cliente/servidor

Vimos que, para se autenticar junto ao servidor, o cliente enviava o cabeçalho:

authorization: Basic YWRtaW46YWRtaW4=

Se essa linha for interceptada por um programa espião, este poderá facilmente recuperar os identificadores [login, mot de passe] codificados em base 64 na sequência [YWRtaW46YWRtaW4=]. Por esse motivo, é necessário que a autenticação ocorra em uma conexão segura entre o cliente e o servidor. As conexões seguras URL utilizam o protocolo [HTTPS] em vez do protocolo HTTP. O protocolo [HTTPS] é o protocolo HTTP dentro de uma conexão cliente/servidor segura. Os URL seguros têm o formato [https://chemin_document].

Nem todos os servidores web aceitam os URL nesse formato. É necessário modificá-los para que sejam seguros. O servidor Apache do Laragon é um servidor seguro, mas o protocolo HTTPS não está ativo por padrão. É necessário ativá-lo no menu do Laragon:

Image

  • para [4]; é necessário ativar a criptografia SSL do servidor Apache;

Feito isso, o servidor Apache é reiniciado automaticamente:

Image

  • para [1], um cadeado verde aparece: isso indica que o protocolo HTTPS foi ativado;
  • No [2], surge uma nova porta de serviço, neste caso a porta 443. Essa é a porta de serviço do protocolo seguro HTTPS;

Agora que temos um servidor seguro, vamos modificar o arquivo de configuração [config-auth-client.json] do cliente da seguinte maneira:


{
    "url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}

No [2], o protocolo passou a ser [https] e a porta, [443].

Agora, vamos executar o cliente [auth-client.php] com o usuário autorizado [admin, admin]. Os resultados no console são os seguintes:

Erreur de communication avec le serveur : Peer certificate cannot be authenticated with given CA certificates for"https://localhost/php7/scripts-web/08/auth-server.php".

O cliente Symfony [HttpClient] gerou uma exceção porque o servidor enviou a ele um certificado de confiança que o [HttpClient] não aceitou. A comunicação SSL é realizada com certificados de confiança certificados por órgãos oficiais. Ao ativar o protocolo HTTPS no servidor Apache do Laragon, foi gerado um certificado autoassinado para o servidor Apache. Um certificado autoassinado é um certificado não validado por uma entidade oficial. O cliente Symfony [HttpClient] rejeitou esse certificado autoassinado.

É possível solicitar ao [HttpClient] que não verifique a validade do certificado enviado pelo servidor. Isso é feito por meio de opções no método [HttpClient::create]:


// Cria-se um cliente HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    "verify_peer" => false
  ]);

A linha 4 solicita que o certificado do servidor não seja verificado. Já havíamos encontrado esse problema no script [http-02.php] do parágrafo com o link. Esse script utilizava a biblioteca [libcurl] para se conectar aos sites HTTP e HTTPS. Na ocasião, utilizamos a seguinte configuração para essa biblioteca:


// Inicialização de uma sessão cURL
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // ocorreu um erro
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // opções do curl
  $options = [
    // modo detalhado
    CURLOPT_VERBOSE => true,
    // nova conexão — sem cache
    CURLOPT_FRESH_CONNECT => true,
    // tempo limite da solicitação (em segundos)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // não verificar a validade dos certificados SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // seguir redirecionamentos
    CURLOPT_FOLLOWLOCATION => true,
    // recuperação do documento solicitado na forma de uma sequência de caracteres
    CURLOPT_RETURNTRANSFER => true
  ];

  // configuração do curl
curl_setopt_array($curl, $options);

Na linha 17, a constante [CURLOPT_SSL_VERIFYPEER] controla se o certificado enviado pelo servidor deve ou não ser verificado. O cliente [HttpClient] é, na verdade, um cliente [curl] quando a extensão [curl] está ativada na configuração de PHP, como é o caso aqui. A classe instanciada por [HttpClient::create] é, então, a classe [CurlHttpClient]. As constantes de [curl] estão disponíveis nessa classe, mas com outros nomes:


$curlopts = [
            CURLOPT_URL => $url,
            CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
            CURLOPT_TCP_NODELAY => true,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
            CURLOPT_COOKIEFILE => '', // Acompanhar cookies durante redirecionamentos
            CURLOPT_CONNECTTIMEOUT_MS => 1000 * $options['timeout'],
            CURLOPT_PROXY => $options['proxy'],
            CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
            CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
            CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
            CURLOPT_CAINFO => $options['cafile'],
            CURLOPT_CAPATH => $options['capath'],
            CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
            CURLOPT_SSLCERT => $options['local_cert'],
            CURLOPT_SSLKEY => $options['local_pk'],
            CURLOPT_KEYPASSWD => $options['passphrase'],
            CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
        ];

Destacamos em amarelo as constantes utilizadas por [CurlHttpClient].

Se agora executarmos o cliente [auth-client] com o usuário [admin, admin], obtemos o seguinte resultado:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:44:37 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :

O usuário foi reconhecido corretamente. Se executarmos o cliente [auth-client] com um usuário diferente de [admin, admin], obtemos o seguinte resultado:

1
2
3
---Réponse avec statut : 403
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 403 Forbidden returned for "https://localhost/php7/scripts-web/08/auth-server.php".

Agora, sabemos como nos autenticar em um servidor seguro.