Il componente HttpFoundation

Il componente HttpFoundation definisce un livello orientato agli oggetti per le specifiche HTTP.

In PHP, la richiesta è rappresentata da alcune variabili globali ($_GET, $_POST, $_FILE, $_COOKIE, $_SESSION...) e la risposta è generata da alcune funzioni (echo, header, setcookie, ...).

Il componente HttpFoundation di Symfony sostituisce queste variabili globali e queste funzioni di PHP con un livello orientato agli oggetti.

Installazione

Si può installare il componente in due modi:

Quindi, richiedere il file vendor/autoload.php per abilitare il meccanismo di auto-caricamento fornito da Composer. Altrimenti, l’applicazione non sarà in grado di trovare le classi di questo componente di Symfony.

Richiesta

Il modo più comune per creare una richiesta è basarla sulle variabili attuali di PHP, con createFromGlobals():

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

che è quasi equivalente al più verboso, ma anche più flessibile, __construct():

$request = new Request(
    $_GET,
    $_POST,
    array(),
    $_COOKIE,
    $_FILES,
    $_SERVER
);

Accedere ai dati della richiesta

Un oggetto richiesta contiene informazioni sulla richiesta del client. Si può accedere a queste informazioni tramite varie proprietà pubbliche:

  • request: equivalente di $_POST;
  • query: equivalente di $_GET ($request->query->get('name'));
  • cookies: equivalente di $_COOKIE;
  • attributes: non ha equivalenti, è usato dall’applicazione per memorizzare altri dati (vedere sotto)
  • files: equivalente di $_FILE;
  • server: equivalente di $_SERVER;
  • headers: quasi equivalente di un sottinsieme di $_SERVER ($request->headers->get('Content-Type')).

Ogni proprietà è un’istanza di Symfony\Component\HttpFoundation\ParameterBag (o di una sua sotto-classe), che è una classe contenitore:

  • request: Symfony\Component\HttpFoundation\ParameterBag;
  • query: Symfony\Component\HttpFoundation\ParameterBag;
  • cookies: Symfony\Component\HttpFoundation\ParameterBag;
  • attributes: Symfony\Component\HttpFoundation\ParameterBag;
  • files: Symfony\Component\HttpFoundation\FileBag;
  • server: Symfony\Component\HttpFoundation\ServerBag;
  • headers: Symfony\Component\HttpFoundation\HeaderBag.

Tutte le istanze di Symfony\Component\HttpFoundation\ParameterBag hanno metodi per recuperare e aggiornare i propri dati:

  • all(): Restituisce i parametri;
  • keys(): Restituisce le chiavi dei parametri;
  • replace(): Sostituisce i parametri attuali con dei nuovi;
  • add(): Aggiunge parametri;
  • get(): Restituisce un parametro per nome;
  • set(): Imposta un parametro per nome;
  • has(): Restituisce true se il parametro è definito;
  • remove(): Rimuove un parametro.

La classe Symfony\Component\HttpFoundation\ParameterBag ha anche alcuni metodi per filtrare i valori in entrata:

  • getAlpha(): Restituisce i caratteri alfabetici nel valore del parametro;
  • getAlnum(): Restituisce i caratteri alfabetici e i numeri nel valore del parametro;
  • getDigits(): Restituisce i numeri nel valore del parametro;
  • getInt(): Restituisce il valore del parametro convertito in intero;
  • filter(): Filtra il parametro, usando la funzione PHP filter_var().

Tutti i getter accettano tre parametri: il primo è il nome del parametro e il secondo è il valore predefinito, da restituire se il parametro non esiste:

// la query string è '?foo=bar'

$request->query->get('foo');
// restituisce bar

$request->query->get('bar');
// restituisce null

$request->query->get('bar', 'bar');
// restituisce 'bar'

