php

Работа с cURL в PHP: GET, POST, API-запросы и загрузка файлов

Расширение cURL в PHP — основной инструмент для отправки HTTP-запросов: забрать данные из API, отправить форму, скачать файл, загрузить картинку на сторонний сервер. В этой статье разберём все ключевые сценарии работы с cURL — от простого GET-запроса до мультизагрузки файлов и параллельных запросов.

Как работает cURL в PHP

PHP-расширение php-curl — обёртка над библиотекой libcurl. Оно поддерживает HTTP, HTTPS, FTP, SMTP и другие протоколы. Работа всегда строится по одной схеме: инициализация → настройка опций → выполнение → закрытие.

Схема работы PHP cURL: запросы к API, веб-страницам и файлам
Схема работы PHP cURL: от скрипта к внешним сервисам

Минимальный пример — GET-запрос:

$ch = curl_init('https://api.example.com/data');

// Вернуть ответ как строку, а не выводить в браузер
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Ошибка cURL: ' . curl_error($ch);
} else {
    echo $response;
}

curl_close($ch);

Ключевая опция — CURLOPT_RETURNTRANSFER. Без неё curl_exec() выведет ответ прямо в браузер и вернёт true. С ней — вернёт тело ответа как строку.


GET-запрос к REST API с заголовками

Большинство API требуют авторизацию через заголовки. Вот как отправить GET-запрос с Bearer-токеном и обработать JSON-ответ:

function apiGet(string $url, string $token): ?array
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Accept: application/json',
        ],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if (curl_errno($ch)) {
        error_log('cURL error: ' . curl_error($ch));
        curl_close($ch);
        return null;
    }

    curl_close($ch);

    if ($httpCode >= 200 && $httpCode < 300) {
        return json_decode($response, true);
    }

    error_log("API вернул HTTP $httpCode: $response");
    return null;
}

// Использование
$users = apiGet('https://api.example.com/users', 'your-token-here');
if ($users) {
    foreach ($users as $user) {
        echo $user['name'] . PHP_EOL;
    }
}

Обратите внимание на curl_setopt_array() — она удобнее, чем множество вызовов curl_setopt(). Также мы проверяем HTTP-код ответа через curl_getinfo() — это обязательная практика, потому что curl_exec() возвращает тело даже при 4xx/5xx ошибках.


POST-запрос: отправка JSON

Для отправки данных на сервер используется POST. Самый частый формат тела запроса — JSON:

function apiPost(string $url, array $data, string $token): ?array
{
    $ch = curl_init($url);
    $json = json_encode($data, JSON_UNESCAPED_UNICODE);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $json,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
            'Content-Length: ' . strlen($json),
        ],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 201 || $httpCode === 200) {
        return json_decode($response, true);
    }

    error_log("POST $url — HTTP $httpCode: $response");
    return null;
}

// Создаём нового пользователя через API
$result = apiPost(
    'https://api.example.com/users',
    ['name' => 'Иван', 'email' => 'ivan@example.com'],
    'your-token'
);

if ($result) {
    echo 'Создан пользователь ID: ' . $result['id'];
}

Важные моменты:

  • CURLOPT_POST => true — переключает метод на POST
  • CURLOPT_POSTFIELDS — тело запроса. Если передать строку, она отправляется как есть. Если массив — cURL автоматически закодирует как multipart/form-data
  • Заголовок Content-Type: application/json обязателен, иначе сервер может не распознать формат

PUT и DELETE запросы

REST API использует и другие HTTP-методы. Для PUT и DELETE метод задаётся через CURLOPT_CUSTOMREQUEST:

// PUT — обновление ресурса
function apiPut(string $url, array $data, string $token): ?array
{
    $ch = curl_init($url);
    $json = json_encode($data, JSON_UNESCAPED_UNICODE);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => 'PUT',
        CURLOPT_POSTFIELDS     => $json,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
        ],
    ]);

    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

// DELETE — удаление ресурса
function apiDelete(string $url, string $token): bool
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => 'DELETE',
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
        ],
    ]);

    curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return $httpCode === 200 || $httpCode === 204;
}

// Обновляем email пользователя
apiPut('https://api.example.com/users/42', ['email' => 'new@example.com'], $token);

// Удаляем пользователя
if (apiDelete('https://api.example.com/users/42', $token)) {
    echo 'Пользователь удалён';
}

Загрузка файлов на сервер (multipart/form-data)

