
curl_init(), curl_setopt_array() z CURLOPT_RETURNTRANSFER, a potem curl_exec(), którego zwracaną wartość trzeba zawsze sprawdzić. Od PHP 8.5 curl_close() jest przestarzałe: uchwyt to obiekt zwalniany automatycznie. Kod HTTP 404 czy 500 nie jest błędem cURL, trzeba osobno odczytać CURLINFO_RESPONSE_CODE.
cURL to najprostszy sposób, żeby odpytać API z poziomu PHP. Rozszerzenie jest dostępne na niemal każdym hostingu, obsługuje HTTP, przekierowania, ciasteczka i TLS. Ten poradnik przechodzi przez typowe przypadki, GET, POST, nagłówki, ciasteczka, token Bearer, razem z obsługą błędów, którą większość przykładów pomija, i pokazuje, co zmieniło się w PHP 8.5.
Co się zmieniło: curl_close() jest przestarzałe
Od PHP 8.0 curl_init() nie zwraca już zasobu, tylko obiekt CurlHandle, zwalniany automatycznie, kiedy zmienna wychodzi z zasięgu. curl_close() od pięciu lat nie robi więc nic. PHP 8.5 przyjmuje to do wiadomości i oznacza funkcję jako przestarzałą.
$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.0Zmierzone na PHP 8.5.10 z cURL 8.14.1. Na PHP 8.4.25 ta sama linia nie wywołuje żadnego ostrzeżenia: kod pozostaje poprawny na współdzielonym hostingu, który został przy 8.4, ale lepiej usunąć te wywołania teraz niż wracać do nich później. Żeby zwolnić uchwyt przed końcem skryptu, wystarczy unset($ch).
Zapytanie GET
Parametry adresu buduje się przez http_build_query(), które koduje wartości. Sklejanie ich ręcznie sypie się, gdy tylko wartość zawiera spację, & albo znak diakrytyczny.
<?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 ustawione na true sprawia, że curl_exec() zwraca odpowiedź, zamiast wypisywać ją na standardowe wyjście. Bez tego odpowiedź wyląduje w środku twojej strony.
Zawsze sprawdzaj zwracaną wartość
To najdroższe przeoczenie. curl_exec() zwraca false, kiedy sieć zawiedzie, a skrypt leci dalej, jakby nic się nie stało.
$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.invalidjson_decode($resultat) kawałek dalej zwróciłoby null, a błąd ujawniłby się trzy ekrany później. Dwie linie sprawdzenia wystarczą, żeby wrócił do swojego źródła.
Uwaga: kod HTTP 404 albo 500 nie jest błędem cURL. Zapytanie doszło, serwer odpowiedział, a curl_exec() zwraca treść strony błędu. Kod odpowiedzi trzeba odczytać osobno albo włączyć CURLOPT_FAILONERROR, żeby cURL traktował odpowiedzi od 400 w górę jako niepowodzenie.
Zapytanie 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);Postać CURLOPT_POSTFIELDS decyduje o typie wysyłanej treści i jest stałym źródłem nieporozumień:
- ciąg znaków (
http_build_query()) idzie jakoapplication/x-www-form-urlencoded, czyli w formacie klasycznego formularza HTML. - tablica idzie jako
multipart/form-data, z granicami MIME. To format wysyłki plików i wiele API go odrzuca.
Żeby wysłać JSON, typ trzeba zadeklarować wprost:
$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),
],
]);Dodawanie nagłówków
Nagłówki podaje się jako tablicę ciągów Nazwa: wartość, przez 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);Jawny User-Agent pozwala uniknąć blokad: wiele serwerów odrzuca domyślny agent cURL albo jego brak.
Uwierzytelnianie tokenem 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,
]);Tokena nie zapisuje się w kodzie źródłowym. Pochodzi ze zmiennej środowiskowej albo z sejfu na sekrety. I podróżuje wyłącznie po HTTPS: pod adresem http:// token leci otwartym tekstem.
Ciasteczka
CURLOPT_COOKIE wysyła ustalone z góry ciasteczka. Do sesji, która ma zachowywać ciasteczka między kolejnymi zapytaniami, potrzebne są CURLOPT_COOKIEJAR i CURLOPT_COOKIEFILE oraz plik tymczasowy.
$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, // zapisuje otrzymane ciasteczka
CURLOPT_COOKIEFILE => $cookies, // odsyła je przy następnym zapytaniu
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['login' => $login, 'mdp' => $mdp]),
]);
curl_exec($ch);
// Drugie zapytanie: ciasteczka sesji lecą same
curl_setopt($ch, CURLOPT_URL, 'https://example.com/mon-compte');
curl_setopt($ch, CURLOPT_POST, false);
$page = curl_exec($ch);
unlink($cookies);Nigdy nie wyłączaj weryfikacji TLS
Na błąd certyfikatu odpowiedź znaleziona na forach jest zawsze ta sama:
// Tak nie rób
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);Te dwie linie usuwają jedyne zabezpieczenie przed przechwyceniem ruchu. Dowolny pośrednik w sieci może wtedy czytać i zmieniać wymianę, razem z tokenem uwierzytelniającym. Ten błąd prawie zawsze oznacza, że magazyn certyfikatów maszyny jest nieobecny albo przeterminowany. Właściwa poprawka to wskazanie curl.cainfo na aktualny plik certyfikatów w php.ini albo aktualizacja systemowego pakietu ca-certificates.
Szkielet do wielokrotnego użytku
Zamiast przepisywać te same opcje wszędzie, jedna mała funkcja skupia w sobie bezpieczne ustawienia.
<?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 => '', // akceptuje gzip i 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),
);
}Wywołana na lokalnym API REST WordPressa funkcja zwraca:
HTTP 200, 294919 octets, 1.580 sKilka zapytań równolegle
Dziesięć wywołań po 200 ms jedno po drugim kosztuje dwie sekundy. Te same równolegle kosztują tyle, co najwolniejsze z dziesięciu. curl_multi_* służy dokładnie do tego.
$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); // zapobiega kręceniu się w pętli na pusto
}
} 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);curl_multi_select() jest tu niezbędne: bez niego pętla zajmuje rdzeń w 100% przez cały czas trwania zapytań.
Na trzech punktach lokalnego API REST WordPressa, mierzone trzy razy po rozgrzewce:
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 msZysk rośnie wraz z liczbą wywołań i z opóźnieniem każdego z nich. Przy trzech lokalnych, już i tak szybkich zapytaniach równoległość skraca czas o połowę, przy dziesięciu wywołaniach do zdalnego API różnica robi się dużo wyraźniejsza.
Kiedy nie używać cURL bezpośrednio
Do pojedynczego wywołania cURL wystarczy. Gdy tylko projekt zaczyna łączyć kolejne integracje, biblioteka kliencka w rodzaju Guzzle albo klient PSR-18 daje ponawianie, obsługę strumieni i interceptory bez pisania ich od nowa. A jeśli rozszerzenie cURL nie jest dostępne, file_get_contents() z kontekstem strumienia ratuje sytuację przy prostym GET, ale bez precyzyjnych limitów czasu i bez użytecznej diagnostyki błędów.
cURL obsługuje wymianę typu zapytanie-odpowiedź. Kiedy to serwer ma wysyłać dane z własnej inicjatywy, potrzebny jest inny transport: zobacz jak stworzyć serwer WebSocket w PHP. Do wywołania API usługi wysyłającej e-maile zobacz wysyłanie e-maila w PHP. A żeby przekazać odpowiedź do przeglądarki, przekazywanie zmiennych z PHP do JavaScriptu.
Zobacz też połączenie z bazą danych w PHP oraz hub Programowanie webowe.
Częste błędy
Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0. Na PHP 8.4.25 żadnego ostrzeżenia. Uchwyt to obiekt CurlHandle zwalniany sam z siebie, w razie potrzeby użyj unset().false, a skrypt leci dalej. json_decode(false) kawałek dalej daje null i błąd wychodzi w miejscu, które nie ma z nim nic wspólnego.n404 mylone z błędem cURL | Odpowiedź 404 jest dla cURL sukcesem. Odczytaj CURLINFO_RESPONSE_CODE albo włącz CURLOPT_FAILONERROR.multipart/form-data, ciąg znaków daje application/x-www-form-urlencoded. Sprawdzone przez CURLINFO_HEADER_OUT. Wiele API odrzuca to pierwsze.curl.cainfo lub pakiet ca-certificates.ncurl_multi bez curl_multi_select | Pętla zajmuje rdzeń w 100% przez cały czas trwania zapytań.

