
curl_init(), curl_setopt_array() com CURLOPT_RETURNTRANSFER e depois curl_exec(), cujo retorno tem de ser sempre testado. Desde o PHP 8.5, curl_close() está depreciada: o handle é um objeto libertado automaticamente. Um código HTTP 404 ou 500 não é um erro do cURL, é preciso ler CURLINFO_RESPONSE_CODE à parte.
O cURL é a forma mais direta de chamar uma API a partir do PHP. A extensão está presente em quase todos os alojamentos e trata do HTTP, dos redirecionamentos, dos cookies e do TLS. Este tutorial percorre os casos correntes, GET, POST, cabeçalhos, cookies, token Bearer, com a gestão de erros que a maior parte dos exemplos esquece, e assinala o que mudou no PHP 8.5.
O que mudou: curl_close() está depreciada
Desde o PHP 8.0, curl_init() já não devolve um recurso, mas um objeto CurlHandle, libertado automaticamente quando a variável sai de âmbito. curl_close() deixou portanto de ter efeito há cinco anos. O PHP 8.5 assume a situação e deprecia a função.
$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.0Medido em PHP 8.5.10 com cURL 8.14.1. Em PHP 8.4.25, a mesma linha não produz aviso nenhum: o código continua válido num alojamento partilhado que tenha ficado no 8.4, mas mais vale retirar já estas chamadas do que voltar a elas mais tarde. Para libertar um handle antes do fim do script, unset($ch) faz o trabalho.
Um pedido GET
Os parâmetros do URL constroem-se com http_build_query(), que codifica os valores. Concatená-los à mão parte assim que um valor contém um espaço, um & ou um acento.
<?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 faz com que curl_exec() devolva a resposta em vez de a escrever na saída padrão. Sem isso, a resposta é impressa a meio da tua página.
Verificar sempre o retorno
É o esquecimento mais caro. curl_exec() devolve false quando a rede falha, e o script segue como se nada 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.invalidUm json_decode($resultat) a seguir devolveria null e o erro apareceria três ecrãs mais à frente. Duas linhas de verificação chegam para o fazer aparecer onde nasceu.
Atenção: um código HTTP 404 ou 500 não é um erro do cURL. O pedido chegou ao destino, o servidor respondeu, curl_exec() devolve o corpo da página de erro. É preciso ler o código de resposta à parte, ou ativar CURLOPT_FAILONERROR para que o cURL trate as respostas 400 e acima como falhas.
Um pedido 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);A forma de CURLOPT_POSTFIELDS decide o tipo de conteúdo enviado, e é uma fonte de confusão permanente:
- uma string (
http_build_query()) segue emapplication/x-www-form-urlencoded, o formato de um formulário HTML clássico. - um array segue em
multipart/form-data, com fronteiras MIME. É o formato dos envios de ficheiros, e muitas API recusam-no.
Para enviar JSON, o tipo tem de ser declarado explicitamente:
$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),
],
]);Acrescentar cabeçalhos
Os cabeçalhos passam-se num array de strings Nom: valeur, através de 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);Um User-Agent explícito evita bloqueios: muitos servidores rejeitam o agente por omissão do cURL, ou a ausência de agente.
Autenticação por 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,
]);Um token não se escreve no código-fonte. Vem de uma variável de ambiente ou de um cofre. E só viaja em HTTPS: num URL em http://, o token circula às claras.
Os cookies
CURLOPT_COOKIE envia cookies fixos. Para uma sessão que tenha de guardar os cookies entre vários pedidos, precisas de CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE, com um ficheiro temporário.
$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, // escreve os cookies recebidos
CURLOPT_COOKIEFILE => $cookies, // devolve-os ao pedido seguinte
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);
// Segundo pedido: os cookies de sessão seguem sozinhos
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);
unlink($cookies);Nunca desativar a verificação TLS
Perante um erro de certificado, a resposta encontrada nos fóruns é sempre a mesma:
// A não fazer
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);Estas duas linhas eliminam a única proteção contra a interceção. Qualquer intermediário na rede pode então ler e alterar as trocas, token de autenticação incluído. O erro significa quase sempre que o repositório de certificados da máquina está em falta ou desatualizado. A correção certa é apontar curl.cainfo para um ficheiro de certificados atualizado no php.ini, ou atualizar o pacote ca-certificates do sistema.
O esqueleto reutilizável
Em vez de copiar as mesmas opções por todo o lado, uma pequena função concentra as definições seguras.
<?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 => '', // aceita 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),
);
}Chamada sobre a API REST do WordPress em local, esta função dá:
HTTP 200, 294919 octets, 1.580 sVários pedidos em paralelo
Dez chamadas sequenciais de 200 ms custam dois segundos. As mesmas em paralelo custam a mais lenta das dez. curl_multi_* serve exatamente para isso.
$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 andar a girar em vazio no processador
}
} 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);O curl_multi_select() é indispensável: sem ele, o ciclo ocupa um núcleo a 100 % durante todo o tempo dos pedidos.
Em três pontos da API REST do WordPress em local, medido três vezes depois de uma volta de aquecimento:
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 msO ganho cresce com o número de chamadas e com a latência de cada uma. Em três pedidos locais já rápidos, o paralelo divide o tempo por dois, em dez chamadas a uma API remota, a diferença torna-se bem mais nítida.
Quando não usar o cURL diretamente
Para uma chamada pontual, o cURL faz o trabalho. Assim que o projeto encadeia integrações, uma biblioteca cliente como o Guzzle ou um cliente PSR-18 traz as retentativas, a gestão dos fluxos e os intercetores sem que os tenhas de reescrever. E se a extensão cURL não estiver disponível, file_get_contents() com um contexto de fluxo safa um GET simples, mas sem controlo fino dos tempos limite nem diagnóstico de erro aproveitável.
O cURL cobre as trocas pedido-resposta. Quando é o servidor que tem de empurrar dados por iniciativa própria, é preciso outro transporte: vê criar um servidor WebSocket em PHP. Para chamar a API de um serviço de envio de e-mails, vê enviar um e-mail com PHP. E para passar a resposta ao browser, passar variáveis de PHP para JavaScript.
Vê também a ligação a uma base de dados em PHP e o hub Desenvolvimento web.
Erros frequentes
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Nenhum aviso em PHP 8.4.25. O handle é um objeto CurlHandle libertado sozinho, usa unset() se for preciso.false e o script continua. Um json_decode(false) mais à frente dá null, e o erro aparece num sítio que nada tem a ver.n404 confundido com um erro do cURL | Uma resposta 404 é um sucesso para o cURL. Lê CURLINFO_RESPONSE_CODE, ou ativa CURLOPT_FAILONERROR.multipart/form-data, uma string produz application/x-www-form-urlencoded. Verificado com CURLINFO_HEADER_OUT. Muitas API recusam o primeiro.curl.cainfo ou o pacote ca-certificates.ncurl_multi sem curl_multi_select | O ciclo ocupa um núcleo a 100 % durante todo o tempo dos pedidos.

