Effettuare richieste HTTP in Laravel
Laravel mette a disposizione un client HTTP di alto livello basato su Guzzle, accessibile tramite la facade Http. Questo client semplifica notevolmente l'esecuzione di richieste verso API esterne, servizi REST e qualsiasi endpoint HTTP, offrendo un'interfaccia fluente, leggibile e ricca di funzionalità pronte all'uso come autenticazione, gestione degli errori, retry automatici e testing integrato.
In questo articolo vengono analizzate in modo approfondito tutte le funzionalità principali del client HTTP di Laravel, con esempi pratici e casi d'uso reali.
Prerequisiti e configurazione
Il client HTTP di Laravel è disponibile a partire dalla versione 7.x del framework. Non richiede installazioni aggiuntive poiché Guzzle è già incluso come dipendenza del framework stesso. La facade Http è registrata automaticamente e può essere utilizzata ovunque nell'applicazione.
Per verificare che Guzzle sia presente nel progetto è sufficiente controllare il file composer.json:
# Verifica della dipendenza Guzzle nel progetto
composer show guzzlehttp/guzzle
In caso non sia presente, si installa con:
# Installazione manuale di Guzzle
composer require guzzlehttp/guzzle
Richieste GET
La forma più semplice di richiesta HTTP è la richiesta GET, utilizzata per recuperare risorse da un server remoto. Con la facade Http si esegue nel seguente modo:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class PostService
{
public function fetchPosts(): array
{
// Recupera tutti i post dall'API remota
$response = Http::get('https://jsonplaceholder.typicode.com/posts');
// Restituisce i dati come array PHP
return $response->json();
}
}
È possibile passare parametri query direttamente come secondo argomento sotto forma di array associativo:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class PostService
{
public function fetchFilteredPosts(int $userId): array
{
// Aggiunge parametri query alla richiesta GET
$response = Http::get('https://jsonplaceholder.typicode.com/posts', [
'userId' => $userId,
'_limit' => 10,
]);
return $response->json();
}
}
Richieste POST, PUT e PATCH
Per inviare dati a un server si utilizzano i metodi post(), put() e patch(). Per impostazione predefinita i dati vengono inviati come application/x-www-form-urlencoded.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class PostService
{
public function createPost(string $title, string $body, int $userId): array
{
// Crea un nuovo post tramite richiesta POST
$response = Http::post('https://jsonplaceholder.typicode.com/posts', [
'title' => $title,
'body' => $body,
'userId' => $userId,
]);
return $response->json();
}
public function updatePost(int $id, string $title, string $body): array
{
// Aggiorna completamente una risorsa con PUT
$response = Http::put("https://jsonplaceholder.typicode.com/posts/{$id}", [
'title' => $title,
'body' => $body,
]);
return $response->json();
}
public function patchPost(int $id, string $title): array
{
// Aggiornamento parziale della risorsa con PATCH
$response = Http::patch("https://jsonplaceholder.typicode.com/posts/{$id}", [
'title' => $title,
]);
return $response->json();
}
}
Richieste DELETE
Il metodo delete() permette di eliminare una risorsa remota:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class PostService
{
public function deletePost(int $id): bool
{
// Elimina il post con l'ID specificato
$response = Http::delete("https://jsonplaceholder.typicode.com/posts/{$id}");
// Restituisce true se la cancellazione è avvenuta con successo
return $response->successful();
}
}
Invio di dati JSON
Per inviare dati con Content-Type: application/json si utilizza il metodo withBody() oppure, più comodamente, il metodo asJson():
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class ApiService
{
public function sendJsonPayload(array $data): array
{
// Invia i dati come JSON impostando automaticamente il Content-Type corretto
$response = Http::asJson()
->post('https://api.example.com/resources', $data);
return $response->json();
}
}
In alternativa si può usare il metodo withJson(), disponibile come alias:
<?php
use Illuminate\Support\Facades\Http;
// Entrambe le forme sono equivalenti
$response = Http::withJson(['name' => 'Laravel', 'version' => 11])
->post('https://api.example.com/frameworks');
Invio di dati multipart e file
Per caricare file o inviare richieste multipart/form-data si utilizza il metodo attach():
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class UploadService
{
public function uploadAvatar(string $filePath, string $originalName): array
{
// Allega un file alla richiesta come multipart/form-data
$response = Http::attach(
'avatar', // Nome del campo nel form
file_get_contents($filePath), // Contenuto binario del file
$originalName // Nome originale del file
)->post('https://api.example.com/users/1/avatar');
return $response->json();
}
public function uploadMultipleFiles(array $files): array
{
$request = Http::asMultipart();
// Allega più file alla stessa richiesta
foreach ($files as $fieldName => $filePath) {
$request = $request->attach(
$fieldName,
file_get_contents($filePath),
basename($filePath)
);
}
$response = $request->post('https://api.example.com/uploads');
return $response->json();
}
}
Headers personalizzati
È possibile aggiungere header HTTP personalizzati tramite il metodo withHeaders():
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class ApiService
{
public function fetchWithCustomHeaders(): array
{
$response = Http::withHeaders([
// Specifica il formato di risposta atteso
'Accept' => 'application/json',
// Header personalizzato per il versioning dell'API
'X-API-Version' => 'v2',
// Header di tracciamento per il logging remoto
'X-Request-ID' => uniqid('req_', true),
])->get('https://api.example.com/data');
return $response->json();
}
}
Autenticazione
Laravel HTTP client supporta diversi meccanismi di autenticazione in modo nativo.
Bearer Token
<?php
use Illuminate\Support\Facades\Http;
// Autenticazione tramite token Bearer (OAuth 2.0, JWT, ecc.)
$response = Http::withToken('your-api-token-here')
->get('https://api.example.com/protected-resource');
Basic Authentication
<?php
use Illuminate\Support\Facades\Http;
// Autenticazione HTTP Basic con username e password
$response = Http::withBasicAuth('username', 'password')
->get('https://api.example.com/secure-endpoint');
Digest Authentication
<?php
use Illuminate\Support\Facades\Http;
// Autenticazione HTTP Digest
$response = Http::withDigestAuth('username', 'password')
->get('https://api.example.com/digest-endpoint');
Gestione della risposta
L'oggetto Response restituito dalla facade Http espone numerosi metodi utili per ispezionare e leggere la risposta del server.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
class ResponseInspector
{
public function inspect(): void
{
$response = Http::get('https://jsonplaceholder.typicode.com/posts/1');
// Codice di stato HTTP della risposta
$statusCode = $response->status();
// Verifica se la richiesta è andata a buon fine (2xx)
$isSuccessful = $response->successful();
// Verifica se si è verificato un errore (4xx o 5xx)
$failed = $response->failed();
// Verifica codici specifici
$isOk = $response->ok(); // 200
$isCreated = $response->created(); // 201
$isNotFound = $response->notFound(); // 404
$isServerErr = $response->serverError(); // 5xx
// Corpo della risposta come stringa grezza
$rawBody = $response->body();
// Corpo della risposta decodificato come array PHP
$data = $response->json();
// Accesso a un campo specifico del JSON tramite dot notation
$title = $response->json('title');
// Lettura degli header della risposta
$contentType = $response->header('Content-Type');
// Tutti gli header come array associativo
$allHeaders = $response->headers();
}
}
Gestione degli errori
Per una gestione robusta degli errori Laravel offre diversi approcci. Il metodo throw() lancia un'eccezione automaticamente in caso di risposta con status 4xx o 5xx:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
class SafeApiService
{
public function fetchOrFail(int $id): array
{
try {
// Lancia RequestException se la risposta indica un errore HTTP
$response = Http::get("https://api.example.com/posts/{$id}")
->throw();
return $response->json();
} catch (RequestException $e) {
// Accesso alla risposta originale nell'eccezione
$statusCode = $e->response->status();
$body = $e->response->body();
logger()->error('Richiesta HTTP fallita', [
'status' => $statusCode,
'body' => $body,
]);
throw $e;
}
}
}
È anche possibile usare throwIf() e throwUnless() per lanciare eccezioni in modo condizionale:
<?php
use Illuminate\Support\Facades\Http;
$response = Http::get('https://api.example.com/data');
// Lancia l'eccezione solo se la condizione è vera
$response->throwIf($response->status() === 403);
// Lancia l'eccezione solo se la condizione è falsa
$response->throwUnless($response->successful());
Timeout e retry
Il client HTTP di Laravel permette di configurare timeout e retry automatici in modo dichiarativo e fluente.
Configurazione del timeout
<?php
use Illuminate\Support\Facades\Http;
// Imposta il timeout massimo della richiesta a 30 secondi
$response = Http::timeout(30)
->get('https://api.example.com/slow-endpoint');
Retry automatici
<?php
use Illuminate\Support\Facades\Http;
$response = Http::retry(
times: 3, // Numero massimo di tentativi
sleepMilliseconds: 500 // Attesa tra un tentativo e l'altro in millisecondi
)->get('https://api.example.com/unreliable-endpoint');
È possibile passare una callback come terzo parametro per decidere dinamicamente se ritentare:
<?php
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
$response = Http::retry(
times: 3,
sleepMilliseconds: 1000,
// Riprova solo in caso di errori del server, non per errori del client
when: function (RequestException $exception): bool {
return $exception->response->serverError();
}
)->get('https://api.example.com/endpoint');
URL di base e istanze riutilizzabili
Quando si effettuano molte richieste verso la stessa API è conveniente usare il metodo baseUrl() per evitare ripetizioni:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\PendingRequest;
class GithubService
{
private PendingRequest $client;
public function __construct()
{
// Crea un'istanza del client preconfigurata con URL base e header comuni
$this->client = Http::baseUrl('https://api.github.com')
->withToken(config('services.github.token'))
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
]);
}
public function getUser(string $username): array
{
// L'URL base viene anteposto automaticamente
return $this->client->get("/users/{$username}")->json();
}
public function getRepositories(string $username): array
{
return $this->client->get("/users/{$username}/repos")->json();
}
}
Richieste asincrone e concorrenti
Laravel HTTP client supporta l'esecuzione asincrona e la concorrenza tramite il metodo pool(), che permette di eseguire più richieste in parallelo riducendo significativamente i tempi di attesa.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Pool;
class ConcurrentFetchService
{
public function fetchMultipleResources(): array
{
// Esegue tutte le richieste in parallelo anziché in sequenza
$responses = Http::pool(function (Pool $pool): array {
return [
// Ogni richiesta viene identificata da una chiave
$pool->as('posts')->get('https://jsonplaceholder.typicode.com/posts'),
$pool->as('users')->get('https://jsonplaceholder.typicode.com/users'),
$pool->as('comments')->get('https://jsonplaceholder.typicode.com/comments'),
];
});
return [
// Accesso ai risultati tramite le chiavi assegnate
'posts' => $responses['posts']->json(),
'users' => $responses['users']->json(),
'comments' => $responses['comments']->json(),
];
}
}
Middleware e intercettori
È possibile aggiungere middleware Guzzle alla pipeline delle richieste tramite il metodo withMiddleware(), utile per logging, trasformazione delle richieste o gestione personalizzata delle risposte:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Middleware;
class InstrumentedApiService
{
public function fetchWithLogging(string $url): array
{
$loggingMiddleware = Middleware::tap(
// Callback eseguita prima dell'invio della richiesta
function (RequestInterface $request): void {
logger()->info('Invio richiesta HTTP', [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
]);
}
);
$response = Http::withMiddleware($loggingMiddleware)
->get($url);
return $response->json();
}
}
Macro e personalizzazioni globali
Il client HTTP supporta le macro di Laravel, permettendo di definire configurazioni riutilizzabili registrandole in un service provider:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Registra una macro per il client dell'API interna
Http::macro('internalApi', function (): \Illuminate\Http\Client\PendingRequest {
return Http::baseUrl(config('app.internal_api_url'))
->withToken(config('app.internal_api_token'))
->timeout(10)
->retry(2, 200);
});
}
}
Una volta registrata, la macro è disponibile ovunque nell'applicazione:
<?php
use Illuminate\Support\Facades\Http;
// Utilizzo della macro registrata nel service provider
$response = Http::internalApi()->get('/products');
Testing delle richieste HTTP
Uno dei punti di forza del client HTTP di Laravel è la profonda integrazione con il sistema di testing. Il metodo Http::fake() permette di simulare le risposte HTTP senza effettuare chiamate reali alla rete.
Fake globale
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class PostServiceTest extends TestCase
{
public function test_fetch_posts_returns_expected_data(): void
{
// Intercetta tutte le richieste HTTP e restituisce una risposta simulata
Http::fake([
'jsonplaceholder.typicode.com/posts' => Http::response([
['id' => 1, 'title' => 'Primo post'],
['id' => 2, 'title' => 'Secondo post'],
], 200),
]);
$service = new \App\Services\PostService();
$posts = $service->fetchPosts();
$this->assertCount(2, $posts);
$this->assertEquals('Primo post', $posts[0]['title']);
}
}
Verifica delle richieste inviate
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class PostServiceTest extends TestCase
{
public function test_create_post_sends_correct_payload(): void
{
Http::fake();
$service = new \App\Services\PostService();
$service->createPost('Titolo di test', 'Corpo del post', userId: 1);
// Verifica che sia stata inviata esattamente una richiesta all'endpoint specificato
Http::assertSent(function (\Illuminate\Http\Client\Request $request): bool {
return $request->url() === 'https://jsonplaceholder.typicode.com/posts'
&& $request->method() === 'POST'
&& $request['title'] === 'Titolo di test'
&& $request['userId'] === 1;
});
}
public function test_no_requests_sent_when_using_cache(): void
{
Http::fake();
// Verifica che non siano state inviate richieste HTTP
Http::assertNothingSent();
}
}
Simulazione di sequenze di risposte
<?php
use Illuminate\Support\Facades\Http;
// Simula una sequenza di risposte diverse per lo stesso endpoint
Http::fake([
'api.example.com/*' => Http::sequence()
->push(['status' => 'processing'], 202) // Prima chiamata: in elaborazione
->push(['status' => 'done'], 200) // Seconda chiamata: completata
->pushStatus(503), // Terza chiamata: errore del server
]);
Integrazione con i servizi di Laravel
Il client HTTP può essere integrato in modo idiomatico con altri componenti di Laravel come la cache, le code e gli eventi.
Caching delle risposte
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class CachedApiService
{
public function getProduct(int $id): array
{
$cacheKey = "product.{$id}";
// Recupera dalla cache o effettua la chiamata HTTP e memorizza il risultato
return Cache::remember($cacheKey, now()->addHours(1), function () use ($id): array {
$response = Http::timeout(10)
->get("https://api.example.com/products/{$id}")
->throw();
return $response->json();
});
}
}
Richieste HTTP all'interno di Job
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SyncProductJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public function __construct(private readonly int $productId)
{
}
public function handle(): void
{
// Effettua la chiamata HTTP all'interno del job con retry gestito dalla coda
$data = Http::timeout(15)
->get("https://api.example.com/products/{$this->productId}")
->throw()
->json();
// Sincronizzazione del prodotto nel database locale
\App\Models\Product::updateOrCreate(
['external_id' => $this->productId],
$data
);
}
}
Conclusioni
Il client HTTP di Laravel rappresenta uno degli strumenti più completi e ben progettati nell'ecosistema PHP moderno. La sua API fluente permette di esprimere richieste HTTP complesse in modo leggibile e dichiarativo, mentre il supporto integrato per fake e asserzioni rende il testing di codice che interagisce con servizi esterni semplice e affidabile.
Grazie al supporto per richieste concorrenti tramite Http::pool(), alla gestione dichiarativa dei retry, alle macro, e alla piena compatibilità con i middleware Guzzle, il client HTTP di Laravel copre scenari che vanno dalla semplice integrazione con un'API esterna fino ad architetture distribuite con esigenze di resilienza e monitoraggio avanzati.
Padroneggiare questo componente significa avere a disposizione un fondamento solido per qualsiasi integrazione con servizi HTTP, interni o esterni all'applicazione.