PSR-7 Http Message del PHP-FIG propone interfacce php standard per rappresentare i messaggi Http di Request e Response oltre ad analizzarne il contenuto.

HTTP messages are the foundation of web development.

Standardizzare con questo aspetto è importantissimo poiché i messaggi Http sono le fondamenta dello sviluppo web. I browser ed i client come cURL inviano richieste Http al server che fornisce un’adeguata risposta Http.

Di seguito analizzeremo le interfacce proposte dal PSR-7 che rappresentano astrazioni dei messaggi Http e degli elementi che li compongono. Per il significato delle parole chiave presenti nell’articolo si rimanda alle note dell’articolo sul PSR-1.

Http Request

Iniziamo esaminando il contenuto di una Http Request.

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

La prima riga, detta “request line”, contiene il metodo della richiesta Http, il target della richiesta Http, solitamente una URI assoluta o un percorso sul server web, la versione del protocollo Http utilizzato. A questo seguono una o più intestazioni Http, una riga vuota ed il corpo del messaggio.

Http Response

Esaminiamo ora il contenuto di una Http Response.

HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body

La prima riga di una Http Response, detta “status line”, contiene la versione del protocollo Http, il codice di stato Http ed una descrizione comprensibile del codice di stato. Come per il messaggio di richiesta, questa riga è seguita da una o più intestazioni Http, una riga vuota ed il corpo del messaggio.

PSR-7: specifiche

Messaggi Http

Un messaggio Http è una richiesta da un client ad un server o una risposta da un server ad un client. PSR-7 definisce le rispettive interfacce per i messaggi Http: Psr\Http\Message\RequestInterface e Psr\Http\Message\ResponseInterface.

Entrambe estendono Psr\Http\Message\MessageInterface.
Mentre Psr\Http\Message\MessageInterface PUÒ essere implementato direttamente, gli implementatori DOVREBBERO implementare Psr\Http\Message\RequestInterface e Psr\Http\Message\ResponseInterface.
Per migliorare la lettura dell’articolo, da qui in avanti verrà omesso il namespace Psr\Http\Message per riferirsi a queste interfacce.

HTTP Headers

Nomi case-insensitive nei campi di intestazione.

I messaggi Http possono includere vari campi nell’intestazione (header) senza distinzione tra maiuscole e minuscole. Allo stesso modo dovranno essere recuperate dalle classi che implementano MessageInterface.
Ad esempio, il recupero dell’intestazione foo restituirà lo stesso risultato del recupero dell’intestazione FoO. Allo stesso modo, l’impostazione dell’intestazione Foo sovrascriverà qualsiasi valore di intestazione foo precedentemente impostato.

$message = $message->withHeader('foo', 'bar');

echo $message->getHeaderLine('foo');
// Outputs: bar

echo $message->getHeaderLine('FOO');
// Outputs: bar

$message = $message->withHeader('fOO', 'baz');

echo $message->getHeaderLine('foo');
// Outputs: baz

Nonostante le intestazioni possano essere recuperate senza distinzione tra maiuscole e minuscole, il case originale DEVE essere rispettato nell’implementazione, in particolar modo quando viene recuperato con getHeaders(). Questo perché applicazioni Http non conformi possono dipendere da un determinato caso.

Campi di intestazione con valori multipli

Per gestire campi di intestazione con più valori e fornire una migliore flessibilità nella gestione, le intestazioni possono essere recuperate da un’istanza di MessageInterface come array o stringa. Nel primo caso useremo getHeader(), nel secondo getHeaderLine().

$message = $message
    ->withHeader('foo', 'bar')
    ->withAddedHeader('foo', 'baz');

$header = $message->getHeaderLine('foo');
// $header contiene: 'bar,baz'

$header = $message->getHeader('foo');
// ['bar', 'baz']

Da notare che non tutti i valori di intestazione possono essere concatenati con la virgola (es: Set-Cookie). In questo caso gli utilizzatori DOVREBBERO usare metodo getHeader() per recuperare tali intestazioni multivalore.

Intestazione “Host”

Nelle richieste, l’intestazione Host riporta l’host dell’URI, nonché l’host utilizzato per stabilire la connessione Tcp. Le specifiche del protocollo Http consentono però all’intestazione Host di differire da entrambi. Le implementazioni, se non viene fornita alcuna intestazione Host, DEVONO tentare di impostarla da un URI predefinito .

