Richieste HTTP con cURL in PHP

cURL (Client URL) è una delle librerie più utilizzate in PHP per effettuare richieste HTTP verso server remoti. Integrata nativamente nell'interprete attraverso l'estensione ext-curl, consente di comunicare con API REST, scaricare file, inviare dati tramite form e gestire autenticazione, cookie, certificati SSL e molto altro. In questa guida analizzeremo in modo approfondito ogni aspetto della libreria, partendo dalle operazioni di base fino ad arrivare a scenari avanzati.

Verifica dell'estensione cURL

Prima di iniziare è necessario accertarsi che l'estensione sia abilitata nel proprio ambiente. Il modo più rapido consiste nell'interrogare la funzione function_exists oppure nel consultare l'output di phpinfo().

<?php

// Verifica se l'estensione cURL è disponibile
if (!function_exists('curl_init')) {
    die('L\'estensione cURL non è installata o non è abilitata.');
}

echo 'cURL è disponibile. Versione: ' . curl_version()['version'];

Se l'estensione non risulta attiva, su sistemi Debian/Ubuntu è sufficiente installarla con sudo apt install php-curl e riavviare il server web. Su Windows occorre decommentare la riga extension=curl nel file php.ini.

Il ciclo di vita di una richiesta cURL

Ogni interazione con cURL segue quattro fasi fondamentali: inizializzazione della sessione, configurazione delle opzioni, esecuzione della richiesta e chiusura della risorsa. Comprendere questo ciclo è essenziale per scrivere codice robusto e privo di memory leak.

<?php

// 1. Inizializzazione della sessione
$ch = curl_init();

// 2. Configurazione delle opzioni
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

// 3. Esecuzione della richiesta
$response = curl_exec($ch);

// 4. Chiusura della risorsa
curl_close($ch);

La funzione curl_init() restituisce un handle, cioè una risorsa che rappresenta la sessione. Con curl_setopt() si impostano le opzioni una alla volta, mentre curl_exec() lancia effettivamente la comunicazione di rete. Al termine, curl_close() libera la memoria allocata.

Richiesta GET semplice

La richiesta GET è il metodo HTTP predefinito ed è quello utilizzato da cURL quando non viene specificato diversamente. Si usa per recuperare risorse dal server senza modificarne lo stato.

<?php

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://jsonplaceholder.typicode.com/posts/1',
    CURLOPT_RETURNTRANSFER => true,   // Restituisce il risultato come stringa
    CURLOPT_TIMEOUT        => 30,     // Timeout massimo in secondi
    CURLOPT_FOLLOWLOCATION => true,   // Segue eventuali redirect
]);

$response = curl_exec($ch);

if ($response === false) {
    // Gestione dell'errore di rete
    echo 'Errore cURL: ' . curl_error($ch);
} else {
    // Decodifica della risposta JSON
    $data = json_decode($response, true);
    print_r($data);
}

curl_close($ch);

L'opzione CURLOPT_RETURNTRANSFER è quasi sempre indispensabile: senza di essa il contenuto della risposta viene stampato direttamente sullo standard output invece di essere restituito come valore di ritorno di curl_exec(). La funzione curl_setopt_array() permette di impostare più opzioni in un'unica chiamata, rendendo il codice più leggibile.

Richiesta GET con parametri nella query string

Quando si devono passare parametri al server tramite GET, è buona pratica costruire la query string con http_build_query() per garantire una corretta codifica dei caratteri speciali.

<?php

// Parametri da inviare nella query string
$params = [
    'page'     => 2,
    'per_page' => 10,
    'sort'     => 'created_at',
    'order'    => 'desc',
];

$url = 'https://api.example.com/users?' . http_build_query($params);

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Accept: application/json',   // Richiede una risposta in formato JSON
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

echo "Codice HTTP: {$httpCode}" . PHP_EOL;
echo $response;

La funzione curl_getinfo() fornisce numerose informazioni sulla richiesta appena eseguita. Passando la costante CURLINFO_HTTP_CODE si ottiene il codice di stato HTTP restituito dal server, utile per distinguere risposte di successo (2xx) da errori client (4xx) o server (5xx).

