PSR-7 definisce interfacce PHP interoperabili per rappresentare messaggi HTTP, inclusi richieste, risposte, header, URI e body. Per creare un’API JSON, la soluzione pratica è usare un’implementazione compatibile, come Nyholm PSR-7, e trattare i messaggi come immutabili: ogni metodo with... restituisce una nuova istanza. Così il codice applicativo resta separato dalla libreria concreta e più semplice da testare.
Sintassi rapida (TL;DR):
$response = $response
->withHeader('Content-Type', 'application/json; charset=utf-8')
->withBody($stream);
$json = json_encode($data, JSON_THROW_ON_ERROR);
$stream = $streamFactory->createStream($json); Quando usare PSR-7 per gestire richieste e risposte HTTP in PHP
PSR-7 è utile quando un’applicazione deve scambiare messaggi HTTP tra componenti, middleware, framework o librerie senza legarsi al tipo di oggetto di un singolo framework. Le interfacce ServerRequestInterface e ResponseInterface definiscono un contratto condiviso: una funzione può ricevere una richiesta PSR-7 e restituire una risposta PSR-7 anche se il runtime HTTP è fornito da un’altra libreria.
Un errore frequente è trattare i metodi withHeader(), withStatus() e withBody() come modifiche in-place. Non lo sono: se il valore restituito non viene assegnato, la richiesta o la risposta originale rimane invariata. Un altro equivoco è confondere PSR-7 con un server HTTP: lo standard descrive interfacce e comportamento degli oggetti, ma non avvia un server e non invia da solo la risposta al client.
Implementazione PSR-7 in PHP con Nyholm e risposta JSON
L’esempio seguente simula un endpoint GET /api/profile. Il pacchetto Nyholm fornisce gli oggetti concreti; la logica del gestore dipende invece dalle interfacce PSR. Per eseguirlo, installa la dipendenza con Composer, salva il codice come index.php ed esegui php index.php.
composer require nyholm/psr7 <?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
/**
* Crea una risposta JSON con header coerenti.
*
* @throws JsonException Se i dati non possono essere codificati in JSON.
*/
function jsonResponse(
ResponseFactoryInterface $responseFactory,
StreamFactoryInterface $streamFactory,
array $data,
int $status = 200
): ResponseInterface {
$json = json_encode(
$data,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
);
$body = $streamFactory->createStream($json);
return $responseFactory
->createResponse($status)
->withHeader('Content-Type', 'application/json; charset=utf-8')
->withHeader('Cache-Control', 'no-store')
->withBody($body);
}
/**
* Gestisce la richiesta senza modificare l'oggetto ricevuto.
*
* @throws JsonException Se i dati di risposta non sono codificabili.
*/
function handleRequest(
ResponseFactoryInterface $responseFactory,
StreamFactoryInterface $streamFactory,
ServerRequestInterface $request
): ResponseInterface {
$path = $request->getUri()->getPath();
if ($path !== '/api/profile') {
return jsonResponse(
$responseFactory,
$streamFactory,
['error' => 'Risorsa non trovata'],
404
);
}
$query = $request->getQueryParams();
$name = $query['name'] ?? 'ospite';
// Evita warning e conversioni impreviste se name arriva come array.
if (!is_string($name) || trim($name) === '') {
$name = 'ospite';
}
return jsonResponse(
$responseFactory,
$streamFactory,
[
'message' => 'Profilo caricato',
'user' => [
'name' => trim($name),
'active' => true,
],
]
);
}
$factory = new Psr17Factory();
$request = $factory
->createServerRequest(
'GET',
'https://example.test/api/profile?name=Laura'
)
->withQueryParams(['name' => 'Laura'])
->withHeader('Accept', 'application/json');
$response = handleRequest($factory, $factory, $request);
// Nel server reale il framework invia status, header e body al client.
printf(
"HTTP %d\nContent-Type: %s\n%s\n",
$response->getStatusCode(),
$response->getHeaderLine('Content-Type'),
(string) $response->getBody()
); Output dell’esecuzione / Risultato atteso
HTTP 200
Content-Type: application/json; charset=utf-8
{"message":"Profilo caricato","user":{"name":"Laura","active":true}} Come funzionano interfacce PSR-7, immutabilità e stream HTTP
1. Interfacce PSR-7 e factory PSR-17
handleRequest() accetta ServerRequestInterface e restituisce ResponseInterface: non richiede una classe specifica di Nyholm. Le interfacce ResponseFactoryInterface e StreamFactoryInterface permettono inoltre di creare risposta e body senza vincolare queste funzioni alla factory concreta. Psr17Factory viene usata nel punto di composizione perché implementa le factory PSR-17 necessarie all’esempio. PSR-17 standardizza infatti la creazione degli oggetti PSR-7.
2. Metodi immutabili e assegnazione del risultato
La catena ->withQueryParams() e ->withHeader() restituisce nuove istanze. Questo è essenziale: chiamare $request->withHeader(...) senza conservare il risultato non aggiunge l’header alla variabile originale. L’immutabilità riguarda gli oggetti messaggio e aiuta a evitare modifiche nascoste tra middleware; non significa che ogni risorsa collegata, come uno stream, sia sempre immutabile.
3. Codifica JSON, validazione e body come stream
json_encode() usa JSON_THROW_ON_ERROR: in caso di dati non codificabili solleva una JsonException invece di restituire silenziosamente false. L’errore può essere gestito da un middleware o dal livello HTTP, che dovrebbe convertirlo in una risposta controllata e registrare i dettagli nei log senza esporli al client. Il nome viene verificato come stringa prima di applicare trim(), evitando il warning che si avrebbe convertendo direttamente un array in stringa.
Il body è uno stream PSR-7, creato qui da una stringa JSON. Per risposte molto grandi, generare prima l’intero JSON può aumentare l’uso di memoria: in quel caso valuta una serializzazione o uno streaming adatto al formato e al server impiegato. Per una normale risposta JSON di dimensioni contenute, la soluzione è semplice e leggibile.
Confronto tra PSR-7, implementazioni concrete e invio HTTP
PSR-7 definisce i contratti, non sceglie da sola l’implementazione né il modo in cui il messaggio raggiunge il client. La tabella distingue i tre livelli coinvolti.
| Metodo / Approccio | Sintassi / Caratteristica | Performance / Complessità | Quando sceglierlo |
|---|---|---|---|
| Interfacce PSR-7 | ServerRequestInterface, ResponseInterface | Overhead minimo; favoriscono l’interoperabilità | Per logica applicativa riutilizzabile e testabile |
| Nyholm PSR-7 | new Psr17Factory() | Richiede una dipendenza Composer; crea oggetti concreti | Per costruire messaggi PSR-7 senza implementare le interfacce |
| Runtime o framework HTTP | Invio di status, header e body al client | Dipende dal server e dal framework utilizzato | In produzione, per integrare la risposta nel ciclo HTTP reale |
Errori comuni da evitare e best practice PSR-7 in produzione
- Risultato immutabile ignorato: chiamare
withHeader()senza assegnarne il ritorno lascia invariata la variabile originale. Conserva sempre la nuova istanza. - Query string non trasferita nei parametri: PSR-7 non garantisce che creare una richiesta da un URI popoli automaticamente
getQueryParams(). Il server o il test deve fornire i parametri; l’esempio usawithQueryParams()esplicitamente. - JSON non valido o errore esposto: usa
JSON_THROW_ON_ERRORe gestisciJsonExceptional confine HTTP, evitando di restituire stack trace o dettagli interni al client. - Header inviati manualmente in un framework: non chiamare
header()edhttp_response_code()se il framework gestisce già la risposta; potresti inviare header duplicati o in un ordine errato. - Body consumato durante la lettura: gli stream hanno una posizione interna. Dopo aver letto un body, verifica la posizione o riportala all’inizio con
rewind()quando lo stream lo consente.
Domande frequenti su PSR-7 in PHP
PSR-7 modifica davvero le richieste e le risposte in modo immutabile?
I metodi with... restituiscono una nuova istanza del messaggio e non modificano quella originale. È necessario assegnare il valore restituito per conservare la modifica. Gli stream associati al messaggio hanno una propria posizione e non vanno confusi con l’immutabilità degli oggetti richiesta e risposta.
Qual è la differenza tra PSR-7 e PSR-17?
PSR-7 definisce le interfacce dei messaggi HTTP, come richieste, risposte, URI e stream. PSR-17 definisce factory standard per creare questi oggetti; un’implementazione come Psr17Factory può fornire entrambe le famiglie di API.
Perché getQueryParams() restituisce un array vuoto?
La presenza di una query nell’URI non implica che ogni implementazione popoli automaticamente i parametri della richiesta. Nelle applicazioni reali li prepara il server o il framework; nei test puoi impostarli esplicitamente con withQueryParams().
Documentazione ufficiale PSR-7 e risorse PHP
- PSR-7: HTTP Message Interfaces — specifica ufficiale delle interfacce dei messaggi HTTP.
- PSR-17: HTTP Factories — interfacce standard per creare oggetti PSR-7.
- PHP: json_encode() — opzioni, codifica JSON ed errori.