RequestInterface :: withUri (UriInterface $uri, $preserveHost = false), per impostazione predefinita, sostituirà l’intestazione Host della richiesta restituita con quella dell’oggetto di tipo UriInterface passatogli. Per mantenere il valore originale basterà passare true al secondo argomento ($preservHost). Ciò manterrà invariata l’intestazione Host a patto che il messaggio ne contenga una.

Questa tabella mostra cosa restituirà getHeaderLin('Host') per una richiesta restituita da withUri() con l’argomento $preservHost impostato su true in varie richieste iniziali e URI.

Request Host header1 Request host component2 URI host component3 Result
’’ ’’ ’’ ’’
’’ foo.com ’’ foo.com
’’ foo.com bar.com foo.com
foo.com ’’ bar.com foo.com
foo.com bar.com baz.com foo.com
  1. Valore dell’intestazione host prima dell’operazione.
  2. Componente host dell’URI composto nella richiesta prima dell’operazione.
  3. Componente host dell’URI settato tramite withUri().

Streams

I messaggi Http sono costituiti da una riga iniziale, alcune intestazioni ed un corpo che può essere anche molto grande. Rappresentare il body di un messaggio come una stringa potrebbe consumare molta memoria fino a precludere la possibiltà di lavorare con body di grandi dimensioni. StreamInterface viene utilizzato proprio per nascondere i dettagli di implementazione quando un flusso di dati viene letto o scritto. Per le situazioni in cui una stringa sarebbe un’implementazione del messaggio appropriata, è possibile utilizzare flussi incorporati come php://memory e php://temp.

StreamInterface espone diversi metodi che consentono di leggere, scrivere e scorrere gli stream in modo efficace.

Gli stram espongono le loro capacità utilizzando tre metodi: isReadable(), isWritable() e isSeekable(). Gli oggetti che vogliono utilizzare uno stream possono utilizzare questi metodi per determinare se è in grado di soddisfare determinati requisiti.

Ogni istanza di uno stream avrà diverse funzionalità: può essere di sola lettura, di sola scrittura o di lettura/scrittura. Può consentire un accesso casuale arbitrario (ricerca in avanti o indietro in qualsiasi posizione) o solo accesso sequenziale come nel caso di socket, pipe, o callback-based stream.

Infine, StreamInterface definisce un metodo __toString() per recuperare o inviare l’intero contenuto del corpo in una sola volta.

A differenza delle interfacce di Request e Response, StreamInterface interagisce con gli stream che, per loro natura, possono mutare durante il loro utilizzo. La raccomandazione di Php-Fig è di utilizzare flussi di sola lettura per le richieste lato server e le risposte lato client.

Target della richiesta ed URI

Secondo lo standard RFC 7230 i messaggi di richiesta contengono uno dei seguenti tipi di “target”:

  • origin-form: costituito da percorso e, se presente, stringa della query. È solitamente definito come un URL relativo. I messaggi trasmessi via Tcp sono tipicamente di tipo origin-form; i dati di scheme e authority sono presenti tramite variabili CGI.
  • absolute-form: costituito da scheme, authority (“[user-info@]host[:port]”, dove gli elementi tra parentesi sono facoltativi), percorso (se presente), stringa della query (se presente) e fragment ( se presente). È solitamente definito come un URI assoluto ed è l’unico che consente di specificare una URI come descritto nel RFC 3986. Questo modulo è comunemente usato quando si effettuano richieste ai proxy Http
  • authority-form: costituito dalla sola authority. È solitamente utilizzato nelle CONNECT Request per stabilire una connessione tra un client Http e un server proxy.
  • asterisk-form: costituito unicamente dalla stringa asterisco ( ” * ” ). È solitamente utilizzato con il metodo OPTIONS per determinare le capacità generali di un server web.

Oltre a questi target di richiesta, c’è spesso un “URL effettivo” che è separato dal target di richiesta. L’URL effettivo non viene trasmesso all’interno di un messaggio Http, ma viene utilizzato per determinare il protocollo (http / https), la porta e l’host-name per effettuare la richiesta.

L’URL effettivo è rappresentato da UriInterface. UriInterface rappresenta gli URI Http e Https fornendo i metodi per interagire con le varie parti dell’URI evitendo un’analisi ripetuta dell’URI. Specifica inoltre un metodo __toString() per rappresentare la URI come stringa.