Richiesta POST con dati JSON

Le richieste POST servono per inviare dati al server, tipicamente per creare nuove risorse. Nelle API moderne il formato più comune è JSON.

<?php

$url = 'https://jsonplaceholder.typicode.com/posts';

// Dati da inviare nel corpo della richiesta
$payload = [
    'title'  => 'Nuovo articolo',
    'body'   => 'Contenuto dell\'articolo di esempio.',
    'userId' => 1,
];

$jsonPayload = json_encode($payload);

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,              // Imposta il metodo POST
    CURLOPT_POSTFIELDS     => $jsonPayload,       // Corpo della richiesta
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',          // Tipo di contenuto inviato
        'Content-Length: ' . strlen($jsonPayload), // Lunghezza del payload
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($httpCode === 201) {
    // Risorsa creata con successo
    $createdPost = json_decode($response, true);
    echo 'Post creato con ID: ' . $createdPost['id'];
} else {
    echo "Errore nella creazione. Codice HTTP: {$httpCode}";
}

Quando si passa una stringa a CURLOPT_POSTFIELDS, cURL la invia così com'è nel body della richiesta. Se invece si passa un array, cURL lo codifica automaticamente come multipart/form-data. Per le API JSON è quindi necessario serializzare manualmente l'array con json_encode() e specificare l'header Content-Type appropriato.

Richiesta POST con dati di un form

In alcuni casi è necessario simulare l'invio di un modulo HTML tradizionale, utilizzando la codifica application/x-www-form-urlencoded.

<?php

$url = 'https://httpbin.org/post';

// Dati del modulo
$formData = [
    'username' => 'mario.rossi',
    'password' => 's3cr3tP@ss!',
    'remember' => '1',
];

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($formData), // Codifica URL-encoded
]);

$response = curl_exec($ch);
curl_close($ch);

echo $response;

Utilizzando http_build_query() l'array viene trasformato in una stringa del tipo username=mario.rossi&password=s3cr3tP%40ss%21&remember=1, e cURL imposta automaticamente il Content-Type su application/x-www-form-urlencoded.

Richieste PUT, PATCH e DELETE

Le API RESTful utilizzano diversi metodi HTTP per distinguere le operazioni sulle risorse. cURL permette di specificare qualsiasi metodo tramite l'opzione CURLOPT_CUSTOMREQUEST.

<?php

/**
 * Funzione generica per eseguire richieste HTTP con metodo personalizzato.
 */
function sendRequest(string $method, string $url, array $data = []): array
{
    $ch = curl_init();

    $options = [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => strtoupper($method), // Metodo HTTP personalizzato
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
        ],
    ];

    // Aggiunge il corpo della richiesta se sono presenti dati
    if (!empty($data)) {
        $options[CURLOPT_POSTFIELDS] = json_encode($data);
    }

    curl_setopt_array($ch, $options);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);

    curl_close($ch);

    return [
        'status'   => $httpCode,
        'body'     => json_decode($response, true),
        'error'    => $error,
    ];
}

// Aggiornamento completo di una risorsa (PUT)
$putResult = sendRequest('PUT', 'https://jsonplaceholder.typicode.com/posts/1', [
    'id'     => 1,
    'title'  => 'Titolo aggiornato',
    'body'   => 'Corpo completamente sostituito.',
    'userId' => 1,
]);

echo 'PUT - Codice: ' . $putResult['status'] . PHP_EOL;

// Aggiornamento parziale di una risorsa (PATCH)
$patchResult = sendRequest('PATCH', 'https://jsonplaceholder.typicode.com/posts/1', [
    'title' => 'Solo il titolo cambia',
]);

echo 'PATCH - Codice: ' . $patchResult['status'] . PHP_EOL;

// Eliminazione di una risorsa (DELETE)
$deleteResult = sendRequest('DELETE', 'https://jsonplaceholder.typicode.com/posts/1');

echo 'DELETE - Codice: ' . $deleteResult['status'] . PHP_EOL;

