
curl_init(), curl_setopt_array() avec CURLOPT_RETURNTRANSFER, puis curl_exec() dont il faut toujours tester le retour. Depuis PHP 8.5, curl_close() est déprécié : le handle est un objet libéré automatiquement. Un code HTTP 404 ou 500 n'est pas une erreur cURL, il faut lire CURLINFO_RESPONSE_CODE séparément.
cURL est le moyen le plus direct d’appeler une API depuis PHP. L’extension est présente sur presque tous les hébergements, elle gère HTTP, les redirections, les cookies et TLS. Ce tutoriel reprend les cas courants, GET, POST, en-têtes, cookies, jeton Bearer, avec la gestion d’erreur que la plupart des exemples oublient, et signale ce qui a changé sur PHP 8.5.
Ce qui a changé : curl_close() est déprécié
Depuis PHP 8.0, curl_init() ne renvoie plus une ressource mais un objet CurlHandle, libéré automatiquement quand la variable sort de portée. curl_close() n’avait donc plus d’effet depuis cinq ans. PHP 8.5 acte la situation et déprécie la fonction.
$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.0Mesuré sur PHP 8.5.10 avec cURL 8.14.1. Sur PHP 8.4.25, la même ligne ne produit aucun avis : le code reste valide sur un hébergement mutualisé resté en 8.4, mais il vaut mieux retirer ces appels maintenant que d’y revenir plus tard. Pour libérer un handle avant la fin du script, unset($ch) fait le travail.
Une requête GET
Les paramètres d’URL se construisent avec http_build_query(), qui encode les valeurs. Les concaténer à la main casse dès qu’une valeur contient un espace, un & ou un accent.
<?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 à true fait renvoyer la réponse par curl_exec() au lieu de l’écrire sur la sortie standard. Sans lui, la réponse s’imprime au milieu de votre page.
Toujours vérifier le retour
C’est l’oubli le plus coûteux. curl_exec() renvoie false en cas d’échec réseau, et le script continue comme si de rien n’était.
$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) derrière renverrait null, et l’erreur se manifesterait trois écrans plus loin. Deux lignes de vérification suffisent à la faire remonter à sa source.
Attention : un code HTTP 404 ou 500 n’est pas une erreur cURL. La requête a abouti, le serveur a répondu, curl_exec() renvoie le corps de la page d’erreur. Il faut lire le code de réponse séparément, ou activer CURLOPT_FAILONERROR pour que cURL traite les réponses 400 et au-delà comme des échecs.
Une requête 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 forme de CURLOPT_POSTFIELDS décide du type de contenu envoyé, et c’est une source de confusion permanente :
- une chaîne (
http_build_query()) part enapplication/x-www-form-urlencoded, le format d’un formulaire HTML classique. - un tableau part en
multipart/form-data, avec des frontières MIME. C’est le format des envois de fichiers, et beaucoup d’API le refusent.
Pour envoyer du JSON, le type doit être déclaré explicitement :
$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),
],
]);Ajouter des en-têtes
Les en-têtes se passent en tableau de chaînes Nom: valeur, via 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);Un User-Agent explicite évite les blocages : beaucoup de serveurs rejettent l’agent par défaut de cURL, ou l’absence d’agent.
Authentification par jeton 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 jeton ne s’écrit pas dans le code source. Il vient d’une variable d’environnement ou d’un coffre. Et il ne voyage que sur HTTPS : sur une URL en http://, le jeton circule en clair.
Les cookies
CURLOPT_COOKIE envoie des cookies fixes. Pour une session qui doit conserver les cookies entre plusieurs requêtes, ce sont CURLOPT_COOKIEJAR et CURLOPT_COOKIEFILE qu’il faut, avec un fichier temporaire.
$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, // écrit les cookies reçus
CURLOPT_COOKIEFILE => $cookies, // les renvoie à la requête suivante
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);
// Deuxième requête : les cookies de session partent tout seuls
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);
unlink($cookies);Ne jamais désactiver la vérification TLS
Devant une erreur de certificat, la réponse trouvée sur les forums est toujours la même :
// À ne pas faire
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);Ces deux lignes suppriment la seule protection contre l’interception. N’importe quel intermédiaire du réseau peut alors lire et modifier les échanges, jeton d’authentification compris. L’erreur signifie presque toujours que le magasin de certificats de la machine est absent ou périmé. La bonne correction est de pointer curl.cainfo vers un fichier de certificats à jour dans php.ini, ou de mettre à jour le paquet ca-certificates du système.
Le squelette réutilisable
Plutôt que de recopier les mêmes options partout, une petite fonction concentre les réglages sûrs.
<?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 => '', // accepte gzip et 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),
);
}Appelée sur l’API REST de WordPress en local, cette fonction donne :
HTTP 200, 294919 octets, 1.580 sPlusieurs requêtes en parallèle
Dix appels séquentiels de 200 ms coûtent deux secondes. Les mêmes en parallèle coûtent le plus lent des dix. curl_multi_* sert exactement à ça.
$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); // évite de tourner à vide sur le processeur
}
} 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);Le curl_multi_select() est indispensable : sans lui, la boucle occupe un cœur à 100 % pendant toute la durée des requêtes.
Sur trois points de l’API REST de WordPress en local, mesuré trois fois après un tour de chauffe :
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 msLe gain grandit avec le nombre d’appels et avec la latence de chacun. Sur trois requêtes locales déjà rapides, le parallèle divise le temps par deux, sur dix appels à une API distante, l’écart devient bien plus net.
Quand ne pas utiliser cURL directement
Pour un appel ponctuel, cURL fait le travail. Dès que le projet enchaîne les intégrations, une bibliothèque cliente comme Guzzle ou un client PSR-18 apporte les réessais, la gestion des flux et les intercepteurs sans qu’on les réécrive. Et si l’extension cURL n’est pas disponible, file_get_contents() avec un contexte de flux dépanne pour un GET simple, mais sans délai fin ni diagnostic d’erreur exploitable.
cURL couvre les échanges requête-réponse. Quand le serveur doit pousser des données de lui-même, il faut un autre transport : voir créer un serveur WebSocket en PHP. Pour appeler l’API d’un service d’envoi d’e-mails, voir envoyer un e-mail avec PHP. Et pour transmettre la réponse au navigateur, passer des variables de PHP à JavaScript.
Voir aussi la connexion à une base de données en PHP et le hub Développement web.
Erreurs fréquentes
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Aucun avis sur PHP 8.4.25. Le handle est un objet CurlHandle libéré tout seul, utilisez unset() si nécessaire.false et le script continue. Un json_decode(false) plus loin donne null, et l'erreur remonte à un endroit qui n'a rien à voir.n404 confondu avec une erreur cURL | Une réponse 404 est un succès pour cURL. Lire CURLINFO_RESPONSE_CODE, ou activer CURLOPT_FAILONERROR.multipart/form-data, une chaîne produit application/x-www-form-urlencoded. Vérifié avec CURLINFO_HEADER_OUT. Beaucoup d'API refusent le premier.curl.cainfo ou le paquet ca-certificates.ncurl_multi sans curl_multi_select | La boucle occupe un cœur à 100 % pendant toute la durée des requêtes.