Quando si recupera la destinazione della richiesta con getRequestTarget(), per impostazione predefinita questo metodo utilizzerà l’oggetto URI ed estrarrà tutti i componenti necessari per costruire l’origin-form che è il request-target più comune.

Se si desidera utilizzare uno degli altri tre tipi di target o se l’utente desidera sovrascrivere esplicitamente il target della richiesta si utilizzerà il metodo withRequestTarget(). La chiamata a questo metodo non influisce sull’URI, poiché viene restituito da getUri().

Ad esempio, un utente potrebbe voler effettuare una richiesta in formato asterisco a un server:

$request = $request
    ->withMethod('OPTIONS')
    ->withRequestTarget('*')
    ->withUri(new Uri('https://example.org/'));

L’esempio creerebbe questo tipo di richiesta Http:

OPZIONI * HTTP / 1.1

Il client Http sarà poi in grado di utilizzare l’URL effettivo (getUri()) per determinare il protocollo, l’host-name e la porta Tcp.

Un client Http DEVE ignorare i valori di Uri::getPath() e Uri::getQuery() ed utilizzare il valore restituito da getRequestTarget() che concatena questi due valori.

I client che scelgono di non implementare 1 o più dei 4 request-target form, DEVONO comunque utilizzare getRequestTarget(). Questi client DEVONO rifiutare request-target che non supportano e NON DEVONO utilizzare i valori di getUri().

RequestInterface fornisce metodi per recuperare il target della richiesta o creare una nuova istanza specificando il target. Per impostazione predefinita, se non viene specificato il request-target, getRequestTarget() restituirà l’origin-form della URI composta o ” / ” se non è stata composta una URI. withRequestTarget($requestTarget) crea una nuova istanza con il request-target specificato ed è utile per creare connessioni verso un server.

Richieste Server-side

  • RequestInterface permette di rappresentare un messaggio di richiesta Http, ma le richieste lato server side richiedono un’attenzione particolare. I processi server-side devono tener conto della Common Gateway Interface (CGI) e dell’interazione di PHP con essa attraverso le sue via Server API (SAPI) . PHP ci fornisce una semplificazione per la gestione degli input attraverso variabili superglobali:
  • $_COOKIE: deserializza e fornisce un accesso ai cookie Http.
  • $_GET: deserializza e fornisce un accesso agli argomenti della query string.
  • $_POST: deserializza e fornisce un accesso per i parametri inviati tramite. Post Http. Può essere considerato il risultato dell’analisi del message body.
  • $_FILES: fornisce metadati relativi ai caricamenti dei file.
  • $_SERVER: fornisce l’accesso alle variabili d’ambiente CGI / SAPI che includono il metodo di richiesta, lo schema di richiesta, l’URI della richiesta e le intestazioni.

ServerRequestInterface estende RequestInterface per fornire un’astrazione su questi variabili superglobali riducendo l’accoppiamento.

La richiesta server-side fornisce una proprietà aggiuntiva, gli “attributi”, che consentono di esaminare, scompore e cercare match in modo da interpretare la richiesta in base alle specifiche necessità nell’applicativo.

Upload dei file

ServerRequestInterface specifica un metodo per recuperare una rappresentazione dei file caricati attraverso una struttura normalizzata in cui ogni elemento è un’istanza di UploadedFileInterface.

$_FILES ha alcuni problemi noti, ad esempio se si tenta di inviare da un form molteplici file attraverso campi di input aventi name “files”, l’array risultante verrà così rappresentato da PHP:

array(
    'files' => array(
        'name' => array(
            0 => 'file0.txt',
            1 => 'file1.html',
        ),
        'type' => array(
            0 => 'text/plain',
            1 => 'text/html',
        ),
        /* etc. */
    ),
)

Mentre ci si aspetterebbe una rappresentazione di questo tipo:

array(
    'files' => array(
        0 => array(
            'name' => 'file0.txt',
            'type' => 'text/plain',
            /* etc. */
        ),
        1 => array(
            'name' => 'file1.html',
            'type' => 'text/html',
            /* etc. */
        ),
    ),
)

Gli utilizzatori devono conoscere quindi i dettagli dell’implementazione nel linguaggio PHP e scrivere il codice per interpretare correttamente i dati di un determinato caricamento.