La differenza semantica tra PUT e PATCH è importante: PUT sostituisce interamente la risorsa con i nuovi dati, mentre PATCH ne modifica solo i campi specificati. DELETE, come suggerisce il nome, rimuove la risorsa dal server.

Upload di file

Per caricare file su un server remoto si utilizza la classe CURLFile, introdotta in PHP 5.5 come sostituto sicuro della vecchia sintassi con il prefisso @.

<?php

$url = 'https://httpbin.org/post';

// Creazione dell'oggetto CURLFile
$file = new CURLFile(
    '/percorso/al/documento.pdf', // Percorso assoluto del file
    'application/pdf',             // Tipo MIME
    'documento.pdf'                // Nome del file visibile al server
);

// Dati da inviare insieme al file
$postData = [
    'file'        => $file,
    'description' => 'Report trimestrale Q1 2026',
    'category'    => 'reports',
];

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $postData, // cURL usa automaticamente multipart/form-data
]);

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Errore durante l\'upload: ' . curl_error($ch);
} else {
    echo 'File caricato con successo.' . PHP_EOL;
    echo $response;
}

curl_close($ch);

Quando CURLOPT_POSTFIELDS riceve un array contenente un oggetto CURLFile, cURL imposta automaticamente la codifica multipart/form-data e costruisce i confini (boundary) del messaggio. Non bisogna impostare manualmente l'header Content-Type in questo caso, altrimenti la richiesta potrebbe fallire.

Download di file

Per scaricare file di grandi dimensioni è preferibile scrivere direttamente su disco anziché caricare l'intero contenuto in memoria.

<?php

$url        = 'https://example.com/archivio.zip';
$outputPath = '/tmp/archivio.zip';

// Apertura del file in scrittura binaria
$fp = fopen($outputPath, 'wb');

if ($fp === false) {
    die('Impossibile aprire il file di destinazione.');
}

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_FILE           => $fp,    // Scrive direttamente nel file handle
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT        => 300,    // Timeout generoso per file grandi
    CURLOPT_NOPROGRESS     => false,  // Abilita la callback di progresso
    CURLOPT_PROGRESSFUNCTION => function ($ch, $totalDownload, $downloaded) {
        if ($totalDownload > 0) {
            $percentage = round(($downloaded / $totalDownload) * 100);
            echo "\rProgresso: {$percentage}%";
        }
    },
]);

$success = curl_exec($ch);

if (!$success) {
    echo PHP_EOL . 'Errore nel download: ' . curl_error($ch);
}

curl_close($ch);
fclose($fp);

// Verifica della dimensione del file scaricato
$fileSize = filesize($outputPath);
echo PHP_EOL . "Download completato. Dimensione: {$fileSize} byte.";

L'opzione CURLOPT_FILE accetta un file handle aperto in modalità di scrittura e vi redirige tutto l'output della richiesta. La callback di progresso, attivata da CURLOPT_NOPROGRESS impostato a false, permette di monitorare lo stato del download in tempo reale.

Gestione degli header HTTP

Oltre ad inviare header personalizzati con CURLOPT_HTTPHEADER, è possibile catturare gli header della risposta per analizzarli.

<?php

$responseHeaders = [];

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://httpbin.org/response-headers?X-Custom=test',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADERFUNCTION => function ($ch, $headerLine) use (&$responseHeaders) {
        $length = strlen($headerLine);
        $parts  = explode(':', $headerLine, 2);

        // Ignora le righe che non contengono un header valido
        if (count($parts) < 2) {
            return $length;
        }

        $name  = strtolower(trim($parts[0]));
        $value = trim($parts[1]);

        $responseHeaders[$name] = $value;

        return $length; // Deve restituire la lunghezza della riga
    },
]);

$body = curl_exec($ch);
curl_close($ch);

echo 'Header ricevuti:' . PHP_EOL;
print_r($responseHeaders);

echo PHP_EOL . 'Corpo della risposta:' . PHP_EOL;
echo $body;

La callback impostata con CURLOPT_HEADERFUNCTION viene invocata per ogni riga di header ricevuta dal server. È importante che la funzione restituisca la lunghezza esatta della riga, altrimenti cURL interrompe il trasferimento segnalando un errore.

