
curl_init(), curl_setopt_array() met CURLOPT_RETURNTRANSFER, daarna curl_exec(), waarvan je de returnwaarde altijd moet testen. Sinds PHP 8.5 is curl_close() deprecated: de handle is een object dat vanzelf wordt vrijgegeven. Een HTTP-code 404 of 500 is geen cURL-fout, je leest CURLINFO_RESPONSE_CODE apart uit.
cURL is de meest directe manier om vanuit PHP een API aan te roepen. De extensie staat op vrijwel elke hosting en regelt HTTP, redirects, cookies en TLS. Deze tutorial neemt de gangbare gevallen door, GET, POST, headers, cookies, Bearer-token, met de foutafhandeling die de meeste voorbeelden weglaten, en wijst aan wat er met PHP 8.5 is veranderd.
Wat er is veranderd: curl_close() is deprecated
Sinds PHP 8.0 geeft curl_init() geen resource meer terug maar een object CurlHandle, dat vanzelf wordt vrijgegeven zodra de variabele buiten scope raakt. curl_close() had dus al vijf jaar geen effect meer. PHP 8.5 trekt daar de conclusie uit en markeert de functie als deprecated.
$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.0Gemeten op PHP 8.5.10 met cURL 8.14.1. Op PHP 8.4.25 levert dezelfde regel geen enkele melding op: de code blijft geldig op een shared hosting die op 8.4 is blijven staan, maar je haalt die aanroepen beter nu weg dan er later op terug te komen. Wil je een handle vrijgeven voor het einde van het script, dan doet unset($ch) het werk.
Een GET-request
URL-parameters bouw je met http_build_query(), dat de waarden encodeert. Ze met de hand aan elkaar plakken breekt zodra een waarde een spatie, een & of een accent bevat.
<?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 op true zorgt dat curl_exec() het antwoord teruggeeft in plaats van het naar de standaarduitvoer te schrijven. Zonder die optie belandt het antwoord midden in je pagina.
Test altijd de returnwaarde
Dit is de duurste vergetelheid. curl_exec() geeft false terug bij een netwerkfout, en het script loopt door alsof er niets aan de hand is.
$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.invalidEen json_decode($resultat) erachter zou null opleveren, en de fout zou pas drie schermen verderop opduiken. Twee regels controle zijn genoeg om hem bij de bron te laten opspelen.
Let op: een HTTP-code 404 of 500 is geen cURL-fout. Het request is geslaagd, de server heeft geantwoord, curl_exec() geeft de body van de foutpagina terug. Je moet de responscode apart uitlezen, of CURLOPT_FAILONERROR aanzetten zodat cURL antwoorden vanaf 400 als mislukking behandelt.
Een POST-request
$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);De vorm van CURLOPT_POSTFIELDS bepaalt welk contenttype er vertrekt, en dat is een blijvende bron van verwarring:
- een string (
http_build_query()) gaat weg alsapplication/x-www-form-urlencoded, het formaat van een klassiek HTML-formulier. - een array gaat weg als
multipart/form-data, met MIME-boundaries. Dat is het formaat voor bestandsuploads, en veel API’s weigeren het.
Om JSON te versturen moet het type expliciet worden opgegeven:
$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),
],
]);Headers toevoegen
Headers geef je via CURLOPT_HTTPHEADER mee als array van strings in de vorm Naam: waarde.
$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);Een expliciete User-Agent voorkomt blokkades: veel servers weigeren de standaardagent van cURL, of het ontbreken van een agent.
Authenticatie met een Bearer-token
$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,
]);Een token schrijf je niet in de broncode. Hij komt uit een omgevingsvariabele of uit een vault. En hij reist alleen over HTTPS: op een URL met http:// gaat het token leesbaar over de lijn.
Cookies
CURLOPT_COOKIE stuurt vaste cookies mee. Voor een sessie die de cookies over meerdere requests moet bewaren heb je CURLOPT_COOKIEJAR en CURLOPT_COOKIEFILE nodig, met een tijdelijk bestand.
$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, // schrijft de ontvangen cookies weg
CURLOPT_COOKIEFILE => $cookies, // stuurt ze mee met het volgende request
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);
// Tweede request: de sessiecookies gaan vanzelf mee
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);
unlink($cookies);Zet de TLS-verificatie nooit uit
Bij een certificaatfout is het antwoord op de forums altijd hetzelfde:
// Zo moet het niet
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);Die twee regels halen de enige bescherming tegen onderschepping weg. Elke tussenpartij op het netwerk kan het verkeer dan lezen en aanpassen, authenticatietoken inbegrepen. De fout betekent bijna altijd dat de certificaatstore van de machine ontbreekt of verlopen is. De juiste oplossing is curl.cainfo in php.ini naar een actueel certificaatbestand laten wijzen, of het systeempakket ca-certificates bijwerken.
Het herbruikbare skelet
In plaats van overal dezelfde opties over te tikken, bundelt een kleine functie de veilige instellingen.
<?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 => '', // accepteert gzip en 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),
);
}Aangeroepen op de lokale REST API van WordPress geeft deze functie:
HTTP 200, 294919 octets, 1.580 sMeerdere requests parallel
Tien opeenvolgende aanroepen van 200 ms kosten twee seconden. Dezelfde aanroepen parallel kosten de traagste van de tien. Daar dient curl_multi_* precies voor.
$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); // voorkomt dat de processor leeg draait
}
} 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);De curl_multi_select() is onmisbaar: zonder die aanroep houdt de lus een core op 100 % bezig zolang de requests duren.
Op drie endpoints van de lokale REST API van WordPress, drie keer gemeten na een warmdraaironde:
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 msDe winst groeit met het aantal aanroepen en met de latency van elk ervan. Op drie lokale requests die al snel zijn, halveert parallel de tijd, bij tien aanroepen naar een externe API wordt het verschil veel duidelijker.
Wanneer je cURL niet rechtstreeks gebruikt
Voor een losse aanroep doet cURL het werk. Zodra een project integratie op integratie stapelt, levert een clientbibliotheek als Guzzle of een PSR-18-client de retries, het streambeheer en de interceptors zonder dat je ze zelf hoeft te schrijven. En is de cURL-extensie niet beschikbaar, dan helpt file_get_contents() met een stream context je uit de brand voor een simpele GET, maar zonder fijnmazige timeouts en zonder bruikbare foutdiagnose.
cURL dekt de uitwisseling van request en response. Moet de server uit zichzelf data pushen, dan heb je een ander transport nodig: zie een WebSocket-server maken in PHP. Voor het aanroepen van de API van een dienst die e-mail verstuurt, zie een e-mail versturen met PHP. En om het antwoord door te geven aan de browser: variabelen van PHP naar JavaScript doorgeven.
Zie ook de verbinding met een database in PHP en de hub Webdevelopment.
Veelgemaakte fouten
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Geen enkele melding op PHP 8.4.25. De handle is een object CurlHandle dat vanzelf wordt vrijgegeven, gebruik unset() als het nodig is.false terug en loopt het script door. Een json_decode(false) verderop levert null op, en de fout duikt op een plek op die er niets mee te maken heeft.n404 verward met een cURL-fout | Een 404-antwoord is voor cURL een succes. Lees CURLINFO_RESPONSE_CODE, of zet CURLOPT_FAILONERROR aan.multipart/form-data op, een string application/x-www-form-urlencoded. Gecontroleerd met CURLINFO_HEADER_OUT. Veel API's weigeren het eerste.curl.cainfo of het pakket ca-certificates.ncurl_multi zonder curl_multi_select | De lus houdt een core op 100 % bezig zolang de requests duren.