Quando PHP importa la query della richiesta, gestisce i parametri della richiesta, come foo[bar]=bar, in modo speciale, creando un array. In questo modo, si può richiedere il parametro foo e ottenere un array con un elemento bar. A volte, però, si potrebbe volere il valore del nome “originale” del parametro: foo[bar]. Ciò è possibile con tutti i getter di ParameterBag, come get(), tramite il terzo parametro:

// la query string è '?foo[bar]=bar'

$request->query->get('foo');
// restituisce array('bar' => 'bar')

$request->query->get('foo[bar]');
// restituisce null

$request->query->get('foo[bar]', null, true);
// restituisce 'bar'

Infine, ma non meno importante, si possono anche memorizzare dati aggiuntivi nella richiesta, grazie alla proprietà pubblica attributes, che è anche un’istanza di Symfony\Component\HttpFoundation\ParameterBag. La si usa soprattutto per allegare informazioni che appartengono alla richiesta e a cui si deve accedere in diversi punti dell’applicazione. Per informazioni su come viene usata nel framework Symfony, vedere il libro.

Infine, si può accedere ai dati grezzi inviati nel corpo della richiesta usando getContent():

$content = $request->getContent();

Questo potrebbe essere utile, per esempio, per processare una stringa JSON inviata all’applicazione da un servizio remoto tramite metodo HTTP POST.

Identificare una richiesta

Nella propria applicazione, serve un modo per identificare una richiesta. La maggior parte delle volte, lo si fa tramite il “path info” della richiesta, a cui si può accedere tramite il metodo getPathInfo():

// per una richiesta a http://example.com/blog/index.php/post/hello-world
// path info è "/post/hello-world"
$request->getPathInfo();

Simulare una richiesta

Invece di creare una richiesta basata sulle variabili di PHP, si può anche simulare una richiesta:

$request = Request::create(
    '/hello-world',
    'GET',
    array('name' => 'Fabien')
);

Il metodo create() crea una richiesta in base a path info, un metodo e alcuni parametri (i parametri della query o quelli della richiesta, a seconda del metodo HTTP) e, ovviamente, si possono forzare anche tutte le altre variabili (Symfony crea dei valori predefiniti adeguati per ogni variabile globale di PHP).

In base a tale richiesta, si possono forzare le variabili globali di PHP tramite overrideGlobals():

$request->overrideGlobals();

Suggerimento

Si può anche duplicare una query esistente, tramite duplicate(), o cambiare molti parametri con una singola chiamata a initialize().

Accedere alla sessione

Se si ha una sessione allegata alla richiesta, vi si può accedere tramite il metodo getSession(). Il metodo hasPreviousSession() dice se la richiesta contiene una sessione, che sia stata fatta partire in una delle richieste precedenti.

Accedere ai dati degli header Accept-*

Si può accedere facilmente ai dati di base estratti dagli header Accept-* usando i seguenti metodi:

  • getAcceptableContentTypes(): restituisce la lista dei tipi di contenuto accettati, ordinata per qualità discendente;

  • getLanguages(): restituisce la lista delle lingue accettate, ordinata per qualità discendente

  • getCharsets(): restituisce la lista dei charset accettati, ordinata per qualità discendente

  • getEncodings(): restituisce la lista delle codifiche accettate, ordinata per qualità discendente

    Nuovo nella versione 2.4: Il metodo getEncodings() è stato introdotto in Symfony 2.4.

Se occorre pieno accesso ai dati analizzati da Accept, Accept-Language, Accept-Charset o Accept-Encoding, si può usare la classe Symfony\Component\HttpFoundation\AcceptHeader:

use Symfony\Component\HttpFoundation\AcceptHeader;

$accept = AcceptHeader::fromString($request->headers->get('Accept'));
if ($accept->has('text/html')) {
    $item = $accept->get('text/html');
    $charset = $item->getAttribute('charset', 'utf-8');
    $quality = $item->getQuality();
}

// accepts items are sorted by descending quality
$accepts = AcceptHeader::fromString($request->headers->get('Accept'))
    ->all();