Autenticazione HTTP

cURL supporta nativamente diversi schemi di autenticazione. I più comuni sono Basic, Digest e Bearer Token.

<?php

// --- Autenticazione Basic ---
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://httpbin.org/basic-auth/user/passwd',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD        => 'user:passwd',        // Formato utente:password
    CURLOPT_HTTPAUTH       => CURLAUTH_BASIC,       // Schema Basic
]);

$response = curl_exec($ch);
curl_close($ch);

echo 'Basic Auth: ' . $response . PHP_EOL;


// --- Autenticazione Bearer Token ---
$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/protected/resource',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token, // Token JWT nell'header
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($httpCode === 401) {
    echo 'Token scaduto o non valido.';
} else {
    echo $response;
}

Con l'autenticazione Basic le credenziali vengono codificate in Base64 e inserite nell'header Authorization. Questo schema non offre alcuna protezione intrinseca: è sicuro solo se utilizzato su connessioni HTTPS. L'autenticazione Bearer, tipica delle API che usano OAuth 2.0 o JWT, richiede di passare il token direttamente nell'header.

Gestione dei cookie

Per mantenere una sessione tra più richieste è necessario gestire i cookie. cURL permette di salvarli su file e di rileggerli nelle richieste successive.

<?php

$cookieFile = tempnam(sys_get_temp_dir(), 'curl_cookies_');

// Prima richiesta: effettua il login e salva i cookie
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://example.com/login',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'username' => 'mario',
        'password' => 'segreto',
    ]),
    CURLOPT_COOKIEJAR      => $cookieFile, // Salva i cookie ricevuti
]);

curl_exec($ch);
curl_close($ch);

// Seconda richiesta: accede a una pagina protetta usando i cookie salvati
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://example.com/dashboard',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE     => $cookieFile, // Legge i cookie dal file
]);

$dashboard = curl_exec($ch);
curl_close($ch);

echo $dashboard;

// Pulizia del file temporaneo
unlink($cookieFile);

L'opzione CURLOPT_COOKIEJAR specifica il percorso del file in cui salvare i cookie al termine della richiesta, mentre CURLOPT_COOKIEFILE indica il file da cui leggerli prima di inviare la richiesta. Usando lo stesso file per entrambe le opzioni si ottiene una gestione trasparente della sessione.

Configurazione SSL/TLS

In ambiente di produzione è fondamentale verificare i certificati SSL del server. In fase di sviluppo, invece, potrebbe essere necessario disabilitare temporaneamente queste verifiche.

<?php

// --- Configurazione sicura per la produzione ---
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/secure',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true,                       // Verifica il certificato del server
    CURLOPT_SSL_VERIFYHOST => 2,                          // Controlla che l'hostname corrisponda
    CURLOPT_CAINFO         => '/etc/ssl/certs/cacert.pem', // Percorso al bundle CA
]);

$response = curl_exec($ch);
curl_close($ch);


// --- Configurazione per lo sviluppo locale (NON usare in produzione!) ---
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://localhost:8443/api/test',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false, // Disabilita la verifica del certificato
    CURLOPT_SSL_VERIFYHOST => 0,     // Disabilita la verifica dell'hostname
]);

$response = curl_exec($ch);
curl_close($ch);

