
curl_init(), curl_setopt_array() con CURLOPT_RETURNTRANSFER, poi curl_exec() di cui va sempre verificato il valore restituito. Da PHP 8.5 curl_close() è deprecata: l'handle è un oggetto liberato automaticamente. Un codice HTTP 404 o 500 non è un errore cURL, il codice va letto a parte con CURLINFO_RESPONSE_CODE.
cURL è il modo più diretto per chiamare un’API da PHP. L’estensione è presente su quasi tutti gli hosting e gestisce HTTP, i redirect, i cookie e TLS. Questo tutorial copre i casi ricorrenti, GET, POST, header, cookie, token Bearer, con la gestione degli errori che la maggior parte degli esempi dimentica, e segnala che cosa è cambiato in PHP 8.5.
Che cosa è cambiato: curl_close() è deprecata
Da PHP 8.0 curl_init() non restituisce più una risorsa ma un oggetto CurlHandle, liberato automaticamente quando la variabile esce dallo scope. curl_close() non aveva quindi più alcun effetto da cinque anni. PHP 8.5 prende atto della situazione e deprecia la funzione.
$ch = curl_init();
var_dump(get_debug_type($ch)); // string(10) "CurlHandle"
var_dump(is_resource($ch)); // bool(false)
curl_close($ch);Deprecated: Function curl_close() is deprecated since 8.5,
as it has no effect since PHP 8.0Misurato su PHP 8.5.10 con cURL 8.14.1. Su PHP 8.4.25 la stessa riga non produce alcun avviso: il codice resta valido su un hosting condiviso rimasto alla 8.4, ma è meglio togliere queste chiamate adesso che tornarci più avanti. Per liberare un handle prima della fine dello script, unset($ch) fa il lavoro.
Una richiesta GET
I parametri dell’URL si costruiscono con http_build_query(), che codifica i valori. Concatenarli a mano si rompe non appena un valore contiene uno spazio, una & o una lettera accentata.
<?php
declare(strict_types=1);
$parametres = ['recherche' => 'café & thé', 'page' => 2];
$url = 'https://api.example.com/articles?' . http_build_query($parametres);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
]);
$corps = curl_exec($ch);
if ($corps === false) {
throw new RuntimeException('cURL : ' . curl_error($ch), curl_errno($ch));
}
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
echo "HTTP {$code}, " . strlen($corps) . " octets\n";CURLOPT_RETURNTRANSFER a true fa restituire la risposta da curl_exec() invece di scriverla sullo standard output. Senza, la risposta finisce stampata in mezzo alla tua pagina.
Verificare sempre il valore restituito
È la dimenticanza più costosa. curl_exec() restituisce false in caso di errore di rete, e lo script prosegue come se niente fosse.
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://hote-qui-nexiste-pas.invalid/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$resultat = curl_exec($ch);
var_dump($resultat); // bool(false)
echo curl_errno($ch), "\n"; // 6
echo curl_error($ch), "\n"; // Could not resolve host: hote-qui-nexiste-pas.invalidUn json_decode($resultat) subito dopo restituirebbe null, e l’errore si manifesterebbe tre schermate più in là. Due righe di controllo bastano a farlo emergere dove nasce.
Attenzione: un codice HTTP 404 o 500 non è un errore cURL. La richiesta è andata a buon fine, il server ha risposto, curl_exec() restituisce il corpo della pagina di errore. Il codice di risposta va letto a parte, oppure va attivato CURLOPT_FAILONERROR perché cURL tratti le risposte dalla 400 in su come fallimenti.
Una richiesta POST
$donnees = ['nom' => 'Damien', 'message' => 'Bonjour & merci'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.example.com/contact',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($donnees),
CURLOPT_TIMEOUT => 15,
]);
$reponse = curl_exec($ch);La forma di CURLOPT_POSTFIELDS decide il tipo di contenuto inviato, ed è una fonte di confusione permanente:
- una stringa (
http_build_query()) parte comeapplication/x-www-form-urlencoded, il formato di un normale form HTML. - un array parte come
multipart/form-data, con i boundary MIME. È il formato dell’invio di file, e molte API lo rifiutano.
Per inviare del JSON, il tipo va dichiarato esplicitamente:
$charge = json_encode(['nom' => 'Damien'], JSON_THROW_ON_ERROR);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $charge,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
'Content-Length: ' . strlen($charge),
],
]);Aggiungere degli header
Gli header si passano come array di stringhe Nome: valore, tramite CURLOPT_HTTPHEADER.
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.example.com/profil',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Custom-Header: MaValeur',
'Accept: application/json',
'User-Agent: Gekkode/1.0 (+https://www.gekkode.com)',
],
]);
$reponse = curl_exec($ch);Uno User-Agent esplicito evita i blocchi: molti server rifiutano l’agent predefinito di cURL, o la sua assenza.
Autenticazione con token Bearer
$jeton = getenv('API_TOKEN') ?: throw new RuntimeException('API_TOKEN manquant');
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.example.com/data',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $jeton],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);Un token non si scrive nel codice sorgente. Arriva da una variabile d’ambiente o da un vault. E viaggia solo su HTTPS: su un URL in http:// il token passa in chiaro.
I cookie
CURLOPT_COOKIE invia cookie fissi. Per una sessione che deve conservare i cookie tra più richieste servono CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE, con un file temporaneo.
$cookies = tempnam(sys_get_temp_dir(), 'ck');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://example.com/connexion',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_COOKIEJAR => $cookies, // scrive i cookie ricevuti
CURLOPT_COOKIEFILE => $cookies, // li rimanda alla richiesta successiva
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);
// Seconda richiesta: i cookie di sessione partono da soli
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);
unlink($cookies);Non disattivare mai la verifica TLS
Davanti a un errore di certificato, la risposta che si trova sui forum è sempre la stessa:
// Da non fare
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);Queste due righe eliminano l’unica protezione contro l’intercettazione. Qualsiasi intermediario di rete può allora leggere e modificare gli scambi, token di autenticazione compreso. L’errore significa quasi sempre che l’archivio dei certificati della macchina è assente o scaduto. La correzione giusta è far puntare curl.cainfo a un file di certificati aggiornato in php.ini, oppure aggiornare il pacchetto ca-certificates del sistema.
Lo scheletro riutilizzabile
Invece di ricopiare le stesse opzioni ovunque, una piccola funzione concentra le impostazioni sicure.
<?php
declare(strict_types=1);
final class ReponseHttp
{
public function __construct(
public readonly int $code,
public readonly string $corps,
public readonly float $duree,
) {}
public function json(): array
{
return json_decode($this->corps, true, 512, JSON_THROW_ON_ERROR);
}
}
function appel(
string $url,
string $methode = 'GET',
?string $corps = null,
array $entetes = [],
int $delai = 15,
): ReponseHttp {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $methode,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => $delai,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => $entetes,
CURLOPT_ENCODING => '', // accetta gzip e deflate
]);
if ($corps !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $corps);
}
$reponse = curl_exec($ch);
if ($reponse === false) {
throw new RuntimeException(
sprintf('cURL %d sur %s : %s', curl_errno($ch), $url, curl_error($ch)),
curl_errno($ch),
);
}
return new ReponseHttp(
code: curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
corps: $reponse,
duree: curl_getinfo($ch, CURLINFO_TOTAL_TIME),
);
}Chiamata sull’API REST di WordPress in locale, questa funzione restituisce:
HTTP 200, 294919 octets, 1.580 sPiù richieste in parallelo
Dieci chiamate sequenziali da 200 ms costano due secondi. Le stesse in parallelo costano quanto la più lenta delle dieci. curl_multi_* serve esattamente a questo.
$urls = [
'https://api.example.com/a',
'https://api.example.com/b',
'https://api.example.com/c',
];
$multi = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
curl_multi_add_handle($multi, $ch);
$handles[$i] = $ch;
}
do {
$etat = curl_multi_exec($multi, $encoursDExecution);
if ($encoursDExecution) {
curl_multi_select($multi); // evita di girare a vuoto sul processore
}
} while ($encoursDExecution && $etat === CURLM_OK);
$reponses = [];
foreach ($handles as $i => $ch) {
$reponses[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);Il curl_multi_select() è indispensabile: senza, il ciclo tiene un core al 100 % per tutta la durata delle richieste.
Su tre endpoint dell’API REST di WordPress in locale, misurato tre volte dopo un giro di riscaldamento:
passe 1 : séquentiel 1367 ms | parallèle 459 ms
passe 2 : séquentiel 868 ms | parallèle 415 ms
passe 3 : séquentiel 733 ms | parallèle 408 msIl guadagno cresce con il numero di chiamate e con la latenza di ciascuna. Su tre richieste locali già veloci il parallelo dimezza il tempo, su dieci chiamate a un’API remota lo scarto diventa molto più netto.
Quando non usare cURL direttamente
Per una chiamata occasionale cURL fa il suo lavoro. Non appena il progetto accumula integrazioni, una libreria client come Guzzle o un client PSR-18 porta i retry, la gestione degli stream e gli interceptor senza doverli riscrivere. E se l’estensione cURL non è disponibile, file_get_contents() con un contesto di stream toglie d’impaccio per un GET semplice, ma senza timeout fini né diagnostica d’errore utilizzabile.
cURL copre gli scambi richiesta-risposta. Quando è il server a dover inviare dati di sua iniziativa serve un altro trasporto: vedi creare un server WebSocket in PHP. Per chiamare l’API di un servizio di invio e-mail, vedi inviare un’e-mail con PHP. E per passare la risposta al browser, passare variabili da PHP a JavaScript.
Vedi anche la connessione a un database in PHP e l’hub Sviluppo web.
Errori frequenti
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Nessun avviso su PHP 8.4.25. L'handle è un oggetto CurlHandle liberato da solo, usa unset() se serve.false e lo script prosegue. Un json_decode(false) più avanti dà null, e l'errore emerge in un punto che non c'entra niente.n404 confuso con un errore cURL | Una risposta 404 per cURL è un successo. Leggi CURLINFO_RESPONSE_CODE, oppure attiva CURLOPT_FAILONERROR.multipart/form-data, una stringa produce application/x-www-form-urlencoded. Verificato con CURLINFO_HEADER_OUT. Molte API rifiutano il primo.curl.cainfo o il pacchetto ca-certificates.ncurl_multi senza curl_multi_select | Il ciclo tiene un core al 100 % per tutta la durata delle richieste.