Accedere ad altri dati

La classe Request ha molti altri metodi, che si possono usare per accedere alle informazioni della richiesta. Si dia uno sguardo alle API di Request per maggiori informazioni.

Sovrascrivere la richiesta

Nuovo nella versione 2.4: Il metodo setFactory() è stato introdotto in Symfony 2.4.

La classe Request non andrebbe sovrascritta, perché è un oggetto che rappresenta un messaggio HTTP. Ma, migrando da un sistema obsoleto, l’aggiunta di metodi o la modifica di alcuni comportamenti potrebbero aiutare. In questo caso, registrare un callable che sia in grado di creare un’istanza della classe Request:

use Symfony\Component\HttpFoundation\Request;

Request::setFactory(function (
    array $query = array(),
    array $request = array(),
    array $attributes = array(),
    array $cookies = array(),
    array $files = array(),
    array $server = array(),
    $content = null
) {
    return SpecialRequest::create(
        $query,
        $request,
        $attributes,
        $cookies,
        $files,
        $server,
        $content
    );
});

$request = Request::createFromGlobals();

Risposta

Un oggetto Symfony\Component\HttpFoundation\Response contiene tutte le informazioni che devono essere rimandate al client, per una data richiesta. Il costruttore accetta fino a tre parametri: il contenuto della risposta, il codice di stato e un array di header HTTP:

use Symfony\Component\HttpFoundation\Response;

$response = new Response(
    'Contenuto',
    Response::HTTP_OK,
    array('content-type' => 'text/html')
);

Nuovo nella versione 2.4: Il supporto per le costanti dei codici di stato HTTP è stato introdotto in Symfony 2.4.

Queste informazioni possono anche essere manipolate dopo la creazione di Response:

$response->setContent('Ciao mondo');

// l'attributo pubblico headers è un ResponseHeaderBag
$response->headers->set('Content-Type', 'text/plain');

$response->setStatusCode(404);

Quando si imposta il Content-Type di Response, si può impostare il charset, ma è meglio impostarlo tramite il metodo setCharset():

$response->setCharset('ISO-8859-1');

Si noti che Symfony presume che le risposte siano codificate in UTF-8.

Inviare la risposta

Prima di inviare la risposta, ci si può assicurare che rispetti le specifiche HTTP, richiamando il metodo prepare():

$response->prepare($request);

Inviare la risposta al client è quindi semplice, basta richiamare send():

$response->send();

Gestire la cache HTTP

La classe Symfony\Component\HttpFoundation\Response ha un corposo insieme di metodi per manipolare gli header HTTP relativi alla cache:

Il metodo setCache() può essere usato per impostare le informazioni di cache più comuni, con un’unica chiamata:

$response->setCache(array(
    'etag'          => 'abcdef',
    'last_modified' => new \DateTime(),
    'max_age'       => 600,
    's_maxage'      => 600,
    'private'       => false,
    'public'        => true,
));

Per verificare che i validatori della risposta (ETag, Last-Modified) corrispondano a un valore condizionale specificato nella richiesta del client, usare il metodo isNotModified():

if ($response->isNotModified($request)) {
    $response->send();
}

Se la risposta non è stata modificata, imposta il codice di stato a 304 e rimuove il contenuto effettivo della risposta.

Rinviare l’utente

Per rinviare il client a un altro URL, si può usare la classe Symfony\Component\HttpFoundation\RedirectResponse:

use Symfony\Component\HttpFoundation\RedirectResponse;

$response = new RedirectResponse('http://example.com/');

Flusso di risposta

La classe Symfony\Component\HttpFoundation\StreamedResponse consente di inviare flussi di risposte al client. Il contenuto della risposta viene rappresentato da un callable PHP, invece che da una stringa:

use Symfony\Component\HttpFoundation\StreamedResponse;

$response = new StreamedResponse();
$response->setCallback(function () {
    echo 'Ciao mondo';
    flush();
    sleep(2);
    echo 'Ciao mondo';
    flush();
});
$response->send();