Disabilitare la verifica SSL in produzione espone l'applicazione ad attacchi man-in-the-middle. Il bundle di certificati CA può essere scaricato dal sito del progetto cURL e aggiornato periodicamente. L'opzione CURLOPT_SSL_VERIFYHOST accetta il valore 2 (verifica che il Common Name o il Subject Alternative Name corrisponda all'hostname) oppure 0 (nessuna verifica).

Timeout e gestione degli errori

Una corretta gestione dei timeout e degli errori è essenziale per evitare che l'applicazione resti bloccata in attesa di risposte che non arriveranno mai.

<?php

/**
 * Esegue una richiesta HTTP con gestione completa degli errori.
 */
function safeRequest(string $url, int $timeout = 10): array
{
    $ch = curl_init();

    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,       // Timeout totale dell'operazione
        CURLOPT_CONNECTTIMEOUT => 5,              // Timeout per la connessione
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,              // Numero massimo di redirect consentiti
    ]);

    $response = curl_exec($ch);
    $info     = curl_getinfo($ch);

    // Verifica degli errori a livello di trasporto
    if (curl_errno($ch)) {
        $errorCode    = curl_errno($ch);
        $errorMessage = curl_error($ch);

        curl_close($ch);

        return [
            'success' => false,
            'error'   => "Errore cURL #{$errorCode}: {$errorMessage}",
        ];
    }

    curl_close($ch);

    // Verifica del codice di stato HTTP
    $httpCode = $info['http_code'];

    if ($httpCode >= 400) {
        return [
            'success' => false,
            'error'   => "Il server ha risposto con codice HTTP {$httpCode}.",
            'body'    => $response,
        ];
    }

    return [
        'success'      => true,
        'body'         => $response,
        'http_code'    => $httpCode,
        'total_time'   => $info['total_time'],       // Tempo totale in secondi
        'download_size' => $info['size_download'],    // Byte scaricati
    ];
}

// Esempio di utilizzo
$result = safeRequest('https://jsonplaceholder.typicode.com/posts/1');

if ($result['success']) {
    echo "Risposta ricevuta in {$result['total_time']}s" . PHP_EOL;
    echo $result['body'];
} else {
    echo $result['error'];
}

La distinzione tra CURLOPT_TIMEOUT e CURLOPT_CONNECTTIMEOUT è importante: il primo definisce il tempo massimo per l'intera operazione (connessione, invio e ricezione), mentre il secondo limita esclusivamente la fase di connessione al server. La funzione curl_getinfo() senza il secondo parametro restituisce un array associativo con decine di metriche utili per il debug e il monitoraggio delle prestazioni.

Richieste multiple con cURL Multi

Quando è necessario effettuare molte richieste contemporaneamente, l'interfaccia multi-handle di cURL permette di eseguirle in parallelo, riducendo drasticamente i tempi di attesa rispetto alle chiamate sequenziali.

<?php

// Lista di URL da richiedere in parallelo
$urls = [
    'posts'    => 'https://jsonplaceholder.typicode.com/posts?_limit=5',
    'users'    => 'https://jsonplaceholder.typicode.com/users?_limit=5',
    'comments' => 'https://jsonplaceholder.typicode.com/comments?_limit=5',
    'albums'   => 'https://jsonplaceholder.typicode.com/albums?_limit=5',
];

// Inizializzazione del multi-handle
$multiHandle = curl_multi_init();
$handles     = [];

// Creazione e aggiunta dei singoli handle
foreach ($urls as $key => $url) {
    $ch = curl_init();

    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
    ]);

    curl_multi_add_handle($multiHandle, $ch);
    $handles[$key] = $ch;
}

// Esecuzione parallela di tutte le richieste
$running = null;

do {
    $status = curl_multi_exec($multiHandle, $running);

    if ($status > CURLM_OK) {
        // Errore nel multi-handle
        break;
    }

    // Attende attività di rete per evitare un ciclo attivo che consuma CPU
    if ($running > 0) {
        curl_multi_select($multiHandle);
    }
} while ($running > 0);

// Raccolta dei risultati
$results = [];

foreach ($handles as $key => $ch) {
    $results[$key] = [
        'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
        'body'      => json_decode(curl_multi_getcontent($ch), true),
        'error'     => curl_error($ch),
    ];

    // Rimozione e chiusura del singolo handle
    curl_multi_remove_handle($multiHandle, $ch);
    curl_close($ch);
}

// Chiusura del multi-handle
curl_multi_close($multiHandle);

// Riepilogo dei risultati
foreach ($results as $key => $result) {
    $count = is_array($result['body']) ? count($result['body']) : 0;
    echo "{$key}: {$count} elementi (HTTP {$result['http_code']})" . PHP_EOL;
}