Для загрузки файлов через cURL используется класс CURLFile. Не передавайте путь к файлу через устаревший синтаксис @/path/to/file — он отключён начиная с PHP 5.6:

function uploadFile(string $url, string $filePath, string $token): ?array
{
    if (!file_exists($filePath)) {
        throw new RuntimeException("Файл не найден: $filePath");
    }

    $ch = curl_init($url);

    // CURLFile — безопасный способ загрузки файлов
    $file = new CURLFile(
        $filePath,
        mime_content_type($filePath), // автоопределение MIME
        basename($filePath)           // имя файла на сервере
    );

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => [
            'file'        => $file,
            'description' => 'Загружено через PHP cURL',
        ],
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $token,
        ],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 200 || $httpCode === 201) {
        return json_decode($response, true);
    }

    return null;
}

// Загружаем аватарку
$result = uploadFile(
    'https://api.example.com/upload',
    '/tmp/avatar.jpg',
    'your-token'
);

echo 'Файл загружен: ' . $result['url'];

Когда CURLOPT_POSTFIELDS получает массив, cURL автоматически выставляет заголовок Content-Type: multipart/form-data. Не указывайте его вручную — cURL сам добавит правильный boundary.


Скачивание файлов

Скачать файл в PHP через cURL можно двумя способами: в строку (для небольших файлов) или напрямую в файл (для больших):

// Способ 1: скачать маленький файл в строку
$ch = curl_init('https://example.com/report.csv');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$content = curl_exec($ch);
curl_close($ch);
file_put_contents('/tmp/report.csv', $content);

// Способ 2: потоковое скачивание (для больших файлов)
function downloadFile(string $url, string $savePath): bool
{
    $fp = fopen($savePath, 'wb');
    if (!$fp) {
        return false;
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_FILE           => $fp,     // писать в файл напрямую
        CURLOPT_FOLLOWLOCATION => true,    // следовать редиректам
        CURLOPT_TIMEOUT        => 300,     // 5 минут на скачивание
        CURLOPT_FAILONERROR    => true,    // ошибка при HTTP 4xx/5xx
    ]);

    $success = curl_exec($ch);

    if (!$success) {
        error_log('Ошибка скачивания: ' . curl_error($ch));
    }

    curl_close($ch);
    fclose($fp);

    // Удалить пустой файл при ошибке
    if (!$success && file_exists($savePath)) {
        unlink($savePath);
    }

    return (bool)$success;
}

// Скачиваем бэкап базы
if (downloadFile('https://example.com/backup.sql.gz', '/tmp/backup.sql.gz')) {
    echo 'Файл скачан, размер: ' . filesize('/tmp/backup.sql.gz') . ' байт';
}

Второй способ не загружает файл целиком в оперативную память — CURLOPT_FILE пишет данные в файловый дескриптор по мере получения. Это критически важно для файлов размером в сотни мегабайт.


Обработка ошибок и отладка

cURL различает два типа ошибок: сетевые (таймаут, DNS, SSL) и HTTP (4xx/5xx). Функция curl_errno() ловит только сетевые ошибки. HTTP-коды нужно проверять отдельно:

$ch = curl_init('https://api.example.com/data');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,       // таймаут запроса (сек)
    CURLOPT_CONNECTTIMEOUT => 5,        // таймаут подключения (сек)
    CURLOPT_SSL_VERIFYPEER => true,     // проверять SSL (не отключайте!)
    CURLOPT_FOLLOWLOCATION => true,     // следовать 301/302
    CURLOPT_MAXREDIRS      => 3,        // макс. редиректов
]);

$response = curl_exec($ch);

// 1. Сетевая ошибка
if (curl_errno($ch)) {
    $errorCode = curl_errno($ch);
    $errorMsg  = curl_error($ch);

    // Типичные коды ошибок
    match ($errorCode) {
        CURLE_OPERATION_TIMEOUTED => error_log("Таймаут: $errorMsg"),
        CURLE_COULDNT_RESOLVE_HOST => error_log("DNS ошибка: $errorMsg"),
        CURLE_SSL_CERTPROBLEM => error_log("SSL ошибка: $errorMsg"),
        default => error_log("cURL [$errorCode]: $errorMsg"),
    };

    curl_close($ch);
    return;
}

// 2. HTTP-ошибка
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);

if ($httpCode >= 400) {
    error_log("HTTP $httpCode за {$totalTime}с: $response");
}