Nota

La funzione flush() non esegue il flush del buffer. Se è stato richiamato ob_start() in precedenza oppure se l’opzione output_buffering è abilitata in php.ini, occorre richiamare ob_flush() prima di flush().

Inoltre, PHP non è l’unico livello possibile di buffer dell’output. Il server web può anche eseguire un buffer, a seconda della configurazione. Ancora, se si usa fastcgi, non si può disabilitare affatto il buffer.

Scaricare file

Quando si scarica un file, occorre aggiungere un header Content-Disposition alla risposta. Sebbene la creazione di questo header per scaricamenti di base sia facile, l’uso di nomi di file non ASCII è più complesso. Il metodo Symfony\Component\HttpFoundation\Response:makeDisposition() astrae l’ingrato compito dietro una semplice API:

use Symfony\Component\HttpFoundation\ResponseHeaderBag;

$d = $response->headers->makeDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'foo.pdf'
);

$response->headers->set('Content-Disposition', $d);

In alternativa, se si sta servendo un file statico, si può usare Symfony\Component\HttpFoundation\BinaryFileResponse:

use Symfony\Component\HttpFoundation\BinaryFileResponse

$file = 'percorrso/del/file.txt';
$response = new BinaryFileResponse($file);

BinaryFileResponse gestirà automaticamente gli header Range e If-Range della richiesta. Supporta anche X-Sendfile (vedere per Nginx e Apache). Per poterlo usare, occorre determinare se l’header X-Sendfile-Type sia fidato o meno e richiamare trustXSendfileTypeHeader() in caso positivo:

$response::trustXSendfileTypeHeader();

Si può ancora impostare il Content-Type del file inviato o cambiarne il Content-Disposition:

$response->headers->set('Content-Type', 'text/plain');
$response->setContentDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'nomefile.txt'
);

Nuovo nella versione 2.6: Il metodo deleteFileAfterSend() è stato introdotto in Symfony 2.6.

È possibile eliminare il file dopo l’invio della richiesta, con il metodo deleteFileAfterSend(). Si noti che questo non funzionerà se è impostato l’header X-Sendfile.

Creare una risposta JSON

Si può creare qualsiasi tipo di risposta tramite la classe Symfony\Component\HttpFoundation\Response, impostando il contenuto e gli header corretti. Una risposta JSON può essere come questa:

use Symfony\Component\HttpFoundation\Response;

$response = new Response();
$response->setContent(json_encode(array(
    'data' => 123,
)));
$response->headers->set('Content-Type', 'application/json');

C’è anche un’utile classe Symfony\Component\HttpFoundation\JsonResponse, che può rendere le cose ancora più semplici:

use Symfony\Component\HttpFoundation\JsonResponse;

$response = new JsonResponse();
$response->setData(array(
    'data' => 123
));

Il risultato è una codifica dell’array di dati in JSON, con header Content-Type impostato a application/json.

Attenzione

Per evitare un Hijacking JSON XSSI , bisogna passare un array associativo come parte più esterna dell’array a JsonResponse e non un array indicizzato, in modo che il risultato finale sia un oggetto (p.e. {"oggetto": "non dentro un array"}) invece che un array (p.e. [{"oggetto": "dentro un array"}]). Si leggano le linee guida OWASP per maggiori informazioni.

Solo i metodi che rispondono a richieste GET sono vulnerabili a ‘JSON Hijacking’ XSSI. I metodi che rispondono a richieste POST restano immuni.

Callback JSONP

Se si usa JSONP, si può impostare la funzione di callback a cui i dati vanno passati:

$response->setCallback('handleResponse');

In tal caso, l’header Content-Type sarà text/javascript e il contenuto della risposta sarà come questo:

handleResponse({'data': 123});

Sessioni

Le informazioni sulle sessioni sono nell’apposito documento: Gestione della sessione.