L'interfaccia multi di cURL funziona con un loop di esecuzione non bloccante. La funzione curl_multi_exec() avvia e prosegue le richieste senza attenderne il completamento, mentre curl_multi_select() mette il processo in attesa fino a quando non si verifica attività su almeno uno dei socket, evitando di saturare la CPU con un ciclo vuoto.

Creazione di un client HTTP riutilizzabile

Per progetti di una certa complessità è utile incapsulare la logica cURL in una classe dedicata, che centralizzi la configurazione e la gestione degli errori.

<?php

class HttpClient
{
    private string $baseUrl;
    private array $defaultHeaders;
    private int $timeout;

    public function __construct(
        string $baseUrl,
        array $defaultHeaders = [],
        int $timeout = 30
    ) {
        $this->baseUrl        = rtrim($baseUrl, '/');
        $this->defaultHeaders = $defaultHeaders;
        $this->timeout        = $timeout;
    }

    /**
     * Esegue una richiesta HTTP e restituisce un array strutturato.
     */
    public function request(
        string $method,
        string $endpoint,
        array $data = [],
        array $headers = []
    ): array {
        $url = $this->baseUrl . '/' . ltrim($endpoint, '/');
        $ch  = curl_init();

        // Unisce gli header predefiniti con quelli specifici della richiesta
        $mergedHeaders = array_merge($this->defaultHeaders, $headers);

        $options = [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST  => strtoupper($method),
            CURLOPT_HTTPHEADER     => $mergedHeaders,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_FOLLOWLOCATION => true,
        ];

        // Aggiunge il payload per i metodi che lo prevedono
        if (!empty($data) && in_array(strtoupper($method), ['POST', 'PUT', 'PATCH'])) {
            $options[CURLOPT_POSTFIELDS] = json_encode($data);
        }

        curl_setopt_array($ch, $options);

        $response  = curl_exec($ch);
        $errorCode = curl_errno($ch);
        $info      = curl_getinfo($ch);

        curl_close($ch);

        if ($errorCode !== 0) {
            throw new RuntimeException(
                "Richiesta {$method} {$url} fallita: " . curl_strerror($errorCode)
            );
        }

        return [
            'status'  => $info['http_code'],
            'body'    => json_decode($response, true) ?? $response,
            'time'    => round($info['total_time'], 3),
        ];
    }

    // Metodi di convenienza

    public function get(string $endpoint, array $headers = []): array
    {
        return $this->request('GET', $endpoint, [], $headers);
    }

    public function post(string $endpoint, array $data, array $headers = []): array
    {
        return $this->request('POST', $endpoint, $data, $headers);
    }

    public function put(string $endpoint, array $data, array $headers = []): array
    {
        return $this->request('PUT', $endpoint, $data, $headers);
    }

    public function patch(string $endpoint, array $data, array $headers = []): array
    {
        return $this->request('PATCH', $endpoint, $data, $headers);
    }

    public function delete(string $endpoint, array $headers = []): array
    {
        return $this->request('DELETE', $endpoint, [], $headers);
    }
}

// Esempio di utilizzo
$client = new HttpClient('https://jsonplaceholder.typicode.com', [
    'Content-Type: application/json',
    'Accept: application/json',
]);

try {
    // Recupero di una lista di post
    $posts = $client->get('/posts?_limit=3');
    echo "Trovati " . count($posts['body']) . " post in {$posts['time']}s" . PHP_EOL;

    // Creazione di un nuovo post
    $newPost = $client->post('/posts', [
        'title'  => 'Articolo dal client HTTP',
        'body'   => 'Creato con la classe HttpClient.',
        'userId' => 1,
    ]);
    echo "Nuovo post creato con stato: {$newPost['status']}" . PHP_EOL;

} catch (RuntimeException $e) {
    echo 'Errore: ' . $e->getMessage();
}

La classe HttpClient illustrata qui sopra è una versione semplificata ma funzionale. In un progetto reale si potrebbe arricchire con meccanismi di retry automatico, logging delle richieste, gestione di middleware per l'autenticazione e supporto per risposte in streaming.

Implementazione di un sistema di retry

Le connessioni di rete sono intrinsecamente instabili. Un meccanismo di retry con backoff esponenziale aumenta la resilienza dell'applicazione senza sovraccaricare il server.