// 3. Подробная отладка — включить verbose-режим
// curl_setopt($ch, CURLOPT_VERBOSE, true);
// curl_setopt($ch, CURLOPT_STDERR, fopen('/tmp/curl-debug.log', 'w'));

curl_close($ch);

Никогда не отключайте CURLOPT_SSL_VERIFYPEER в продакшне. Если есть проблемы с SSL — укажите путь к CA-бандлу через CURLOPT_CAINFO.


Универсальная функция для HTTP-запросов

Чтобы не дублировать код, оберните всю логику cURL в одну функцию:

function httpRequest(
    string $method,
    string $url,
    array $headers = [],
    mixed $body = null,
    int $timeout = 10
): array {
    $ch = curl_init($url);

    $options = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTPHEADER     => $headers,
    ];

    // Настройка метода и тела
    if ($method === 'POST') {
        $options[CURLOPT_POST] = true;
        $options[CURLOPT_POSTFIELDS] = $body;
    } elseif ($method !== 'GET') {
        $options[CURLOPT_CUSTOMREQUEST] = $method;
        if ($body !== null) {
            $options[CURLOPT_POSTFIELDS] = $body;
        }
    }

    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);

    $result = [
        'status'  => curl_getinfo($ch, CURLINFO_HTTP_CODE),
        'body'    => $response,
        'time'    => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
        'error'   => curl_errno($ch) ? curl_error($ch) : null,
    ];

    curl_close($ch);
    return $result;
}

// GET-запрос
$res = httpRequest('GET', 'https://api.example.com/users', [
    'Authorization: Bearer token123',
]);

// POST с JSON
$res = httpRequest('POST', 'https://api.example.com/users', [
    'Content-Type: application/json',
], json_encode(['name' => 'Иван']));

// DELETE
$res = httpRequest('DELETE', 'https://api.example.com/users/42', [
    'Authorization: Bearer token123',
]);

// Проверка результата
if ($res['error']) {
    echo "Ошибка: {$res['error']}";
} elseif ($res['status'] >= 400) {
    echo "HTTP {$res['status']}: {$res['body']}";
} else {
    $data = json_decode($res['body'], true);
    print_r($data);
}

Полезные опции cURL

Справочник часто используемых опций, которые пригодятся в реальных проектах:

  • CURLOPT_USERAGENT — строка User-Agent. Некоторые API отдают 403 без него
  • CURLOPT_COOKIE — отправить cookies в формате 'name=value; name2=value2'
  • CURLOPT_COOKIEJAR / CURLOPT_COOKIEFILE — сохранять и загружать cookies из файла
  • CURLOPT_PROXY — HTTP/SOCKS прокси: 'socks5://127.0.0.1:1080'
  • CURLOPT_USERPWD — HTTP Basic Auth: 'login:password'
  • CURLOPT_ENCODING — принять сжатый ответ (gzip, deflate). Передайте пустую строку '' для автовыбора
  • CURLOPT_HEADER — включить заголовки ответа в вывод
  • CURLOPT_NOBODY — отправить HEAD-запрос (только заголовки, без тела)

Пример — проверить доступность URL без скачивания содержимого:

function isUrlAccessible(string $url): bool
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_NOBODY         => true,    // HEAD-запрос
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT      => 'PHP Link Checker/1.0',
    ]);
    curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return $code >= 200 && $code < 400;
}

// Проверяем массив ссылок
$urls = ['https://example.com', 'https://broken-link.test'];
foreach ($urls as $url) {
    $status = isUrlAccessible($url) ? 'OK' : 'FAIL';
    echo "$url — $status\n";
}

Итог

Чеклист для работы с cURL в PHP:

  1. Всегда включайте CURLOPT_RETURNTRANSFER
  2. Устанавливайте CURLOPT_TIMEOUT и CURLOPT_CONNECTTIMEOUT — без них запрос может висеть бесконечно
  3. Проверяйте и сетевые ошибки (curl_errno), и HTTP-коды (curl_getinfo)
  4. Для загрузки файлов используйте CURLFile, а не устаревший @path
  5. Для скачивания больших файлов используйте CURLOPT_FILE
  6. Не отключайте CURLOPT_SSL_VERIFYPEER в продакшне
  7. Оборачивайте повторяющуюся логику в универсальную функцию
  8. Для сложных сценариев с cookies, прокси и аутентификацией — смотрите в сторону Guzzle HTTP