Skip to content

17. Servizi web

Nota: con "servizio web" intendiamo qui qualsiasi applicazione web che fornisce dati grezzi utilizzati da un client, ovvero uno script da console negli esempi che seguono. Non ci occupiamo di alcuna tecnologia in particolare, ad esempio REST (REpresentational State Transfer) o SOAP (Simple Object Access Protocol), che forniscono più o meno dati grezzi in un formato ben definito. REST restituisce JSON, mentre SOAP restituisce XML. Ciascuna di queste tecnologie definisce con precisione come il client deve interrogare il server e il formato che la risposta del server deve assumere. In questo corso, saremo molto più flessibili riguardo alla natura della richiesta del client e della risposta del server. Tuttavia, gli script scritti e gli strumenti utilizzati sono simili a quelli della tecnologia REST.

17.1. Introduzione

Poiché i programmi PHP possono essere eseguiti da un server web, un programma di questo tipo diventa un programma lato server in grado di servire più client. Dal punto di vista del client, chiamare un servizio web equivale a richiedere l'URL di quel servizio. Il client può essere scritto in qualsiasi linguaggio, incluso PHP. In quest'ultimo caso, utilizziamo le funzioni di rete che abbiamo appena trattato. Dobbiamo anche sapere come "comunicare" con un servizio web, ovvero comprendere il protocollo HTTP per la comunicazione tra un server web e i suoi client. Questo era lo scopo della sezione "link".

Il client web descritto nella sezione "link" ci ha permesso di esplorare parte del protocollo HTTP.

Image

Nella loro forma più semplice, gli scambi client/server procedono come segue:

  • il client apre una connessione alla porta 80 sul server web;
  • effettua una richiesta per un documento;
  • il server web invia il documento richiesto e chiude la connessione;
  • il client chiude quindi la connessione;

Il documento può essere di vario tipo: testo in formato HTML, un'immagine, un video... Può trattarsi di un documento esistente (documento statico) o di un documento generato al volo da uno script (documento dinamico). In quest'ultimo caso, si parla di programmazione web. Lo script per la generazione dinamica dei documenti può essere scritto in vari linguaggi: PHP, Python, Perl, Java, Ruby, C#, VB.NET...

Di seguito, utilizzeremo script PHP per generare dinamicamente documenti di testo.

Image

  • In [1], il client stabilisce una connessione con il server, richiede uno script PHP e può inviare o meno dei parametri a tale script;
  • In [2], il server web esegue lo script PHP utilizzando l'interprete PHP. Lo script genera un documento che viene inviato al client [3];
  • Il server chiude la connessione. Il client fa lo stesso;

Il server web può gestire più client contemporaneamente.