<?php

/**
 * Esegue una richiesta GET con logica di retry e backoff esponenziale.
 */
function fetchWithRetry(
    string $url,
    int $maxRetries = 3,
    float $baseDelay = 1.0
): string {
    $attempt = 0;

    while ($attempt <= $maxRetries) {
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_CONNECTTIMEOUT => 5,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_errno($ch);

        curl_close($ch);

        // Successo: restituisce immediatamente la risposta
        if ($error === 0 && $httpCode >= 200 && $httpCode < 300) {
            return $response;
        }

        $attempt++;

        // Controlla se vale la pena riprovare
        $isRetryable = $error !== 0
            || in_array($httpCode, [408, 429, 500, 502, 503, 504]);

        if (!$isRetryable || $attempt > $maxRetries) {
            throw new RuntimeException(
                "Richiesta fallita dopo {$attempt} tentativi. "
                . "Ultimo codice HTTP: {$httpCode}. "
                . "Errore cURL: " . ($error ? curl_strerror($error) : 'nessuno')
            );
        }

        // Calcolo del ritardo con backoff esponenziale e jitter casuale
        $delay = $baseDelay * pow(2, $attempt - 1);
        $jitter = $delay * 0.2 * (mt_rand() / mt_getrandmax());
        $sleepTime = $delay + $jitter;

        echo "Tentativo {$attempt} fallito. Nuovo tentativo tra "
             . round($sleepTime, 2) . "s..." . PHP_EOL;

        usleep((int) ($sleepTime * 1_000_000));
    }

    // Non dovrebbe mai arrivare qui
    throw new RuntimeException('Numero massimo di tentativi superato.');
}

// Esempio di utilizzo
try {
    $data = fetchWithRetry('https://jsonplaceholder.typicode.com/posts/1');
    $post = json_decode($data, true);
    echo "Post recuperato: {$post['title']}";
} catch (RuntimeException $e) {
    echo $e->getMessage();
}

Il backoff esponenziale aumenta progressivamente l'intervallo tra un tentativo e l'altro (1s, 2s, 4s, ...), mentre il jitter aggiunge una componente casuale che impedisce a molti client di riprovare nello stesso istante, fenomeno noto come "thundering herd".

Invio di richieste con certificato client

Alcune API richiedono un'autenticazione basata su certificato client (mutual TLS). cURL supporta questa modalità tramite opzioni dedicate.

<?php

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.sicura.example.com/data',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSLCERT        => '/percorso/al/certificato.pem', // Certificato client
    CURLOPT_SSLCERTTYPE    => 'PEM',                          // Formato del certificato
    CURLOPT_SSLKEY         => '/percorso/alla/chiave.pem',    // Chiave privata
    CURLOPT_SSLKEYPASSWD   => 'passphrase_della_chiave',     // Password della chiave privata
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_CAINFO         => '/percorso/al/ca-bundle.pem',   // Certificati CA del server
]);

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Errore SSL: ' . curl_error($ch);
} else {
    echo $response;
}

curl_close($ch);

In questo scenario il server verifica l'identità del client attraverso il suo certificato, stabilendo un canale di comunicazione bidirezionalmente autenticato. I file del certificato e della chiave privata devono avere permessi restrittivi sul filesystem per evitare accessi non autorizzati.

Debug e ispezione delle richieste

Quando una richiesta non funziona come previsto, cURL offre uno strumento di debug interno che registra l'intera conversazione HTTP, compresi gli header scambiati e i dettagli della negoziazione TLS.

<?php

// Apertura di uno stream in memoria per il log di debug
$verboseLog = fopen('php://temp', 'w+');

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://httpbin.org/get',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_VERBOSE        => true,         // Abilita l'output dettagliato
    CURLOPT_STDERR         => $verboseLog,  // Redirige il log nel nostro stream
]);

$response = curl_exec($ch);
$info     = curl_getinfo($ch);

curl_close($ch);

// Lettura del log di debug
rewind($verboseLog);
$debugOutput = stream_get_contents($verboseLog);
fclose($verboseLog);