In alcuni casi poi $_FILES non viene popolato con i dati relativi ai file caricati:

  • Quando il metodo Http non è POST.
  • Quando si esegue una unit test.
  • Quando si opera in un ambiente non SAPI come ad esempio ReactPHP.

In questi casi si dovranno utilizzare approcci diversi, ad esempio:

  • Un processo potrebbe analizzare il message body per scoprire i file caricati.
  • Negli scenari di unit test, gli sviluppatori devono essere in grado di bloccare e/o simulare i metadati di caricamento del file per convalidare e verificare i diversi scenari.

getUploadedFiles() fornisce una struttura normalizzata e l’implementazione dovrebbe:

  • Aggregare tutte le informazioni per un determinato caricamento di file e usarle per popolare un’istanza Psr\Http\Message\UploadedFileInterface.
  • Ricreare la struttura ad albero inviata in cui ogni foglia rappresenti un’istanza di Psr\Http\Message\UploadedFileInterface appropriata.
  • La struttura ad albero dovrebbe seguire la struttura dei nomi in cui sono stati inviati i file.

Vediamo un esempio con un singolo campo di input con un “name semplice”:

<input type="file" name="avatar" />

In questo caso, la struttura di $ _FILES sarebbe:

array(
    'avatar' => array(
        'tmp_name' => 'phpUxcOty',
        'name' => 'my-avatar.png',
        'size' => 90996,
        'type' => 'image/png',
        'error' => 0,
    ),
)

Mentre la forma normalizzata restituita da getUploadedFiles() potrebbe essere:

array(
    'avatar' => /* UploadedFileInterface instance */
)

Vediamo invece un caso di campo di input che utilizza crea un array nell’attributo nome:

<input type="file" name="my-form[details][avatar]" />

In questo caso il contenuto di $_FILES sarà

array (
    'my-form' => array (
        'name' => array (
            'details' => array (
                'avatar' => 'my-avatar.png',
            ),
        ),
        'type' => array (
            'details' => array (
                'avatar' => 'image/png',
            ),
        ),
        'tmp_name' => array (
            'details' => array (
                'avatar' => 'phpmFLrzD',
            ),
        ),
        'error' => array (
            'details' => array (
                'avatar' => 0,
            ),
        ),
        'size' => array (
            'details' => array (
                'avatar' => 90996,
            ),
        ),
    ),
)

E l’albero corrispondente restituito da getUploadedFiles() dovrebbe essere:

array(
    'my-form' => array(
        'details' => array(
            'avatar' => /* UploadedFileInterface instance */
        ),
    ),
)

Vediamo ora il caso di un array di file:

Upload an avatar: <input type="file" name="my-form[details][avatars][]" />
Upload an avatar: <input type="file" name="my-form[details][avatars][]" />

Ecco cosa conterrà $_FILES:

array (
    'my-form' => array (
        'name' => array (
            'details' => array (
                'avatar' => array (
                    0 => 'my-avatar.png',
                    1 => 'my-avatar2.png',
                    2 => 'my-avatar3.png',
                ),
            ),
        ),
        'type' => array (
            'details' => array (
                'avatar' => array (
                    0 => 'image/png',
                    1 => 'image/png',
                    2 => 'image/png',
                ),
            ),
        ),
        'tmp_name' => array (
            'details' => array (
                'avatar' => array (
                    0 => 'phpmFLrzD',
                    1 => 'phpV2pBil',
                    2 => 'php8RUG8v',
                ),
            ),
        ),
        'error' => array (
            'details' => array (
                'avatar' => array (
                    0 => 0,
                    1 => 0,
                    2 => 0,
                ),
            ),
        ),
        'size' => array (
            'details' => array (
                'avatar' => array (
                    0 => 90996,
                    1 => 90996,
                    3 => 90996,
                ),
            ),
        ),
    ),
)

L’array sopra riportato corrisponderebbe alla seguente struttura restituita da getUploadedFiles():

array(
    'my-form' => array(
        'details' => array(
            'avatars' => array(
                0 => /* UploadedFileInterface instance */,
                1 => /* UploadedFileInterface instance */,
                2 => /* UploadedFileInterface instance */,
            ),
        ),
    ),
)

Per accedere all’elemento [1]:

$request->getUploadedFiles()['my-form']['details']['avatars'][1];

Seguendo il nostro esempio avremo:

$file0 = $request->getUploadedFiles()['files'][0];
$file1 = $request->getUploadedFiles()['files'][1];

printf(
    "Received the files %s and %s",
    $file0->getClientFilename(),
    $file1->getClientFilename()
);

Poiché le inforazioni sui file caricati, oltre che da $_FILES, possono derivare da altri elementi della richiesta, è presente il metodo withUploadedFiles() che consente di delegare la normalizzazione ad un altro processo.

Lo standard PSR-7 considera anche le implementazioni in ambienti non SAPI. Pertanto, UploadedFileInterface fornisce metodi per garantire che le operazioni funzionino indipendentemente dall’ambiente. In particolare:

  • moveTo($targetPath) viene fornito come alternativa sicura e consigliata per richiamare move_uploaded_file() sul file di caricamento temporaneo. Le implementazioni rileveranno il corretto funzionamento da utilizzare in base all’ambiente.
  • getStream() restituirà un’istanza StreamInterface. In ambienti non SAPI, una possibilità proposta è quella di eseguire il parsing dei file caricati nello stream php://temp.

Ad esempio:

// Move a file to an upload directory
$filename = sprintf(
    '%s.%s',
    create_uuid(),
    pathinfo($file0->getClientFilename(), PATHINFO_EXTENSION)
);
$file0->moveTo(DATA_DIR . '/' . $filename);

// Stream a file to Amazon S3.
// Assume $s3wrapper is a PHP stream that will write to S3, and that
// Psr7StreamWrapper is a class that will decorate a StreamInterface as a PHP
// StreamWrapper.
$stream = new Psr7StreamWrapper($file1->getStream());
stream_copy_to_stream($stream, $s3wrapper);

PSR-7: Interfacce

Psr\Http\Message\MessageInterface

interface MessageInterface {
    /**
     * Recupera la versione del protocollo HTTP (Es: "1.1", "1.0")
     *
     * @return string HTTP protocol version.
     */
    public function getProtocolVersion();

    /**
     * Restituisce un'istanza con una specifica versione del protocollo HTTP
     *
     * @param string $version HTTP protocol version
     * @return static
     */
    public function withProtocolVersion($version);

    /**
     * Recupera tutti i valori di intestazione del messaggio
     *
     * @return string[][] Returns an associative array of the message's headers.
     */
    public function getHeaders();

    /**
     * Verifica se esiste un'intestazione in base al nome senza distinzione tra maiuscole e minuscole.
     *
     * @param string $name Case-insensitive header field name.
     * @return bool
     */
    public function hasHeader($name);

    /**
     * Recupera un valore di intestazione del messaggio in base al nome senza distinzione tra maiuscole e minuscole.
     *
     * @param string $name Case-insensitive header field name.
     * @return string[] An array of string values as provided for the given header.
     */
    public function getHeader($name);

    /**
     * Recupera una stringa separata da virgole dei valori per una singola intestazione.
     *
     * @param string $name Case-insensitive header field name.
     * @return string A string of values as provided for the given header concatenated together using a comma.
     */
    public function getHeaderLine($name);

    /**
     * Restituisce un'istanza sostituendo il valore dell'intestazione specificata.
     *
     * @param string $name Case-insensitive header field name.
     * @param string|string[] $value Header value(s).
     * @return static
     * @throws \InvalidArgumentException for invalid header names or values.
     */
    public function withHeader($name, $value);

    /**
     * Restituisce un'istanza appendendo un valore al campo di intestazione specificato.
     *
     * @param string $name Case-insensitive header field name to add.
     * @param string|string[] $value Header value(s).
     * @return static
     * @throws \InvalidArgumentException for invalid header names or values.
     */
    public function withAddedHeader($name, $value);

    /**
     * Restituisce un'istanza eliminando l'intestazione specificata.
     *
     * @param string $name Case-insensitive header field name to remove.
     * @return static
     */
    public function withoutHeader($name);

    /**
     * Ottiene il corpo del messaggio.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody();

    /**
     * Restituisce un'istanza con il corpo del messaggio specificato.
     *   
     * @param StreamInterface $body Body.
     * @return static
     * @throws \InvalidArgumentException When the body is not valid.
     */
    public function withBody(StreamInterface $body);
}