
PSR-18, l'arte di standardizzare i client HTTP
alberto • April 22, 2021
focus psrDa qualche tempo, il PHP-FIG, un gruppo di persone dedicate a farci amare ancor di più PHP grazie a strumenti di collaborazione e standard condivisi, ha finalmente chiuso il capitolo dedicato al mondo HTTP.
Ma questo esattamente cosa significa? Cercheremo di scoprirlo in questo approfondimento, con anche un bell'esempio finale.
Lo sporco lavoro del PHP-FIG
Il PHP-FIG elabora i cosiddetti PSR, ovvero le PHP Standards Recommendations, dei documenti parecchio tecnici che cercano di migliorare l'ecosistema PHP introducendo standard. Ne esistono di diverso tipo e che si occupano di diversi temi.
Il PSR-1 e il PSR-12 si occupano di definire standard nella modalità di scrittura del codice, rispondendo per esempio alla domanda devo andare a capo prima parentesi graffa di apertura di un metodo?.
Il PSR-0 (ormai deprecato) e il PSR-4 si occupano di gestire la modalità con cui funziona l'autoloading di composer
.
In particolare in questo articolo approfondiremo il set di PSR dedicati all'integrazione HTTP.
Evitare problemi di dipendenze
Il principale scopo dei PSR-7, PSR-15, PSR-17 e PSR-18, ovvero gli standard dedicati al mondo HTTP, è quello di creare una serie di interfacce comuni tra i diversi client HTTP disponibili e futuri cosí da renderli tra di loro interscambiali per creare applicazioni disaccoppiate dalla reale implementazione.
Facciamo un esempio chiarificatore, anche se molto banalotto.
Marco ha sviluppato una libreria. Questa libreria permette di scaricare, sfruttando le API pubbliche, i video piú visti della settimana. Per poterlo fare senza grosse fatiche, utilizza Guzzle un client HTTP disponibile in PHP. Nel momento in cui ha sviluppato la libreria, l'ultima versione di Guzzle era la 6. Una volta terminato, pubblica la libreria su Github.
Andrea ha sviluppato anche lui una libreria. Questa libreria, simile a quella di Marco, permette di ottenere le stesse informazioni da Vimeo. La libreria di Andrea è più recente, tant'è che quando l'ha sviluppata era già disponibile Guzzle 7.
Simone vuole inserire nel suo portale due box, con i video piú visti su Youtube e Vimeo. Si accorge peró che non può utilizzare le librerie di Marco e di Andrea in contemporanea, dato che il codice di Guzzle 7 non è retrocompatibile e che le versioni richieste dalle due dipendenze differiscono. Simone è triste perchè deve reimplementare una delle due funzionalità a mano.
Programmare per interfacce
Il modo per risolvere a monte un problema del genere è quello dettato dalla coding by interfaces ovvero una tecnica che permette di sganciarsi dall'implementazione di una determinata funzionalità, appoggiandosi solamente ad interfacce pubbliche e comuni.
Nell'esempio di prima, Marco e Andrea, non avevamo davvero bisogno di Guzzle (6 o 7 che sia), ma necessitavano di un client HTTP. Quale fosse la reale implementazione a loro non interessava. Hanno scelto Guzzle solamente perchè più famoso e utilizzato nella community.
Il PHP-FIG, tramite i 4 PSR anticipati prima, cerca di rispondere alla domanda:
Ma quindi cosa avrebbero dovuto fare Marco e Andrea?
Avrebbero dovuto eliminare la dipendenza forte con un'implementazione particolare e si sarebbero dovuti affidare al suo posto delle interfacce generaliste evitando quindi di includere nella propria libreria una dipendenza verso Guzzle.
Approfondiamo i PSR
Abbiamo detto che PHP-FIG ci offre 4 diversi standard per gestire la cosa, introduciamoli singolarmente.
PSR-7: HTTP Message Interfaces
Lo standard definisce proprietà e metodi delle classi che rappresentano i messaggi scambiati durante una comunicazione HTTP. Abbiamo quindi:
MessageInterface
che rappresenta un singolo messagio, sia esso Request che ResponseRequestInterface
che rappresenta una singola Request effettuata da un client verso un serverServerRequestInterface
che rappresenta una singola Request ma in un contesto server-side dato che presenta ulteriori metodiRequestInterface
che rappresenta una singola Response indirizzata da un server ad un clientStreamInterface
che rappresenta uno stream di contenuti, spesso contenuto in una ResponseUriInterface
che rappresenta un URIUploadFileInterface
che rappresenta un file uploadato da un client
Questo è il pacchetto ufficiale che contiene la definizione delle interfacce.
PSR-15: HTTP Server Request Handlers
Standard che definisce le logiche di alcuni componenti server:
RequestHandlerInterface
che rappresenta un singolo controller webMiddlewareInterface
che rappresenta un middleware
PSR-15 si appoggia a PSR-7 per la definizione dei messaggi HTTP che i vari componenti ricevono come parametri.
Questo è il pacchetto ufficiale che contiene la definizione delle interfacce.
PSR-17: HTTP Factories
Lo standard definisce la modalità con la quale gli oggetti HTTP di PSR-7 dovrebbero venire costruiti:
RequestFactoryInterface
ResponseFactoryInterface
ServerRequestFactoryInterface
StreamFactoryInterface
UploadedFileFactoryInterface
UriFactoryInterface
Questo è il pacchetto ufficiale che contiene la definizione delle interfacce.
PSR-18: HTTP Client
Il documento definisce una interfaccia comune per inviare e ricevere messaggi HTTP. Questo standard chiude il cerchio delle specifiche rendendo di fatto gli standard precedenti davvero utilizzabili.
Definisce le seguenti interfacce:
ClientInterface
che rappresenta un clientClientExceptionInterface
che rappresenta un'eccezione relativa ad un errore clientRequestExceptionInterface
che rappresenta un'eccezione relativa ad un errore nella requestNetworkExceptionInterface
che rappresenta un'eccezione relativa ad un errore di rete
Questo è il pacchetto ufficiale che contiene la definizione delle interfacce.
Sporchiamoci le mani
Dopo tutte questi pipponi teorici difficilmente attuabili e concretizzabili nel mondo di tutti i giorni è arrivato il momento di capire realmente come tutto questo si traduce in codice PHP. E lo faremo realizzando una banalissima libreria che sfrutterà alcuni dei PSR visti nel precedente capitolo.
Realizzeremo una piccolo pacchetto in grado di ottenere l'anno attuale facendo scraping della homepage di Laravello. In basso infatti è presente l'anno corrente.
La libreria sarà composta da una sola semplice classe:
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
class LaravelloScraper
{
public function __construct(
public ClientInterface $httpClient,
public RequestFactoryInterface $requestFactory
) {
}
public function getCurrentYear()
{
$request = $this->requestFactory->createRequest('GET', 'https://www.laravello.it/');
$response = $this->httpClient->sendRequest($request);
$content = $response->getBody()->getContents();
$regexp = '/Copyright © (\d{4}) Laravello/';
preg_match($regexp, $content, $matches);
return (int)$matches[1];
}
}
La parte interessante non è tanto l'implementazione della funzionalità (fatta oltretutto in un modo da evitare sfruttando una regexp sul contenuto HTML) ma è l'utilizzo di due dipendenze, iniettate nel costruttore, e che non sono implementazioni, ma bensí interfacce.
Interfacce che sono presenti nei pacchetti psr/http-client
e psr/http-factory
scaricate sempre tramite composer:
composer require psr/http-client psr/http-factory
Grazie poi ai metodi createRequest
, sendRequest
, getBody
e getContents
riusciamo ad implementare totalmente la logica di business della nostra classe ignorando l'implementazione del client HTTP sottostante.
Ok tutto bello, ma come testo se funziona?
Ora, dato che la nostra classe LaravelloScraper
non è istanziabile autonomamente, come posso testare che funzioni?
Una delle possibilità per procedere è quella di realizzare uno unit test dedicato includendo alcune implementazioni solamente nel contesto dev
cosi da avere la possibilità di testare il codice senza generare strascichi nella codebase di chiunque voglia poi usare la libreria.
In particolare ho installato con
composer require nyholm/psr7 kriswallsmith/buzz --dev
due librerie che implementano le interfacce PSR e grazie a questo test (realizzato utilizzando PEST):
use App\LaravelloScraper;
use Nyholm\Psr7\Factory\Psr17Factory;
use Buzz\Client\Curl;
test('current year is 2021', function () {
$requestFactory = new Psr17Factory();
$client = new Curl($requestFactory);
$laravelloScraper = new LaravelloScraper($client, $requestFactory);
expect($laravelloScraper->getCurrentYear())->toBe(2021);
});
sono riuscito ad assicurarmi che il codice funzionasse correttamente.
Il nostro pacchetto è quindi pronto per il rilascio. Presenta un parte di codice totalmente agnostico dall'implementazione del client HTTP e un test funzionante che usa, solamente per scopi di testing interni, uno dei tanti client HTTP a disposizione nell'ecosistema. Chiunque voglia utilizzare la nostra libreria è autonomo nel scegliere quale client HTTP utilizzare dato che molto probabilmente ne stà già utilizzando uno.
(e ora le tanto attese) Conclusioni
In questo articolo abbiamo fatto filosofia. Il concetto di coding by interfaces è molto affascinante anche se talvolta molto distante dalla realtà.
I vantaggi descritti in questo articolo sono invece molto concreti, permettono di evitare problemi parecchio complessi da risolvere e che potrebbero costare parecchio tempo.
Grazie al lavoro del PHP-FIG, oggi abbiamo a disposizione un set molto esaustivo di interfacce che permettono davvero di utilizzare questo pattern da domani. Esistono già molti client che implementano questi contratti ed esistono già librerie, parecchio più utili dell'esempio visto precedentemente, che sfruttano questo approccio.