echo '=== Informazioni sulla richiesta ===' . PHP_EOL;
echo 'URL effettivo: '    . $info['url']           . PHP_EOL;
echo 'Codice HTTP: '      . $info['http_code']     . PHP_EOL;
echo 'Tempo totale: '     . $info['total_time']    . 's' . PHP_EOL;
echo 'Tempo DNS: '        . $info['namelookup_time'] . 's' . PHP_EOL;
echo 'Tempo connessione: '. $info['connect_time']  . 's' . PHP_EOL;
echo 'Byte scaricati: '   . $info['size_download'] . PHP_EOL;
echo 'IP del server: '    . $info['primary_ip']    . PHP_EOL;

echo PHP_EOL . '=== Log di debug ===' . PHP_EOL;
echo $debugOutput;

L'opzione CURLOPT_VERBOSE attiva un flusso di informazioni dettagliate che include la risoluzione DNS, la negoziazione TLS, gli header inviati e ricevuti e i dati trasferiti. Redirigendo questo flusso su uno stream con CURLOPT_STDERR si evita di inquinare lo standard output e si può analizzare il log in modo programmatico.

Uso dei proxy

cURL permette di instradare le richieste attraverso un server proxy, funzionalità utile per il web scraping, il bypass di restrizioni geografiche o l'anonimizzazione del traffico.

<?php

$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://httpbin.org/ip',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_PROXY          => 'http://proxy.example.com:8080',  // Indirizzo del proxy
    CURLOPT_PROXYUSERPWD   => 'utente:password',                // Credenziali del proxy
    CURLOPT_PROXYTYPE      => CURLPROXY_HTTP,                   // Tipo di proxy
]);

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Errore proxy: ' . curl_error($ch);
} else {
    echo 'IP visibile: ' . $response;
}

curl_close($ch);

I tipi di proxy supportati includono HTTP (CURLPROXY_HTTP), HTTPS (CURLPROXY_HTTPS), SOCKS4 (CURLPROXY_SOCKS4) e SOCKS5 (CURLPROXY_SOCKS5). Per i proxy SOCKS5 con risoluzione DNS remota si utilizza la costante CURLPROXY_SOCKS5_HOSTNAME.

Best practice e raccomandazioni

Lavorare con cURL in modo professionale richiede attenzione ad alcuni aspetti trasversali che non emergono dai singoli esempi ma sono decisivi in produzione.

Chiudere sempre gli handle con curl_close() al termine di ogni richiesta previene accumuli di memoria, soprattutto in script che restano in esecuzione a lungo, come i worker di una coda. Lo stesso vale per i file handle aperti con fopen() per il download o il logging.

Non disabilitare mai la verifica SSL in produzione. Se il certificato del server causa errori, la soluzione corretta è aggiornare il bundle dei certificati CA, non abbassare il livello di sicurezza. L'opzione CURLOPT_CAINFO permette di specificare un bundle personalizzato senza modificare la configurazione globale di PHP.

Impostare sempre un timeout ragionevole. Una richiesta senza timeout può bloccare l'intero processo per un tempo indefinito. Valori tra 5 e 30 secondi sono appropriati per la maggior parte delle API; per il download di file di grandi dimensioni si possono usare valori più alti.

Per applicazioni che necessitano di centinaia di richieste al secondo, l'interfaccia multi-handle è preferibile a quella classica. In alternativa, librerie come Guzzle offrono un'astrazione di livello superiore costruita sopra cURL, con un'interfaccia basata su promise e un ecosistema di middleware.

Controllare sempre sia gli errori di trasporto (con curl_errno()) sia il codice di stato HTTP (con curl_getinfo()). Un curl_exec() che restituisce una stringa non vuota non garantisce il successo dell'operazione: il server potrebbe aver risposto con un codice 4xx o 5xx e un messaggio di errore nel body.

Registrare le richieste in un log strutturato, includendo URL, metodo, codice di risposta e tempo di esecuzione, facilita enormemente il debug di problemi intermittenti e il monitoraggio delle prestazioni nel tempo.