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.