Con il pacchetto software [Laragon], il server web è un server Apache, un server open-source della Apache Foundation (http://www.apache.org/). Nelle seguenti applicazioni, è necessario avviare [Laragon]:

Image

Questo avvia il server web Apache e il DBMS MySQL.

Gli script eseguiti dal server web saranno scritti utilizzando lo strumento NetBeans. Finora abbiamo scritto script PHP eseguiti in un ambiente console:

Image

L'utente utilizza la console per richiedere l'esecuzione di uno script PHP e ricevere i risultati.

Nelle applicazioni client/server che seguono:

  • lo script client viene eseguito in un ambiente console;
  • lo script server viene eseguito in un contesto web;

Image

Lo script PHP lato server non può essere collocato in una posizione qualsiasi del file system. Infatti, il server web cerca i documenti statici e dinamici che gli vengono richiesti nelle posizioni specificate dalla configurazione. La configurazione predefinita di Laragon fa sì che i documenti vengano cercati nella cartella <Laragon>/www, dove <Laragon> è la cartella di installazione di Laragon . Pertanto, se un client web richiede un documento D con l'URL [http://localhost/D], il server web fornirà il documento D situato nel percorso [<Laragon>/www/D].

Negli esempi seguenti, collocheremo gli script del server nella cartella [www/php7/scripts-web]. Se uno script del server si chiama S.php, verrà richiesto al server web utilizzando l'URL [http://localhost/php7/scripts-web/S.php]. Verrà quindi servito il documento [<Laragon>/www/php7/scripts-web/S.php].

Image

  • in [1], la cartella [<laragon>/www];
  • in [2], la cartella [php7/scripts-web];

Per creare script server con NetBeans, procederemo come segue:

Image

  • in [1-2], creiamo un nuovo progetto
  • nei passaggi [3-4], selezioniamo la categoria [PHP] e il progetto [PHP Application]

Image

  • in [5], il nome del progetto;
  • in [6], la cartella del progetto nel file system. Si noti che questa si trova nella cartella [<laragon>/www], dove dovrebbe essere;
  • In [7-8], accettiamo i valori predefiniti;
  • In [9-10], accettare i valori predefiniti forniti. In [10], notare che l'URL degli script che inseriremo in questo progetto inizierà con il percorso [http://localhost/php7/scripts-web/];

Image

  • In [11], ti vengono proposti framework web scritti in PHP. Questi framework sono essenziali non appena l'applicazione web cresce di dimensioni;
  • In [12], puoi aggiungere librerie PHP utilizzando lo strumento [Composer]. Abbiamo utilizzato questo strumento due volte in una finestra [Terminale] di Laragon:
    • per installare la libreria [SwiftMailer], che consente di inviare e-mail;
    • per installare la libreria [php-mime-mail-parser], che consente di leggere le e-mail;
  • in [13], una volta confermata la procedura guidata di creazione del progetto, il progetto appare in [13] nella scheda Progetti;

17.2. Scrivere una pagina statica

Nota: per il resto di questa guida, [Laragon] deve essere in esecuzione.

Mostreremo come creare una pagina HTML (HyperText Markup Language) statica utilizzando NetBeans:

Image

  • Nei passaggi [1-5], creiamo una cartella denominata [01];

Image

Image

  • In [6-12], creiamo un file HTML denominato [example-01.html];

Il file [example-01.html] viene generato con il seguente contenuto precompilato (maggio 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>

Aggiorniamo il contenuto come segue:


<!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>

Abbiamo modificato il titolo della pagina (riga 4) e il suo contenuto (riga 9).

Ora facciamo in modo che il server Apache di Laragon visualizzi questa pagina HTML:

Image

  • nei punti [1-2], il server Apache Laragon visualizza la pagina;
  • in [3], l'URL della pagina visualizzata;
  • in [4], il titolo che abbiamo modificato;
  • in [5], il contenuto che abbiamo modificato;

La pagina visualizzata è una pagina statica: puoi ricaricarla tutte le volte che vuoi nel browser (F5) e verrà sempre visualizzato lo stesso contenuto.

La maggior parte dei browser consente di accedere ai dati scambiati tra il client e il server, come descritto nella sezione “Link”. In Firefox (a maggio 2019), premere F12 per accedere a questi dati:

Image

Come indicato in [1], ricarichiamo la pagina (F5):

Image

  • in [2], il documento caricato dal browser: lo selezioniamo;

Image

  • in [5], il documento da analizzare è selezionato;
  • in [3-4], richiediamo di visualizzare gli scambi client/server;
  • In [6], tali scambi;

Image

  • in [7], selezionare la scheda delle intestazioni;
  • in [8], l'URL richiesto dal browser;
  • in [9], il comando inviato al server è [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1];
  • in [10], le intestazioni HTTP successivamente inviate dal browser (il client);
  • in [11], le intestazioni HTTP della risposta del server;

Image

  • in [12-14], la risposta del server inviata dopo le intestazioni HTTP;
  • in [14], vediamo che il browser client ha ricevuto la pagina HTML che abbiamo creato. Ha quindi interpretato questo codice per visualizzare quanto segue:

Image

17.3. Creazione di una pagina dinamica in PHP

Ora scriveremo una pagina dinamica in PHP:

Image

Image

  • nei punti [1-8], creiamo una pagina [example-01.php];

Il file [example-01.php] viene generato precompilato come segue (maggio 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
        // put your code here
        ?>
    </body>
</html>

Modifichiamo il codice sopra riportato come segue:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time : nb de millisecondes entre le moment présent et le 01/01/1970
        // format affichage date-heure
        // d : jour sur 2 chiffres
        // m : mois sur 2 chiffres
        // y : année sur 2 chiffres
        // H : heure 0,23
        // I : minutes
        // s: secondes
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

Commenti

  • riga 5: abbiamo modificato il titolo della pagina;
  • riga 17: stampa la data e l'ora correnti;

In sostanza, lo script PHP sopra riportato stampa l'ora corrente sulla console. Tuttavia, quando viene eseguito da un server web, l'output dell'istruzione [print] — che di solito è indirizzato alla console di esecuzione dello script — viene reindirizzato qui alla connessione che collega il server al suo client. Pertanto, in un contesto web, lo script sopra riportato invia l'ora corrente come testo al client, in questo caso un browser.

Eseguiamo lo script [example-01.php]:

Image

  • in [3], l'URL richiesto al server web Apache;
  • in [4], il titolo della pagina che abbiamo modificato;
  • in [5], il contenuto generato dall'istruzione [print];

Si tratta di una pagina dinamica perché se si ricarica la pagina più volte nel browser (F5), il suo contenuto cambia (l'ora cambia).

Il browser ha ricevuto un flusso HTML. Per visualizzarlo, è necessario visualizzare il codice sorgente della pagina nel browser:

Image

  • per accedere al menu [1], fare clic con il tasto destro del mouse sulla pagina nel browser;
  • in [2], l'URL della pagina [example-01.php] preceduto da [view-source:] [3];
  • in [4], il contenuto HTML visualizzato dal browser;

È quindi importante ricordare che uno script PHP destinato ad essere eseguito da un server web deve produrre un flusso HTML.

Diamo ora un'occhiata (F12) alle intestazioni HTTP inviate dal server al browser del client:

Image

  • in [3], un'intestazione HTTP che non era presente quando è stata richiesta la pagina statica. Questa intestazione indica che la risposta del server è stata generata da uno script PHP;

Abbiamo visto che la risposta del server (in questo caso l'output HTML) può essere generata da uno script PHP. Lo script può anche generare le intestazioni HTTP e praticamente tutti gli elementi della risposta del server.

17.4. Nozioni di base sull'HTML

Questo capitolo non approfondirà la programmazione web in PHP. Un'applicazione web MVC è sviluppata nella sezione collegata. Questo capitolo si concentra invece sui servizi web: pagine PHP che forniscono dati, tramite un server web, ad altri client PHP. Tuttavia, abbiamo ritenuto utile fornire al lettore alcune nozioni di base sull'HTML.

Un browser web può visualizzare vari documenti, i più comuni dei quali sono i documenti HTML (HyperText Markup Language). Questi consistono in testo formattato con tag nella forma <tag>testo</tag>. Pertanto, il testo <b>importante</b> visualizzerà il testo "importante" in grassetto. Esistono tag autonomi, come il tag <hr/>, che visualizza una linea orizzontale. Non esamineremo tutti i tag che si possono trovare in un testo HTML. Esistono molti programmi software WYSIWYG che consentono di creare una pagina web senza scrivere una sola riga di codice HTML. Questi strumenti generano automaticamente il codice HTML per un layout creato utilizzando il mouse e controlli predefiniti. È quindi possibile inserire (utilizzando il mouse) una tabella nella pagina e poi visualizzare il codice HTML generato dal software per scoprire i tag da utilizzare per definire una tabella su una pagina web. È semplicissimo. Inoltre, la conoscenza dell'HTML è essenziale poiché le applicazioni web dinamiche devono generare autonomamente il codice HTML da inviare ai client web. Questo codice viene generato a livello di programmazione e, ovviamente, è necessario sapere cosa generare affinché il client riceva la pagina web desiderata.

In breve, non è necessario conoscere l'intero linguaggio HTML per iniziare a programmare per il web. Tuttavia, questa conoscenza è necessaria e può essere acquisita utilizzando editor di pagine web WYSIWYG come DreamWeaver e decine di altri. Un altro modo per scoprire le complessità dell'HTML è navigare sul web e visualizzare il codice sorgente delle pagine che presentano elementi interessanti che non avete ancora incontrato.

Si consideri il seguente esempio, che evidenzia alcuni elementi comunemente presenti in un documento web, quali:

  • una tabella;
  • un'immagine;
  • un link.

Image

Un documento HTML ha generalmente la seguente struttura:

<html> <head> <title>Un titolo</title> ... </head> <attributi del corpo> ... </body></html>

L'intero documento è racchiuso tra i tag <html>…</html>. È composto da due parti:

  1. <head>…</head>: questa è la parte non visualizzabile del documento. Fornisce informazioni al browser che visualizzerà il documento. Spesso contiene il tag <title>…</title>, che imposta il testo da visualizzare nella barra del titolo del browser. Può contenere anche altri tag, in particolare quelli che definiscono le parole chiave del documento, che vengono poi utilizzate dai motori di ricerca. Questa sezione può contenere anche script, solitamente scritti in JavaScript o VBScript, che verranno eseguiti dal browser.
  1. <attributi body>…</body>: Questa è la sezione che verrà visualizzata dal browser. I tag HTML contenuti in questa sezione indicano al browser il layout visivo "desiderato" per il documento. Ogni browser interpreta questi tag a modo suo. Di conseguenza, due browser possono visualizzare lo stesso documento web in modo diverso. Questa è generalmente una delle sfide che devono affrontare i web designer.

Il codice HTML per il nostro documento di esempio è il seguente:


<!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>
HTML
Tag HTML ed esempi
titolo del documento
<title>Alcuni tag HTML</title> (riga 5)
Il testo [Alcuni tag HTML] apparirà nella barra del titolo del browser quando il documento viene visualizzato
barra orizzontale
<hr />: visualizza una linea orizzontale (riga 10)
tabella
<table attributes>….</table>: per definire la tabella (righe 12, 32)
<thead>…</thead>: per definire le intestazioni delle colonne (righe 13, 19)
<tbody>…</tbody>: per definire il contenuto della tabella (righe 20, 31)
<tr attributes>…</tr>: per definire una riga (righe 21, 25)
<attributi td>…</td>: per definire una cella (riga 22)
esempi:
<table border="1">…</table>: l'attributo border definisce lo spessore del bordo della tabella
<td style="text-align: center;">cell(1,2)</td> (riga 23): definisce una cella il cui contenuto sarà cell(1,2). Questo contenuto sarà centrato orizzontalmente (text-align: center).
immagine
<img border="0" src="images/cherrytree.jpg"/> (riga 38): definisce un'immagine senza bordo (border="0") il cui file sorgente è [images/cherrytree.jpg] sul server web (src="images/cherrytree.jpg"). Questo collegamento si trova in un documento web accessibile tramite l'URL http://localhost/php7/scripts-web/01/balises.html. Pertanto, il browser richiederà l'URL http://localhost/php7/scripts-web/01/images/cerisier.jpg per recuperare l'immagine a cui si fa riferimento qui.
link
<a href="http://www.polytech-angers.fr/fr/index.html">qui</a> (riga 42): fa sì che il testo "qui" funga da collegamento all'URL http://www.polytech-angers.fr/fr/index.html.
sfondo della pagina
<body style="background-image: url(images/standard.jpg)"> (riga 8): indica che l'immagine da utilizzare come sfondo della pagina si trova all'URL [images/standard.jpg] sul server web. Nel contesto del nostro esempio, il browser richiederà l'URL http://localhost/php7/scripts-web/01/images/standard.jpg per recuperare questa immagine di sfondo.

In questo semplice esempio possiamo vedere che, per costruire l'intero documento, il browser deve effettuare tre richieste al server:

  1. http://localhost/php7/scripts-web/01/images/balises.html per recuperare il codice sorgente HTML del documento
  2. http://localhost/php7/scripts-web/01/images/cerisier.jpg per recuperare l'immagine cerisier.jpg
  3. http://localhost/php7/scripts-web/01/images/standard.jpg per recuperare l'immagine di sfondo standard.jpg

Ciò è dimostrato dal traffico di rete tra il client e il server (F12 nel browser):

Image

  • In [3-5], vediamo le tre richieste effettuate dal browser;

17.5. Rendere dinamica una pagina statica

Vediamo come rendere dinamica la pagina HTML [example-01.html]. Copia il contenuto

Image

Abbiamo copiato il contenuto di [example-01.html] nel file [page-01.php]. Se eseguiamo [2] questo script web, nel browser vediamo quanto segue:

Image

  • in [3], l'URL richiesto;
  • in [4], il titolo della pagina;
  • in [5], il contenuto della pagina;

Se visualizziamo il codice ricevuto dal browser, troviamo questo:

Image

  • in [7], il codice HTML inserito nello script [example-01.php]

L'interprete PHP ha interpretato lo script [page-01.php] e ha prodotto lo stesso output HTML della pagina statica [example-01.html]. Nello script [page-01.php] non c'era PHP, ma solo HTML. Questo ci insegna qualcosa: quando l'interprete PHP trova dell'HTML in uno script PHP, lo lascia stare e lo invia così com'è al client.

Ora aggiungiamo alcune istruzioni PHP allo script [page-01.php] in modo che l'interprete PHP abbia qualcosa da fare:


<!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>

Nelle righe 4 e 9 abbiamo aggiunto del codice PHP per generare dinamicamente il titolo e il contenuto della pagina. Qui supponiamo che la variabile [$page] sia un oggetto contenente i dati da visualizzare.

Se eseguiamo questo nuovo codice, otteniamo il seguente risultato nel browser:

Image

  • in [1], l'URL richiesto;
  • in [2], il titolo della pagina non è stato visualizzato perché la variabile [$page] non era definita;
  • in [3], lo stesso vale per il contenuto;

Ora, scriviamo il seguente script web [example-02.php]:

Image

Lo script [example-02.php] sarà il seguente:


<?php
 
// define the page elements to be displayed
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// display [page-01]
require_once "page-01.php";
  • righe 4-6: definiamo l'oggetto [$page];
  • riga 8: includere lo script [page-01.php]. Il codice contenuto in questo script verrà quindi interpretato:
    • la variabile [$page] è ora definita e l'interprete PHP la utilizzerà;
    • il codice HTML da [page-01.php] verrà inviato così com'è al client;
    • i risultati delle operazioni PHP [print] saranno inclusi nel flusso di testo inviato al client;

Ora, se eseguiamo lo script web [example-02.php], nel browser otteniamo quanto segue:

Image

Se visualizziamo il contenuto testuale ricevuto dal browser:

Image

  • il codice PHP che si trovava in [2] e [3] è stato sostituito dai risultati dei due comandi [print];

Da questo esempio possiamo trarre due punti chiave:

  • le pagine HTML destinate al browser possono essere isolate in script PHP contenenti solo codice HTML e alcune parti dinamiche generate dal codice PHP. In queste pagine dovrebbe esserci il minor codice PHP possibile;
  • tutta la logica che genera i dati dinamici inclusi nelle pagine HTML deve essere isolata in script PHP puri, che non contengano codice di presentazione della pagina (HTML, CSS, JavaScript, ecc.);

Ciò consente una separazione dei compiti:

  • il compito di creare le pagine web da visualizzare (HTML, CSS, JavaScript, ecc.);
  • il compito della logica dell'applicazione web che stiamo realizzando. Questa logica può essere implementata utilizzando un'architettura a tre livelli, esattamente come abbiamo fatto con gli script da console;

Successivamente, realizzeremo script web specifici;

  • questi invieranno al client solo dati e nessun elemento di presentazione (HTML, CSS, JavaScript). Saranno quindi dei server di dati piuttosto che delle pagine web;
  • I client per questi script web saranno script da console che recupereranno i dati inviati dal server e li elaboreranno;

17.6. Applicazione client/server per data e ora

Ora ci troviamo nella seguente configurazione:

Image

Scriveremo:

  • uno script web [1] che invia la data e l'ora correnti al proprio client;
  • uno script da console [2] che fungerà da client per lo script web: recupererà la data e l'ora inviate dallo script web e le visualizzerà sulla console;

Image

  • in [1], lo script web [date-time-server.php];
  • in [2], lo script da console [date-time-client], che è il client dello script web;

17.6.1. Lo script server

Abbiamo già scritto uno script web che genera la data e l'ora correnti nella sezione collegata. Si trattava del seguente script [example-01.php]:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time : nb de millisecondes depuis 01/01/1970
        // format affichage date-heure
        // d: jour sur 2 chiffres
        // m: mois sur 2 chiffres
        // y : année sur 2 chiffres
        // H : heure 0,23
        // i : minutes
        // s: secondes
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

Avevamo detto che avremmo scritto dei server di dati: dati grezzi senza markup HTML. Lo script del server [date-time-server.php] sarà quindi il seguente:


<?php
 
// set header HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// send date and time
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: 2-digit day
// m: 2-digit month
// y: 2-digit year
// H: hour 0.23
// i : minutes
// s: seconds
print date("d/m/y H:i:s", time());
  • Riga 4: Impostiamo l'intestazione HTTP [Content-Type], che indica al client la natura del documento che riceverà. Finora, il [Content-Type] era: [Content-Type: text/html; charset=UTF-8]. Qui, comunichiamo al client che il documento è testo semplice senza markup HTML. Questo non è importante per il nostro client console, che non utilizzerà questa intestazione. È più importante per i client browser, che invece utilizzano questa intestazione;

Eseguiamo questo script lato server:

Image

Se esaminiamo la risposta del server nel browser (F12), vediamo in [5] l'intestazione HTTP impostata dallo script del server e in [8] il documento di testo ricevuto;

Image

17.6.2. Lo script lato client

Nella sezione precedente abbiamo sviluppato diversi client HTTP. Potremmo usarli per recuperare il documento di testo inviato dallo script del server [date-time-server.php]. Non lo faremo. Come abbiamo fatto per i protocolli SMTP e IMAP, useremo una libreria di terze parti, ovvero il componente [HttpClient] del framework Symfony [https://symfony.com/doc/master/components/http_client.html].

Come per le due librerie precedenti, utilizziamo lo strumento [Composer] per installare il componente [HttpClient] di Symfony. In una finestra [Terminale] di Laragon (vedi la sezione collegata), inserisci il seguente comando:

Image

  • in [3], verifica di trovarti nella directory [<laragon>/www/], dove <laragon> è la directory di installazione di Laragon;
  • in [4], il comando [composer] che installa la libreria Symfony [HttpClient];
  • in [5], non viene installato nulla perché la libreria [HttpClient] era già stata installata su questa macchina;
  • In [6-7], compaiono nuove cartelle in [<laragon>/www/vendor/symfony];

Al posto di [5], dovresti avere qualcosa di simile a quanto segue:


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

Assicurati che la cartella [<laragon>/www/vendor] sia inclusa nel [Include Path] del tuo progetto (vedi la sezione collegata):

Image

Una volta fatto questo, possiamo scrivere lo script da console [date-time-client.php]:

Image

Lo script da console [date-time-client.php] utilizzerà il seguente file JSON [config-date-time-client.json]:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server.php"
}
  • riga 2: l'URL dello script del server;

Lo script client [date-time-client.php] sarà il seguente:


<?php
 
// service customer date / time
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-date-time-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // query
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // retrieve the body of the reply
  $content = $response->getContent();
  // we display it
  print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
  exit;
}

Commenti

  • riga 10: come abbiamo fatto per le librerie precedenti, carichiamo il file [<laragon>/www/vendor/autoload.php];
  • riga 11: dichiariamo la classe [HttpClient] che useremo;
  • righe 13–24: recuperiamo la configurazione dello script dal dizionario [$config];
  • riga 27: creiamo un oggetto di tipo [HttpClient];
  • riga 31: richiediamo l'URL dello script del server utilizzando una richiesta GET: [GET URL HTTP/1.1]. Questa operazione è asincrona. L'esecuzione prosegue alla riga 33 senza attendere la risposta;
  • riga 33: viene recuperato lo stato della risposta. Questo stato si trova nel primo header HTTP restituito dal server. Pertanto, se questo header è [HTTP/1.1 200 OK], lo stato della risposta è 200. Questa operazione è bloccante: l'esecuzione riprende solo una volta che il client ha ricevuto l'intera risposta dal server;
  • riga 37: vengono richieste le intestazioni HTTP della risposta;
  • riga 42: recuperiamo il documento restituito dal server: sappiamo che questo documento è di tipo testo.
  • righe 45–49: se si verifica un errore, viene visualizzato il messaggio di errore;

Quando viene eseguito lo script client (Laragon deve essere in esecuzione affinché lo script server sia accessibile), sulla console viene visualizzato il seguente risultato:


---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]

Abbiamo recuperato con successo la data e l'ora correnti alla riga 8.

Potresti essere curioso di sapere cosa ha inviato lo script client al server. Per farlo, useremo il nostro server TCP generico (vedi la sezione "link"):

Image

  • in [1], la cartella utilities;
  • in [2], il server TCP è in esecuzione sulla porta 100;
  • in [3], in attesa di un comando immesso tramite la tastiera;

Modifichiamo il file di configurazione dello script [date-time-client.php]:


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

In questo caso, il client contatta il server [localhost] sulla porta 100. Pertanto, verrà richiamato il nostro server TCP generico. Quando eseguiamo lo script della console [date-time-client.php], la console del server TCP generico cambia come segue:

Image

  • in [3], la richiesta HTTP GET costruita dallo script client;
  • in [4], la firma dello script della console;
  • in [5], la risposta del server allo script client. Si noti che questa non è una risposta HTTP valida:
    • dovrebbero esserci le intestazioni HTTP;
    • seguite da una riga vuota;
    • poi il documento di testo inviato al client;
  • in [6], chiudiamo la connessione con lo script client in modo che rilevi di aver ricevuto l'intera risposta;

Nello script lato client, la console visualizza quanto segue:

Image

  • in [7], ciò che ha ricevuto il client Symfony;

17.6.3. Lo script del server – versione 2

Per impostazione predefinita, le funzioni PHP per la scrittura di uno script web non sono orientate agli oggetti. Sul lato server, siamo quindi costretti a mescolare classi e funzioni PHP tradizionali. Per ottenere uno stile di codifica più coerente, useremo la libreria [HttpFoundation] del framework Symfony. Essa incapsula tutte le funzioni PHP tradizionali per un servizio web in un sistema di classi e interfacce. La documentazione della libreria è disponibile all'indirizzo [https://symfony.com/doc/current/components/http_foundation.html] (maggio 2019).

Per installare la libreria, seguire questi passaggi in un terminale Laragon (vedere la sezione collegata):

Image

  • [2-3]: Assicurati di trovarti nella cartella [<laragon>/www];
  • [4]: il comando [composer], che installerà la libreria [HttpFoundation];
  • [5]: In questo esempio, la libreria era già installata;

Al momento della prima installazione, dovresti vedere dei log della console simili ai seguenti:


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

La seconda versione del server web [date-time-server-2.php] è la seguente:


<?php
 
// using Symfony libraries
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
 
// we set the Content-Type header
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");
 
// set the content of the response
//
// send date and time
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: 2-digit day
// m: 2-digit month
// y: 2-digit year
// H: hour 0.23
// i : minutes
// s: seconds
$response->setContent(date("d/m/y H:i:s", time()));
 
// we send the answer
$response->send();

Commenti

  • Riga 7: la classe [Response] della libreria [HttpFoundation] di Symfony gestisce l'intera risposta ai client del servizio web;
  • riga 10: creazione di un'istanza della classe [Response];
  • riga 11: specifica che la risposta è di tipo [text/plain];
  • riga 12: la risposta è testo UTF-8;
  • riga 25: il corpo della risposta viene impostato sul contenuto richiesto dal client;
  • riga 28: la risposta viene inviata al client;

17.6.4. Lo script client – versione 2

Lo script client rimane invariato. Modifichiamo solo il suo file di configurazione [config-date-time-client.json]:

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

I risultati sono gli stessi della versione 1.

17.7. Un server di dati JSON

La risposta di uno script web può consistere in più punti dati che possono essere organizzati in array e oggetti. Lo script può quindi inviare questi vari elementi all'interno di una stringa JSON che il client decodificherà.

Image

17.7.1. Lo script del server

Lo script [json-server.php] utilizza la seguente classe [Person]:


<?php
 
namespace Modèles;
 
class Personne implements \JsonSerializable {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
 
  // convert associative array to object [Person]
  public function setFromArray(array $assoc): Personne {
    // initialize the current object with the associative array
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // result
    return $this;
  }

  // getters and 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]";
  }
 
  // implements the JsonSerializable interface
  public function jsonSerialize(): array {
    // render an associative array with the object's attributes as keys
    // this table can then be encoded as jSON
    return get_object_vars($this);
  }
 
  // convert a jSON to a [Person] object
  public static function jsonUnserialize(string $json): Personne {
    // we create a person from the string jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }
 
}

Commenti

  • riga 5: la classe implementa l'interfaccia PHP [JsonSerializable]. Ciò richiede l'implementazione del metodo [jsonSerialize] nelle righe 55–59. Il metodo deve restituire un array associativo che verrà serializzato in JSON. Quando si utilizza l'espressione [json_encode($person)], la funzione [json_encode] verifica se la classe [Person] implementa l'interfaccia [JsonSerializable]. In tal caso, l'espressione diventa [json_encode($person→serialize())];
  • Righe 12–19: La classe non ha un costruttore ma ha un inizializzatore. La classe [Person] può quindi essere istanziata utilizzando l'espressione [(new Person()) → setFromArray($array)]. Possono esserci vari tipi di inizializzatori, mentre può esserci un solo costruttore. Questi inizializzatori consentono varie modalità di istanziazione della forma [(new Person())→initializer(…));
  • righe 62–65: La funzione statica [jsonUnserialize] consente di creare un oggetto [Person] dalla sua stringa JSON;

Lo script [json-server.php] sarà il seguente:


<?php
 
// dependencies
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
 
// set the Content-Type header and the character library used
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// create a Person object
$personne = (new Personne())->setFromArray([
  "nom" => "de la Hûche",
  "prénom" => "jean-paul",
  "âge" => 27]);
// an associative table
$assoc = ["attr1" => "value1",
  "attr2" => [
    "prenom" => "Jean-Paul",
    "nom" => "de la Hûche"
  ]
];
// the content of the response is jSON
$response->setContent(json_encode([$personne, $assoc]));
 
// reply sent
$response->send();

Commenti

  • righe 4-5: importiamo la classe [Person];
  • riga 11: specifichiamo che il documento sarà di tipo [application/json]. Alla ricezione di questa intestazione, i browser visualizzeranno la stringa JSON formattata anziché come testo semplice;
  • riga 12: la stringa JSON conterrà caratteri UTF-8;
  • righe 15-18: creiamo un oggetto [Person];
  • righe 20–25: viene creato un array associativo a due livelli;
  • riga 27: inviamo la stringa JSON di un array al client:
    • l'elemento [$person] verrà serializzato in JSON utilizzando il suo metodo [jsonSerialize];
    • l'elemento [$assoc] verrà serializzato in JSON in modo nativo;

Se eseguiamo questo script lato server (Laragon deve essere in esecuzione), otteniamo la seguente risposta in un browser:

Image

Image

Commenti

  • in [2], la risposta JSON formattata;
  • in [4], la risposta JSON non formattata. Si noti la codifica dei caratteri accentati;
  • In [6], è stato il tipo di contenuto [application/json] inviato dal server a far sì che il browser formattasse l'output in questo modo;

17.7.2. Il client

Image

Il client [json-client.php] è configurato dal seguente file JSON [config-json-client.json]:

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

Lo script [json-client.php] è il seguente:


<?php
 
// service customer jSON
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
 
// customer configuration
const CONFIG_FILE_NAME = "config-json-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // query
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // retrieve the jSON body of the response
  list($personne, $assoc) = json_decode($response->getContent(), true);
  // a person is instantiated from an array of attributes
  $personne = (new Personne())->setFromArray($personne);
  // server response is displayed
  print "---Réponse du serveur\n";
  print "$personne\n";
  print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
 
 

Commenti

  • righe 12-13: importare la classe [Person];
  • riga 30: creare il client HTTP;
  • riga 44: decodifica della stringa JSON inviata dal server. Sappiamo che ciò che è stato codificato è un array a due elementi contenente due array associativi;
  • riga 46: creazione di un oggetto [Person] per visualizzarlo alla riga 49;
  • riga 50: viene visualizzato il secondo array associativo. L'istruzione [print] non è in grado di visualizzare gli array. Pertanto, lo convertiamo in una stringa JSON. Per visualizzare correttamente i caratteri accentati, dobbiamo impostare il secondo parametro su [JSON_UNESCAPED_UNICODE]. Abbiamo visto che i caratteri accentati sono effettivamente codificati nella stringa JSON;

L'esecuzione dello script lato client produce i seguenti risultati:


---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"}}

Righe 11 e 12: i caratteri accentati sono stati recuperati correttamente.

17.8. Recupero delle variabili d'ambiente del servizio web

Uno script server viene eseguito in un ambiente web a cui può accedere. Questo ambiente è memorizzato nel dizionario $_SERVER, una variabile globale PHP. Se utilizziamo la libreria [HttpFoundation], questo ambiente si troverà nel campo [Request→server], dove [Request] è la richiesta HTTP elaborata dallo script web.

17.8.1. Lo script server

Stiamo scrivendo un'applicazione server che invia il proprio ambiente di esecuzione ai propri client.

Image

Lo script web [env-server.php] è il seguente:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
// we retrieve the request
$request = Request::createFromGlobals();
// we work out the answer
$response = new Response();
// response content is json utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// set the jSON content of the response
$response->setContent(json_encode($request->server->all()));
// reply sent
$response->send();
  • riga 9: recuperiamo l'oggetto [Request], che incapsula tutte le informazioni disponibili sulla richiesta HTTP ricevuta dallo script web e sul suo ambiente di esecuzione;
  • righe 13–14: inviamo al client testo in chiaro con caratteri UTF-8;
  • riga 16: le informazioni inviate al client saranno una stringa ottenuta dalla serializzazione JSON dell'oggetto [$request→server→all()]: [$request→server] rappresenta l'ambiente di esecuzione dello script web. È un oggetto di tipo [ServerBag], una sorta di dizionario. [$request→server→all()] è un vero e proprio dizionario, contenente il contenuto del [ServerBag];
  • Riga 18: Le informazioni vengono inviate;

Se questo script viene eseguito da NetBeans, il browser visualizza la seguente pagina:

Image

  • in [2], le varie chiavi del dizionario dell'ambiente;
  • in [3], i valori di queste chiavi;

17.8.2. Lo script client

Image

Lo script client [env-client.php] è configurato dal seguente file JSON [config-env-client.json]:

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

Lo script client [env-client.php] è il seguente:


<?php
 
// server script environment
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-env-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // make a request to the server
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur\n";
  $env = json_decode($response->getContent());
  foreach ($env as $key => $value) {
    print "[$key]=>$value\n";
  }
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • riga 42: deserializza la risposta JSON dal server. Questo restituisce un hash;
  • righe 43–45: visualizza tutti i valori in questo array associativo;

Si ottiene il seguente output della 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

Ecco il significato di alcune delle variabili (per Windows. Su Linux sarebbero diverse):

HTTP_HOST
Il valore xxx dell'intestazione HTTP [Host: xxx] inviata dal client
HTTP_USER_AGENT
il valore xxx dell'intestazione HTTP [User_Agent: xxx] inviata dal client
HTTP_ACCEPT_ENCODING
il valore xxx dell'intestazione HTTP [Accept-Encoding: xxx] inviata dal client
PATH
il percorso degli eseguibili sulla macchina in cui è in esecuzione lo script del server
COMSPEC
il percorso del prompt dei comandi DOS
PATHEXT
estensioni dei file eseguibili
WINDIR
la cartella di installazione di Windows
SERVER_SIGNATURE
la firma del server web. Niente qui.
SERVER_SOFTWARE
il tipo di server web
SERVER_NAME
Il nome Internet del computer che ospita il server web
PORTA_SERVER
La porta di ascolto del server web
SERVER_ADDR
l'indirizzo IP del computer del server web, in questo caso 127.0.0.1
REMOTE_ADDR
l'indirizzo IP del client. In questo caso, il client si trovava sulla stessa macchina del server.
REMOTE_PORT
la porta di comunicazione del client
DOCUMENT_ROOT
la radice dell'albero di directory dei documenti serviti dal server web
REQUEST_SCHEME
il protocollo TCP della richiesta URL (http://localhost/php7/, ecc.)
SERVER_ADMIN
l'indirizzo e-mail dell'amministratore del server web
SCRIPT_FILENAME
il percorso completo dello script del server
REMOTE_PORT
la porta da cui il client ha effettuato la richiesta
SERVER_PROTOCOL
la versione del protocollo HTTP utilizzata dal server web
METODO_RICHIESTA
il metodo HTTP utilizzato dal client. Ce ne sono quattro: GET, POST, PUT, DELETE
QUERY_STRING
i parametri inviati con una richiesta GET /url?parametri
REQUEST_URI
L'URL richiesto dal client. Se il browser richiede l'URL http://machine[:port]/uri, REQUEST_URI sarà uri
SCRIPT_NAME
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME']

17.9. Recupero da parte del server dei parametri inviati da un client

17.9.1. Introduzione

Nel protocollo HTTP, un client dispone di due metodi per trasmettere parametri al server web:

  • richiede l'URL del servizio nel modulo

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

dove i valori validi devono essere prima codificati in modo che alcuni caratteri riservati siano sostituiti dai loro valori esadecimali;

  • richiede l'URL del servizio nella forma

POST url HTTP/1.0

quindi, tra le intestazioni HTTP inviate al server, è presente la seguente intestazione:

Content-length=N

Il resto delle intestazioni inviate dal client termina con una riga vuota. Può quindi inviare i propri dati nel formato

val1&param2=val2&param3=val3…

dove i valori validi devono, come nel metodo GET, essere codificati in anticipo. Il numero di caratteri inviati al server deve essere N, dove N è il valore dichiarato nell'intestazione

Content-length=N

Lo script PHP del servizio web che recupera i precedenti parametri parami inviati dal client ottiene i loro valori dall'array:

  • $_GET["parami"] per una richiesta GET;
  • $_POST["parami"] per una richiesta POST;

Questo vale per le funzioni PHP di base. Se si utilizza la libreria [HttpFoundation], questi parametri si trovano in:

  • [Request]->query->get('parami') per una richiesta GET;
  • [Request]->request->get('parami') per una richiesta POST;

dove [Request] rappresenta tutte le informazioni relative alla richiesta ricevuta dallo script web;

17.9.2. Il client GET – versione 1

Image

Gli script client vengono configurati utilizzando il seguente file 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"
}
  • riga 1: l'URL dello script web di destinazione per i client GET;
  • riga 2: l'URL dello script web di destinazione per i client POST;

I client GET inviano tre parametri [last_name, first_name, age] al server. Il client [parameters-get-client.php] è il seguente:


<?php
 
// client GET of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// information is encoded
  $parameters = "prenom=" . urlencode($prenom) .
    "&nom=" . urlencode($nom) .
    "&age=$age”;
  // query
  $response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • righe 33-35: codifica dei parametri inviati al server. I parametri [$first_name, $last_name], che possono contenere caratteri UTF-8, vengono codificati utilizzando la funzione [urlencode]. Tutti i caratteri non alfanumerici (come definiti dalle espressioni relazionali) vengono sostituiti da %xx, dove xx è il valore esadecimale del carattere. Gli spazi vengono sostituiti dal segno +;
  • riga 37: l'URL richiesto è $URL?$parameters, dove $parameters ha la forma name=val1&firstname=val2&age=val3;
  • riga 48: il client visualizzerà semplicemente la risposta del server;

Potresti essere curioso di vedere cosa riceve il server durante una richiesta GET parametrizzata. Per farlo, avviamo il nostro server generico [RawTcpServer] sulla porta 100 della macchina locale da un terminale Laragon (vedi sezione link):

Image

Verifica che in [4] ti trovi effettivamente nella cartella utilities.

Modifichiamo il file JSON [parameters-get-client.json] che configura i client 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"
}
  • Riga 2: Abbiamo modificato la porta del server web. Di conseguenza, verrà contattato [RawTcpServer];

Eseguiamo il client. Nella finestra [RawTcpServer] vediamo le seguenti informazioni:

Image

  • in [1], la richiesta GET inviata dal client. Possiamo vedere chiaramente la codifica di alcuni caratteri;

17.9.3. Il server GET / POST

Image

Lo script del server [parameters-server.php] è il seguente:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
// we retrieve the request
$request = Request::createFromGlobals();
// retrieve query parameters
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
 
// we work out the answer
$response = new Response();
// the content of the answer is utf-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// response content - an array encoded in jSON
$response->setContent(json_encode([
  "method" => $request->getMethod(),
  "uri" => $request->getRequestUri(),
  "getParameters" => $getParameters,
  "bodyParameters" => $bodyParameters
    ], JSON_UNESCAPED_UNICODE));
// reply sent
$response->send();

Commenti

  • riga 9: creazione dell'oggetto [Request] per lo script web. Questo oggetto racchiude tutte le informazioni che lo script web ha ricevuto dal client;
  • riga 11: l'oggetto [Request→query] è di tipo [ParameterBag] e raccoglie i parametri di qualsiasi operazione GET da un client. L'espressione [Request→query→get("X")] recupera il parametro denominato X dai parametri GET [name=val1&firstname=val2&age=val3]. L'espressione [Request→query→all()] recupera il dizionario dei parametri GET;
  • riga 12: l'oggetto [Request→request] è di tipo [ParameterBag] e contiene i parametri inviati come documento dal client al server. Questi parametri sono anche detti "caricati" perché appartengono a un documento che il client invia al server. L'espressione [Request→request→get("X")] recupera il parametro denominato X dai parametri caricati [last_name=val1&first_name=val2&age=val3]. L'espressione [Request→request→all()] recupera il dizionario dei parametri caricati;
  • righe 17–18: il client viene informato che riceverà JSON codificato in UTF-8;
  • righe 20–25: il server restituisce al client tutti i parametri ricevuti, il tipo di operazione [GET / POST / …] eseguita dal client e l'URI richiesto. Questo metodo si ottiene tramite l'espressione [$request→getMethod()]. Il documento inviato al client è la stringa JSON di un array associativo, alcuni dei cui valori sono a loro volta array associativi. Il parametro [JSON_UNESCAPED_UNICODE] garantisce che i caratteri Unicode (come ad esempio i caratteri accentati) vengano inviati così come sono e non codificati;
  • riga 27: la risposta viene inviata al client;

L'esecuzione dello script lato client produce i seguenti risultati:

---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":[]}]
  • riga 10:
    • [method]: il metodo è GET;
    • [uri]: i parametri codificati nell'URL della richiesta GET sono visibili nell'URI richiesto;
    • [getParameters]: l'array dei parametri GET;
    • [bodyParameters]: l'array dei parametri caricati: è vuoto;

17.9.4. Il client GET – versione 2

Nella versione precedente dello script client, abbiamo codificato noi stessi in URL i parametri inviati al server, a scopo didattico. L'oggetto [HttpClient] è in grado di gestire questa operazione autonomamente. Ecco lo script corrispondente [parameters-get-client-2.php]:


<?php
 
// client GET of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('GET', $config['url-get'],
    ["query" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • righe 33–37: aggiunta di parametri alla richiesta GET della riga 32. L'oggetto [HttpClient] gestirà autonomamente la codifica dell'URL;

17.9.5. Il client POST

Un client HTTP invia la seguente sequenza di testo al server web: intestazioni HTTP, riga vuota, documento. Nel client precedente, questa sequenza era la seguente:

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

Non c'era alcun documento. Esiste un altro modo per inviare i parametri, noto come metodo POST. In questo caso, la sequenza di testo inviata al server web è la seguente:

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

In questo caso, i parametri che erano inclusi nelle intestazioni HTTP per il client GET fanno parte del documento inviato dopo le intestazioni nel client POST.

Lo script del client POST [parameters-postclient.php] è il seguente:


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";

// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('POST', $config['url-post'],
    ["body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
  • riga 32: ora abbiamo una richiesta HTTP POST;
  • righe 33–37: i parametri POST sono chiamati corpo della richiesta POST: si tratta del documento inviato dal client al server. Qui vengono inviati tre parametri [last_name, first_name, age];
  • riga 48: visualizziamo la risposta JSON del server;

I risultati dell'esecuzione dello script client sono i seguenti:


---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"}}]
  • Riga 10: il metodo è [Post] e i parametri sono di tipo [bodyParameters]. Non ci sono [getParameters], come indicato dall'[uri];

Potresti essere curioso di vedere cosa riceve il server durante una richiesta POST. Per farlo, avviamo il nostro [RawTcpServer] generico sulla porta 100 della macchina locale da un terminale Laragon (vedi paragrafo collegato):

Image

Verifica che in [4] ti trovi effettivamente nella cartella utilities.

Modifichiamo il file JSON [config-parameters-client.json] che configura il client 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"
}
  • Riga 3: Abbiamo modificato la porta del server web. Pertanto, verrà contattato [RawTcpServer];

Eseguiamo il client. Nella finestra [RawTcpServer] vediamo le seguenti informazioni:

Image

  • in [6], la richiesta POST;
  • in [7]: l'intestazione HTTP [Content-Length] specifica il numero di byte nel documento che il client invierà al server. L'intestazione HTTP [Content-Type] specifica la natura di questo documento. Il tipo [application/x-www-form-urlencoded] indica testo codificato in URL;
  • in [8], la riga vuota che segna la fine delle intestazioni HTTP e l'inizio del documento di 44 byte. Ciò che lo screenshot non mostra è il documento stesso. Si tratta della stringa di parametri codificata in URL: [first_name=jean-paul&last_name=de+la+h%C3%BBche&age=45]. Il lettore può verificare che abbia effettivamente 44 caratteri;

17.9.6. Un client POST misto

In una richiesta POST, è possibile mescolare i parametri codificati nell'URL con quelli codificati nel documento inviato dal client dopo le intestazioni HTTP. Ecco un esempio [parameters-mixte-postclient.php]:


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('POST', $config['url-post'],
    [
      // document parameters (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parameters of URL (query)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • riga 32: una richiesta POST;
  • righe 40–45: parametri codificati nell'URL;
  • righe 35–39: parametri codificati nell'URL nel corpo della richiesta (body, document);

All'esecuzione, si ottiene il seguente output della 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"}}]
  • riga 10: possiamo vedere che il server è stato in grado di recuperare entrambi i tipi di parametri;

17.9.7. Un client GET misto

Proveremo a fare la stessa cosa di prima con una richiesta GET. Lo script [parameters-mixte-get-client.php] è il seguente:


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('GET', $config['url-post'],
    [
      // document parameters (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parameters of URL (query)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • riga 32: una richiesta POST;
  • righe 40–45: parametri codificati nell'URL;
  • righe 35–39: parametri codificati nell'URL nel corpo della richiesta (body, document);

All'esecuzione, si ottiene il seguente output della 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":[]}]
  • Riga 10: Possiamo vedere che il server non ha ricevuto alcun parametro codificato nell'URL nel documento inviato dal client. Quando esaminiamo le intestazioni HTTP inviate dal client, vediamo che ha effettivamente inviato un documento di 44 caratteri, ma il server non lo ha elaborato;

Quindi, quale metodo dovresti scegliere per inviare informazioni al server?

  • Il metodo [GET URL?param1=val1&param2=val2&…] utilizza un URL parametrizzato che può fungere da link. Questo è il suo principale vantaggio: l'utente può aggiungere tali link ai preferiti;
  • In altre applicazioni, potresti non voler visualizzare i parametri inviati al server in un URL. Per motivi di sicurezza, ad esempio. In tal caso, utilizzerai il metodo [POST] e includerai i parametri codificati nell'URL in un documento inviato al server;

17.10. Gestione delle sessioni web

Negli esempi client/server precedenti, il processo era il seguente:

  • il client apre una connessione alla porta 80 sul server web;
  • invia la sequenza di testo: intestazioni HTTP, riga vuota, [documento];
  • in risposta, il server invia una sequenza dello stesso tipo;
  • il server chiude la connessione con il client;
  • il client chiude la connessione al server;

Se lo stesso client effettua una nuova richiesta al server web poco dopo, viene stabilita una nuova connessione tra il client e il server. Il server non è in grado di distinguere se il client che si connette abbia già visitato il sito in precedenza o se si tratti di una richiesta effettuata per la prima volta. Tra una connessione e l’altra, il server “dimentica” il proprio client. Per questo motivo, il protocollo HTTP è definito un protocollo stateless. Tuttavia, è utile che il server ricordi i propri client. Ad esempio, se un'applicazione è sicura, il client invierà al server un nome utente e una password per autenticarsi. Se il server "dimenticasse" il proprio client tra una connessione e l'altra, il client dovrebbe autenticarsi ad ogni nuova connessione, il che non è fattibile.

Per tracciare un client, il server procede come segue: quando un client effettua una richiesta iniziale, il server include un identificatore nella sua risposta, che il client deve poi rispedire con ogni nuova richiesta. Grazie a questo identificatore, che è unico per ogni client, il server può riconoscere un client. Può quindi gestire una voce di memoria per quel client sotto forma di una voce di memoria associata in modo univoco all’identificatore del client.

Tecnicamente, ecco come funziona:

  • Nella risposta a un nuovo client, il server include l'intestazione HTTP Set-Cookie: Key=Identifier. Lo fa solo alla prima richiesta;
  • nelle richieste successive, il client invierà il proprio identificatore tramite l'intestazione HTTP Cookie: Key=Identifier in modo che il server possa riconoscerlo;

Ci si potrebbe chiedere come faccia il server a capire se si tratta di un nuovo client o di uno già noto. È la presenza dell'intestazione HTTP Cookie nelle intestazioni HTTP del client a fornirgli questa informazione. Nel caso di un nuovo client, tale intestazione è assente.

Tutte le connessioni provenienti da un determinato client vengono definite come una sessione.

17.10.1. Il file di configurazione [php.ini]

Affinché la gestione delle sessioni funzioni correttamente con PHP, è necessario verificare che sia configurato correttamente. Su Windows, il file di configurazione è php.ini. A seconda del contesto di esecuzione (console, web), il file di configurazione [php.ini] deve trovarsi in directory diverse. Per individuarle, utilizzare il seguente script:

1
2
3
4
<?php

// infos PHP
phpinfo();

Riga 4: La funzione phpinfo fornisce informazioni sull'interprete PHP che esegue lo script. In particolare, fornisce il percorso del file di configurazione [php.ini] in uso.

Abbiamo già utilizzato questo script in un ambiente console (vedi la sezione "link"). In un ambiente web, otteniamo il seguente risultato:

Image

  • In [1-2], il file [php.ini] che configura l'interprete dello script web. Questo file contiene una sezione dedicata alle sessioni:
[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
  • riga 2: i dati della sessione del client vengono salvati in un file;
  • riga 3: la directory in cui vengono salvati i dati di sessione. Se questa directory non esiste, non viene segnalato alcun errore e la gestione della sessione non funziona;
  • righe 4–6: indicano che l'ID di sessione è gestito dalle intestazioni HTTP Set-Cookie e Cookie;
  • riga 7: l'intestazione Set-Cookie avrà il formato Set-Cookie: PHPSESSID=session_id;
  • riga 8: una sessione client non viene avviata automaticamente. Lo script del server deve richiederla esplicitamente utilizzando la funzione session_start();
  • riga 9: il cookie di sessione rimane valido fino alla chiusura del browser del client;
  • riga 10: il percorso per il quale il cookie di sessione deve essere inviato. Se [session.cookie_path = /xxx], allora ogni volta che il browser richiede un URL del tipo [/xxx/yyy/zzz], deve inviare il cookie. Qui, il percorso [/] indica che il cookie deve essere inviato per ogni URL del sito;
  • riga 13: alcuni oggetti di sessione devono essere serializzati per poter essere memorizzati in un file. PHP gestisce questa serializzazione/deserializzazione utilizzando le funzioni [serialize / unserialize];
  • riga 16: periodo di timeout dopo il quale gli oggetti di sessione memorizzati nel file di sessione sono considerati obsoleti;
  • riga 19: durata della sessione. Dopo questo periodo, viene creata una nuova sessione e gli oggetti salvati nella sessione precedente vanno persi;

17.10.2. Esempio 1

17.10.2.1. Il server

Image

La gestione dell'ID di sessione è trasparente per un servizio web. Questo ID è gestito dal server web. Un servizio web accede alla sessione del client tramite la funzione session_start(). Da quel momento in poi, il servizio web può leggere/scrivere dati nella sessione del client tramite il dizionario $_SESSION. Se si utilizza la libreria [HttpFoundation], la sessione è disponibile tramite l'espressione [Request→getSession].

Il codice seguente [session-server.php] mostra la gestione basata su sessione di tre contatori. Ad ogni nuova richiesta, lo script web incrementa questi contatori e li memorizza nella sessione in modo che possano essere recuperati durante la richiesta successiva.


<?php
// dependencies
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;
 
//
// we retrieve the request
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// three counters are retrieved from the session
if ($session->has("N1")) {
  // n1 counter increment
  $session->set("N1", (int) $session->get("N1") + 1);
} else {
  // counter N1 is not in session - create it
  $session->set("N1", 0);
}
if ($session->has("N2")) {
  // n2 counter increment
  $session->set("N2", (int) $session->get("N2") + 1);
} else {
  // counter N2 is not in session - create it
  $session->set("N2", 10);
}
if ($session->has("N3")) {
  // n3 counter increment
  $session->set("N3", (int) $session->get("N3") + 1);
} else {
  // counter N3 is not in session - create it
  $session->set("N3", 100);
}
// we work out the answer
$response = new Response();
// the content of the answer is utf-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// the answer will be the jSON of an array containing the three counters
$response->setContent(json_encode([
  "N1" => $session->get("N1"),
  "N2" => $session->get("N2"),
  "N3" => $session->get("N3")]));
 
// reply sent
$response->send();
  • riga 10: l'oggetto [$request] incapsula tutte le informazioni relative alla richiesta ricevuta dallo script web;
  • righe 12-13: viene creata e attivata una sessione. L'oggetto [Session] incapsula i dati di sessione corrispondenti al cookie di sessione inviato dal client. Se il client non ha inviato tale cookie, nessun dato viene memorizzato in [Session]. Lo script web includerà l'intestazione HTTP [Set-Cookie: PHPSESSID=xxx] nella sua prima risposta. Nelle richieste successive, il client invierà l'intestazione HTTP [Cookie: PHPSESSID=xxx] per indicare la sessione di cui desidera utilizzare i contenuti. Una sessione è la memoria di un client;
  • riga 15: verifichiamo se la sessione ha una chiave denominata [N1]. Questo sarà il nome del nostro primo contatore. In caso contrario (riga 20), impostiamo il suo valore a 0 e lo aggiungiamo alla sessione. In caso contrario (riga 23), noi:
    • lo recuperiamo dalla sessione;
    • incrementiamo il suo valore di 1;
    • lo reinseriamo nella sessione;
  • righe 22–35: facciamo lo stesso per gli altri due contatori, N2 e N3;
  • righe 36–40: prepariamo una risposta di tipo [application/json];
  • righe 42–45: la risposta sarà la stringa JSON di un array contenente i tre contatori;
  • riga 48: inviamo la risposta al client;

Nella relazione client/server, la gestione della sessione client sul server dipende da entrambe le parti, il client e il server:

  • il server è responsabile dell'invio di un identificatore al client durante la sua prima richiesta
  • il client ha il compito di rispedire questo identificatore ad ogni nuova richiesta. Se non lo fa, il server supporrà che si tratti di un nuovo client e genererà un nuovo identificatore per una nuova sessione.

Risultati

Utilizziamo un browser web come client. Per impostazione predefinita (in realtà, per configurazione), il browser invia effettivamente al server gli identificatori di sessione che il server gli invia. Man mano che vengono effettuate le richieste, il browser riceverà i tre contatori inviati dal server e ne vedrà incrementarsi i valori.

Image

  • In [2], la prima richiesta al servizio web;
  • in [4], la quarta richiesta mostra che i contatori sono stati effettivamente incrementati. I valori dei contatori vengono effettivamente memorizzati nel corso delle richieste;

Utilizziamo la modalità sviluppatore per visualizzare le intestazioni HTTP scambiate tra il server e il client. Chiudiamo Firefox per terminare la sessione corrente con il server, lo riapriamo e attiviamo la modalità sviluppatore (F12). Ciò cancellerà la sessione corrente del browser, facendone avviare una nuova. Richiediamo il servizio [session-server.php]:

Image

In [5], vediamo l'ID di sessione inviato dal server nella sua risposta alla prima richiesta del client. Utilizza l'intestazione HTTP Set-Cookie.

Effettuiamo una nuova richiesta aggiornando (F5) la pagina nel browser web:

Image

Qui noteremo due cose:

  • In [11], il browser web rinvia l'ID di sessione con l'intestazione HTTP Cookie.
  • In [12], il servizio web non include più questo identificatore nella sua risposta. Ora spetta al client inviarlo con ciascuna delle sue richieste.

17.10.2.2. Il client

Ora scriveremo uno script lato client basato sullo script lato server precedente. Nella gestione della sessione, deve comportarsi come il browser web:

  • nella risposta del server alla sua prima richiesta, deve trovare l'ID di sessione che il server gli invia. Sa che lo troverà nell'intestazione HTTP Set-Cookie.
  • Per ogni richiesta successiva, deve inviare l'identificatore ricevuto al server. Lo farà utilizzando l'intestazione HTTP Cookie.

Image

Il client [session-client] è configurato dal seguente file JSON [config-session-client.json]:

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

Il codice client [session-client] è il seguente:


<?php
 
// session management
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-session-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // we'll make 10 requests
  for ($i = 0; $i < 10; $i++) {
    // make a request to the server
    if (!isset($sessionCookie)) {
      // no session
      $response = $httpClient->request('GET', $config['url']);
    } else {
      // with session
      $response = $httpClient->request('GET', $config['url'],
        ["headers" => ["Cookie" => $sessionCookie]]);
    }
    // answer status
    $statusCode = $response->getStatusCode();
    print "---Réponse avec statut : $statusCode\n";
    // we retrieve the headers
    print "---Entêtes de la réponse\n";
    $headers = $response->getHeaders();
    foreach ($headers as $type => $value) {
      print "$type: " . $value[0] . "\n";
    }
    // retrieve the session cookie if it exists
    if (isset($headers["set-cookie"])) {
      // session cookie ?
      foreach ($headers["set-cookie"] as $cookie) {
        $match = [];
        $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
        if ($match) {
          $sessionCookie = "PHPSESSID=" . $champs[1];
        }
      }
    }
  }
  // the jSON server response is displayed
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
 
 

Commenti

  • riga 27: creazione del client HTTP;
  • riga 30: invieremo la stessa richiesta al server [session-server.php] 10 volte;
  • riga 32: la variabile [$sessionCookie] verrà impostata sul valore dell'intestazione HTTP [Set-Cookie] ricevuta dal client;
  • righe 32–34: se questa variabile non esiste, significa che la sessione non è ancora iniziata. Inviamo la richiesta [GET] senza l'intestazione [Cookie];
  • righe 35–38: altrimenti, la sessione è iniziata e inviamo la richiesta [GET] con l'intestazione [Cookie]. Il valore di questa intestazione sarà [$sessionCookie];
  • riga 50: se l'intestazione [Set-Cookie] è tra le intestazioni HTTP ricevute, allora cerchiamo il cookie di sessione;
  • riga 52: il server web può inviare più intestazioni [Set-Cookie]. Il cookie di sessione è solo uno di questi. Nel nostro esempio, ha il formato specifico [PHPSESSID=xxx;;]
  • righe 53–57: viene utilizzata un'espressione regolare per trovare il cookie di sessione;
  • riga 62: una volta effettuate le 10 richieste, visualizziamo l'ultima risposta JSON dal server;

Risultati

L'esecuzione dello script client fa sì che nella console di NetBeans venga visualizzato quanto segue:


"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}
  • Riga 8: Nella sua prima risposta, il server invia l'ID di sessione. Nelle risposte successive, non lo invia più;
  • riga 41: i tre contatori [N1, N2, N3] sono stati effettivamente incrementati 9 volte. Durante la richiesta n. 1, sono stati azzerati;

L'esempio seguente mostra che è possibile salvare nella sessione anche i valori di un array o di un oggetto.

17.10.3. Esempio 2

17.10.3.1. Il server

Image

Inseriremo un oggetto [Person] nella sessione. La definizione di questa classe è la seguente:


<?php
 
namespace Modèles;
 
class Personne implements \JsonSerializable {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
 
  // convert associative array to object [Person]
  public function setFromArray(array $assoc): Personne {
    // initialize the current object with the associative array
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // result
    return $this;
  }
 
  // getters and 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]";
  }
 
  // implements the JsonSerializable interface
  public function jsonSerialize(): array {
    // render an associative array with the object's attributes as keys
    // this table can then be encoded as jSON
    return get_object_vars($this);
  }
 
  // convert a jSON to a [Person] object
  public static function jsonUnserialize(string $json): Personne {
    // we create a person from the string jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }
 
}

Lo script del server sarà il seguente:


<?php
 
// dependencies
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;
 
//
// retrieve the current query
$request = Request::createFromGlobals();
 
// session
$session = new Session();
$session->start();
 
// retrieve various data from the session
// table
if ($session->has("tableau")) {
  // the array is in the session - all its values are incremented
  $tableau = $session->get("tableau");
  for ($i = 0; $i < count($tableau); $i++) {
    $tableau[$i] += 1;
  }
  // put the table back in the session
  $session->set("tableau", $tableau);
} else {
  // the array is not in the session - we create it
  $tableau = [0, 10, 100];
  // we put it in the session
  $session->set("tableau", $tableau);
}
// dictionary
if ($session->has("assoc")) {
  // [assoc] is in the session - all its elements are incremented
  $assoc = $session->get("assoc");
  foreach ($assoc as $key => $value) {
    $assoc[$key] = $value + 1;
  }
  // put $assoc in the session
  $session->set("assoc", $assoc);
} else {
  // [assoc] is not in the session - we create it
  $assoc = ["un" => 0, "deux" => 10, "trois" => 100];
  // put $assoc in the session
  $session->set("assoc", $assoc);
}
// object Person
if ($session->has("personne")) {
  // [person] is in the session - his age is incremented
  $personne = $session->get("personne");
  $personne->setÂge($personne->getÂge() + 1);
} else {
  // [person] is not in the session - we create it
  $personne = (new Personne())->setFromArray(
    ["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
  // put $personne in the session
  $session->set("personne", $personne);
}
// we work out the answer
$response = new Response();
// the content of the response is 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));
 
// reply sent
$response->send();

Commenti

  • righe 16-17: recuperiamo la sessione corrente e la attiviamo;
  • righe 21-34: gestiamo un array [array] memorizzato nella sessione. Ad ogni nuova richiesta, i suoi elementi vengono incrementati di 1;
  • righe 36-49: gestiamo un array associativo [assoc] memorizzato nella sessione. Ad ogni nuova richiesta, i suoi elementi vengono incrementati di 1;
  • righe 51–61: gestiamo un oggetto associato alla sessione [Person]. Ad ogni nuova richiesta, l'età di questa persona viene incrementata di 1;
  • righe 62–73: viene inviata una risposta JSON al client: la stringa JSON di un array associativo;

Eseguiamo questo script da NetBeans. Le prime due richieste producono i seguenti risultati (premere F5 nel browser per la seconda):

Image

  • vediamo che in [6-8] tutti i contatori sono stati incrementati;

17.10.3.2. Il client

Image

Il client è lo stesso dell'Esempio 1 (sezione link). Modifichiamo solo il suo file di configurazione [config-session-client]:


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

L'esecuzione produce i seguenti risultati:


---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}}
  • riga [22], possiamo vedere che tutti i contatori sono stati incrementati;

17.11. Autenticazione

Ci concentreremo ora sui servizi web destinati esclusivamente a utenti specifici. Il client deve quindi autenticarsi presso il servizio web prima di ricevere una risposta.

17.11.1. Il client

Image

Il codice del client [auth-client.php] è il seguente:


<?php
 
// session management
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-auth-client.json";
 
// we retrieve the configuration
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;
}
 
// create a HTTP customer
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    // "verify_peer" => false,
    // "verify_host" => false
  ]);
 
 
try {
  // make a request to the server
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // the jSON server response is displayed
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

Commenti

  • righe 27–31: abbiamo passato un parametro al metodo statico [HttpClient::create], un hash;
  • riga 28: la chiave [auth_basic] ha come valore un array a due elementi [user, password]. Il client utilizzerà questi elementi per autenticarsi con il servizio web. La chiave [auth_basic] si riferisce a un tipo di autenticazione chiamato [Basic Authorization], che prende il nome dall'intestazione HTTP che il client invierà. Esistono altri tipi di autenticazione;
  • A parte questo codice, il client è identico a quelli precedenti;

Per visualizzare le intestazioni HTTP inviate dal client, lo collegheremo al server TCP generico [RawTcpServer] come abbiamo fatto molte volte in precedenza:

Image

Avviamo il client con la seguente configurazione [config-auth-client.json]:


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

Il [RawTcpServer] riceve quindi le seguenti righe:

Image

  • In [5], vediamo l'intestazione [Authorization: Basic XXX] inviata dal client. La stringa XXX è la stringa [user:password] codificata in Base64;

Per verificarlo, è possibile decodificare la stringa ricevuta sul sito web [https://www.base64decode.org/]:

Image

17.11.2. Il server

Image

Il server [auth-server.php] è il seguente:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
 
// authorized users
$users = ["admin" => "admin"];
//
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// answer preparation
$response = new Response();
// set the response status code
if (!$trouvé) {
  // not found - code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
  // found - code 200
  $response->setStatusCode(Response::HTTP_OK);
}
// response has no content, only HTTP headers
$response->send();

Commenti

  • riga 9: utenti autorizzati, in questo caso un singolo utente con login [admin] e password [admin];
  • riga 14: l'ID dell'utente viene recuperato dall'intestazione [PHP-AUTH-USER]. Non si tratta di un'intestazione inviata dal client, ma di una costruita dal PHP del server;
  • riga 15: la password dell'utente viene recuperata dall'intestazione [PHP-AUTH-PW], un'intestazione creata da PHP;
  • riga 17: cerchiamo l'utente che sta tentando di effettuare l'accesso nell'elenco degli utenti autorizzati;
  • righe 23–24: se l'utente non viene riconosciuto, al client viene inviato quanto segue
    • riga 23: il codice di stato [401 Unauthorized];
    • riga 24: un'intestazione [WWW-Authenticate: Basic realm=”something”]. La maggior parte dei browser riconosce questa intestazione e visualizzerà una finestra di autenticazione che richiede all'utente di effettuare l'accesso. Le intestazioni HTTP devono essere codificate in ISO 8859-1. Il testo di NetBeans, tuttavia, è codificato in UTF-8. La funzione [utf8_decode] gestisce la conversione da UTF-8 a ISO 8859-1. In questo caso non era necessaria perché i caratteri nella stringa [PHP7 in questo esempio] sono gli stessi sia in UTF-8 che in ISO 8859-1. La funzione è inclusa esclusivamente come promemoria della codifica utilizzata dalle intestazioni HTTP;
  • riga 25: se l'utente è stato riconosciuto, inviamo al client il codice [200 OK];

Richiediamo l'URL [auth-server.php] utilizzando un browser:

Image

Notiamo che il browser visualizza una finestra di autenticazione. In [2], vediamo il valore dell'intestazione [WWW-Authenticate] inviata dal server. Se osserviamo le intestazioni HTTP ricevute dal browser, troviamo quanto segue:

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
  • Riga 1: il codice di stato della risposta [401 Non autorizzato];
  • riga 6: l'intestazione HTTP [WWW-Authenticate];
  • riga 7: il corpo della risposta è vuoto;

Se, nei punti [3-4], digiti [admin] due volte, la risposta del server è la seguente:

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
  • riga 1: il codice di risposta 200 OK;
  • riga 6: il corpo della risposta è vuoto;

Se, nei punti [3-4], vengono inserite credenziali errate, il browser [Firefox] utilizzato per il test continua a visualizzare la finestra di autenticazione fino a quando non vengono inserite le credenziali corrette. Ogni volta che si verifica un round trip al server, viene ricevuta la stessa risposta, che fa apparire la finestra di autenticazione del browser.

Eseguiamo il client [auth-client.php] con un utente non autorizzato. La risposta del server è la seguente:


---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".
  • In [1], il client ha effettivamente ricevuto un codice 401;
  • in [3], è stata generata un'eccezione nel client. È stato Symfony [HttpClient] a generarla: genera un'eccezione quando il codice di stato della risposta HTTP indica un errore lato server e il client tenta di leggere le intestazioni o il contenuto della risposta del server. Il messaggio alla riga 3 mostra che il server ha risposto con [HTTP/1.0 401 Unauthorized] per indicare che l'utente non è stato riconosciuto;

Ora eseguiamo il client [auth-client.php] con l'utente autorizzato [‘admin’,’admin’]. La risposta del server è quindi la seguente:


---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 :
 
  • riga 1: il server ha risposto [HTTP/1.2 200 OK];
  • riga 7: la risposta non ha contenuto (0 byte);

17.11.3. Protezione della connessione client/server

Abbiamo visto che per autenticarsi con il server, il client invia l'intestazione:

authorization: Basic YWRtaW46YWRtaW4=

Se questa riga viene intercettata da uno spyware, è possibile recuperare facilmente le credenziali [nome utente, password] codificate in Base64 all'interno della stringa [YWRtaW46YWRtaW4=]. Per questo motivo, l'autenticazione deve avvenire tramite una connessione sicura tra il client e il server. Gli URL sicuri utilizzano il protocollo [HTTPS] anziché il protocollo HTTP. Il protocollo [HTTPS] è il protocollo HTTP all'interno di una connessione client/server sicura. Gli URL sicuri hanno il formato [https://chemin_document].

Non tutti i server web accettano URL in questo formato. Devono essere modificati per essere sicuri. Il server Apache di Laragon è un server sicuro, ma il protocollo HTTPS non è abilitato di default. È necessario abilitarlo nel menu di Laragon:

Image

  • in [4], abilitare la crittografia SSL per il server Apache;

Una volta fatto ciò, il server Apache viene riavviato automaticamente:

Image

  • in [1], compare un lucchetto verde: ciò indica che il protocollo HTTPS è stato abilitato;
  • in [2], compare una nuova porta di servizio, in questo caso la porta 443. Questa è la porta di servizio per il protocollo HTTPS sicuro;

Ora che disponiamo di un server sicuro, modifichiamo il file di configurazione del client [config-auth-client.json] come segue:


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

In [2], il protocollo è stato modificato in [https] e la porta in [443].

Ora eseguiamo il client [auth-client.php] con l'utente autorizzato [admin, admin]. L'output della console è il seguente:

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".

Il client [HttpClient] di Symfony ha generato un'eccezione perché il server gli ha inviato un certificato di fiducia che [HttpClient] non ha accettato. La comunicazione SSL si basa su certificati di fiducia emessi da autorità ufficiali. Quando abbiamo abilitato il protocollo HTTPS sul server Apache di Laragon, è stato generato un certificato autofirmato per il server Apache. Un certificato autofirmato è un certificato che non è stato convalidato da un'autorità ufficiale. Il client [HttpClient] di Symfony ha rifiutato questo certificato autofirmato.

È possibile indicare a [HttpClient] di non verificare la validità del certificato inviato dal server. Ciò si ottiene utilizzando le opzioni nel metodo [HttpClient::create]:


// on crée un client HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    "verify_peer" => false
  ]);

La riga 4 specifica che il certificato del server non deve essere verificato. Avevamo già riscontrato questo problema nello script [http-02.php] nella sezione collegata. Quello script utilizzava la libreria [libcurl] per connettersi a siti HTTP e HTTPS. Per quella libreria avevamo utilizzato la seguente configurazione:


// Initialisation d'a cURL session
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // il y a eu une erreur
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // options de curl
  $options = [
    // mode verbose
    CURLOPT_VERBOSE => true,
    // nouvelle connexion - pas de cache
    CURLOPT_FRESH_CONNECT => true,
    // timeout de la requête (en secondes)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // ne pas vérifier la validité des certificats SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // suivre les redirections
    CURLOPT_FOLLOWLOCATION => true,
    // récupération du document demandé sous la forme d'a character string
    CURLOPT_RETURNTRANSFER => true
  ];
 
  // paramétrage de curl
curl_setopt_array($curl, $options);

Riga 17: La costante [CURLOPT_SSL_VERIFYPEER] controlla se verificare o meno il certificato inviato dal server. Il client [HttpClient] è in realtà un client [curl] quando l'estensione [curl] è abilitata nella configurazione PHP, come in questo caso. La classe istanziata da [HttpClient::create] è quindi la classe [CurlHttpClient]. Le costanti [curl] sono disponibili in questa classe ma con nomi diversi:

$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 => '', // Keep track of cookies during redirects
            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'],
        ];

Abbiamo evidenziato in giallo le costanti utilizzate da [CurlHttpClient].

Se ora eseguiamo il client [auth-client] con l'utente [admin, admin], otteniamo il seguente risultato:


---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 :

L'utente è stato autenticato con successo. Se eseguiamo il client [auth-client] con un utente diverso da [admin, admin], otteniamo il seguente risultato:

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".

Ora sappiamo come effettuare l'autenticazione con un server sicuro.