MidТеория12 min

cURL

HTTP запросы, REST API клиент, параллельные запросы, загрузка файлов

cURL (Client URL) -- самый мощный и гибкий инструмент для HTTP-запросов в PHP. Расширение предоставляет доступ к библиотеке libcurl, поддерживающей десятки протоколов: HTTP, HTTPS, FTP, SMTP и другие.

Основы: curl_init, curl_setopt, curl_exec, curl_close

<?php
declare(strict_types=1);

// Basic GET request
$ch = curl_init(); // Initialize cURL session

curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return response as string
curl_setopt($ch, CURLOPT_TIMEOUT, 30);          // Timeout in seconds

$response = curl_exec($ch);

if ($response === false) {
    echo 'cURL Error: ' . curl_error($ch);
}

curl_close($ch); // Close session (free resources)

// Shorthand: pass URL to curl_init()
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

// Set multiple options at once
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_USERAGENT      => 'MyApp/1.0',
]);
$response = curl_exec($ch);
curl_close($ch);

Важно: Без CURLOPT_RETURNTRANSFER = true функция curl_exec() выведет ответ напрямую в stdout и вернет true/false. Всегда устанавливайте эту опцию для программной обработки ответа.

GET запросы с параметрами

<?php
declare(strict_types=1);

// Query parameters via URL
$params = [
    'page'     => 1,
    'per_page' => 25,
    'sort'     => 'created_at',
    'order'    => 'desc',
    'search'   => 'John Doe',
];

$url = 'https://api.example.com/users?' . http_build_query($params);
// Result: https://api.example.com/users?page=1&per_page=25&sort=created_at&order=desc&search=John+Doe

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPGET        => true, // Explicitly set GET (default)
]);

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

$data = json_decode($response, true, flags: JSON_THROW_ON_ERROR);

POST запросы

<?php
declare(strict_types=1);

// === POST with form data (application/x-www-form-urlencoded) ===
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'name'  => 'John Doe',
        'email' => '[email protected]',
        'role'  => 'admin',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

// === POST with JSON body ===
$payload = json_encode([
    'name'  => 'John Doe',
    'email' => '[email protected]',
    'roles' => ['admin', 'editor'],
], JSON_THROW_ON_ERROR);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Accept: application/json',
        'Content-Length: ' . strlen($payload),
    ],
]);

$response = curl_exec($ch);
curl_close($ch);

// === POST with raw body ===
$xmlBody = '<?xml version="1.0"?><user><name>John</name></user>';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $xmlBody,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/xml',
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

PUT, PATCH, DELETE запросы

<?php
declare(strict_types=1);

// === PUT — full resource update ===
$data = json_encode(['name' => 'Jane Doe', 'email' => '[email protected]']);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users/42',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_POSTFIELDS     => $data,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// === PATCH — partial update ===
$patch = json_encode(['status' => 'active']);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users/42',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_POSTFIELDS     => $patch,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
]);
$response = curl_exec($ch);
curl_close($ch);

// === DELETE ===
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users/42',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 204 No Content
curl_close($ch);

Заголовки и аутентификация

<?php
declare(strict_types=1);

// === Custom headers ===
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/data',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer eyJhbGciOiJIUzI1NiIs...',
        'Accept: application/json',
        'X-Request-ID: ' . bin2hex(random_bytes(16)),
        'X-Custom-Header: my-value',
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// === Basic authentication ===
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/protected',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD        => 'username:password', // Basic auth
    CURLOPT_HTTPAUTH       => CURLAUTH_BASIC,
]);
$response = curl_exec($ch);
curl_close($ch);

// === Bearer token authentication ===
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/resource',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $accessToken,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// === Read response headers ===
$responseHeaders = [];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/data',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$responseHeaders): int {
        $len = strlen($header);
        $parts = explode(':', $header, 2);

        if (count($parts) === 2) {
            $name = strtolower(trim($parts[0]));
            $responseHeaders[$name] = trim($parts[1]);
        }

        return $len; // Must return number of bytes processed
    },
]);

$body = curl_exec($ch);
curl_close($ch);

echo $responseHeaders['content-type'] ?? 'unknown';

Основные CURLOPT опции

<?php
declare(strict_types=1);

$ch = curl_init();
curl_setopt_array($ch, [
    // URL and method
    CURLOPT_URL             => 'https://api.example.com/data',
    CURLOPT_CUSTOMREQUEST   => 'POST',

    // Response handling
    CURLOPT_RETURNTRANSFER  => true,  // Return string instead of outputting
    CURLOPT_HEADER          => false, // Don't include headers in output
    CURLOPT_NOBODY          => false, // Set true for HEAD request

    // Timeouts
    CURLOPT_TIMEOUT         => 30,    // Total timeout in seconds
    CURLOPT_CONNECTTIMEOUT  => 10,    // Connection timeout in seconds
    CURLOPT_TIMEOUT_MS      => 30000, // Total timeout in milliseconds
    CURLOPT_CONNECTTIMEOUT_MS => 5000, // Connection timeout in ms

    // SSL/TLS
    CURLOPT_SSL_VERIFYPEER  => true,  // Verify SSL certificate (NEVER set to false in production!)
    CURLOPT_SSL_VERIFYHOST  => 2,     // Check CN matches hostname
    CURLOPT_CAINFO          => '/etc/ssl/certs/ca-certificates.crt',

    // Redirects
    CURLOPT_FOLLOWLOCATION  => true,  // Follow HTTP redirects
    CURLOPT_MAXREDIRS       => 5,     // Max number of redirects
    CURLOPT_AUTOREFERER     => true,  // Set Referer on redirect

    // User agent and cookies
    CURLOPT_USERAGENT       => 'MyApp/2.0 (PHP/' . PHP_VERSION . ')',
    CURLOPT_COOKIE          => 'session_id=abc123; theme=dark',
    CURLOPT_COOKIEFILE      => '/tmp/cookies.txt', // Read cookies from file
    CURLOPT_COOKIEJAR       => '/tmp/cookies.txt', // Save cookies to file

    // Encoding
    CURLOPT_ENCODING        => '',    // Accept all encodings (gzip, deflate, br)
    CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_2_0, // HTTP/2

    // Proxy
    CURLOPT_PROXY           => 'http://proxy.example.com:8080',
    CURLOPT_PROXYUSERPWD    => 'user:pass',
]);

$response = curl_exec($ch);
curl_close($ch);

curl_getinfo() -- информация об ответе

<?php
declare(strict_types=1);

$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Get specific info
$httpCode    = curl_getinfo($ch, CURLINFO_HTTP_CODE);        // 200
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);     // 'application/json'
$totalTime   = curl_getinfo($ch, CURLINFO_TOTAL_TIME);       // 0.234 seconds
$dnsTime     = curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME);  // 0.012 seconds
$connectTime = curl_getinfo($ch, CURLINFO_CONNECT_TIME);     // 0.045 seconds
$sslTime     = curl_getinfo($ch, CURLINFO_APPCONNECT_TIME);  // 0.089 seconds (TLS handshake)
$downloadSize = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);   // bytes downloaded
$speed       = curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD);   // bytes per second
$effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);   // Final URL after redirects
$redirectCount = curl_getinfo($ch, CURLINFO_REDIRECT_COUNT); // Number of redirects

// Get all info at once
$info = curl_getinfo($ch);
print_r($info);

curl_close($ch);

// Practical: logging request performance
function logRequestPerformance(CurlHandle $ch, string $url): void
{
    $info = curl_getinfo($ch);
    $log = sprintf(
        "[%s] %s -> %d | DNS: %.3fs, Connect: %.3fs, TLS: %.3fs, Total: %.3fs | %d bytes",
        date('Y-m-d H:i:s'),
        $url,
        $info['http_code'],
        $info['namelookup_time'],
        $info['connect_time'],
        $info['appconnect_time'],
        $info['total_time'],
        $info['size_download'],
    );
    error_log($log);
}

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

<?php
declare(strict_types=1);

$ch = curl_init('https://api.example.com/data');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_FAILONERROR    => false, // Don't fail on HTTP errors (4xx, 5xx)
]);

$response = curl_exec($ch);

// Check for cURL errors (network, DNS, timeout, SSL)
if ($response === false) {
    $errorCode = curl_errno($ch);
    $errorMessage = curl_error($ch);

    match ($errorCode) {
        CURLE_OPERATION_TIMEDOUT  => throw new RuntimeException("Request timed out: $errorMessage"),
        CURLE_COULDNT_RESOLVE_HOST => throw new RuntimeException("DNS resolution failed: $errorMessage"),
        CURLE_COULDNT_CONNECT     => throw new RuntimeException("Connection failed: $errorMessage"),
        CURLE_SSL_CONNECT_ERROR   => throw new RuntimeException("SSL error: $errorMessage"),
        default                   => throw new RuntimeException("cURL error [$errorCode]: $errorMessage"),
    };
}

// Check HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode >= 400) {
    $body = json_decode($response, true);
    $message = $body['error']['message'] ?? 'Unknown error';

    throw match (true) {
        $httpCode === 401 => new RuntimeException("Unauthorized: $message"),
        $httpCode === 403 => new RuntimeException("Forbidden: $message"),
        $httpCode === 404 => new RuntimeException("Not found: $message"),
        $httpCode === 429 => new RuntimeException("Rate limited: $message"),
        $httpCode >= 500  => new RuntimeException("Server error [$httpCode]: $message"),
        default           => new RuntimeException("HTTP error [$httpCode]: $message"),
    };
}

curl_multi -- параллельные запросы

<?php
declare(strict_types=1);

// Execute multiple HTTP requests in parallel
$urls = [
    'users'    => 'https://api.example.com/users',
    'products' => 'https://api.example.com/products',
    'orders'   => 'https://api.example.com/orders',
    'stats'    => 'https://api.example.com/stats',
];

// Initialize multi handle
$mh = curl_multi_init();
$handles = [];

// Create individual handles and add to multi
foreach ($urls as $name => $url) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);

    curl_multi_add_handle($mh, $ch);
    $handles[$name] = $ch;
}

// Execute all requests in parallel
$running = null;
do {
    $status = curl_multi_exec($mh, $running);

    if ($status > CURLM_OK) {
        throw new RuntimeException('cURL multi error: ' . curl_multi_strerror($status));
    }

    // Wait for activity (avoids busy loop)
    if ($running > 0) {
        curl_multi_select($mh, 1.0); // Block up to 1 second
    }
} while ($running > 0);

// Collect results
$results = [];
foreach ($handles as $name => $ch) {
    $error = curl_error($ch);

    if ($error !== '') {
        $results[$name] = ['error' => $error];
    } else {
        $results[$name] = [
            'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
            'body' => json_decode(curl_multi_getcontent($ch), true),
            'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
        ];
    }

    curl_multi_remove_handle($mh, $ch);
    curl_close($ch);
}

curl_multi_close($mh);

// All 4 requests completed in ~time of slowest one (not sum!)
foreach ($results as $name => $result) {
    echo sprintf("%s: %d (%.3fs)\n", $name, $result['code'] ?? 0, $result['time'] ?? 0);
}

Загрузка файлов

<?php
declare(strict_types=1);

// === Upload file with CURLFile ===
$file = new CURLFile(
    filename: '/path/to/document.pdf',
    mime_type: 'application/pdf',
    posted_filename: 'my-document.pdf', // Name seen by server
);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/upload',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        'file'        => $file,
        'description' => 'Important document',
    ],
    // Content-Type: multipart/form-data is set automatically with array POSTFIELDS
]);

$response = curl_exec($ch);
curl_close($ch);

// === Upload from string (without temp file) ===
$csvData = "name,email\nJohn,[email protected]\nJane,[email protected]";
$tempFile = tmpfile();
fwrite($tempFile, $csvData);
$tempPath = stream_get_meta_data($tempFile)['uri'];

$file = new CURLFile($tempPath, 'text/csv', 'users.csv');

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/import',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => ['file' => $file],
]);
$response = curl_exec($ch);
curl_close($ch);
fclose($tempFile); // Cleanup temp file

// === Download file ===
$ch = curl_init();
$fp = fopen('/tmp/downloaded-file.zip', 'wb');

curl_setopt_array($ch, [
    CURLOPT_URL  => 'https://example.com/large-file.zip',
    CURLOPT_FILE => $fp, // Write directly to file (memory efficient)
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT        => 300,
]);

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

if ($httpCode !== 200) {
    unlink('/tmp/downloaded-file.zip');
    throw new RuntimeException("Download failed with HTTP $httpCode");
}

// === Download with progress callback ===
$ch = curl_init('https://example.com/large-file.zip');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_NOPROGRESS     => false,
    CURLOPT_PROGRESSFUNCTION => function (
        CurlHandle $ch,
        int $downloadTotal,
        int $downloadNow,
        int $uploadTotal,
        int $uploadNow,
    ): int {
        if ($downloadTotal > 0) {
            $percent = round($downloadNow / $downloadTotal * 100);
            echo "\rDownload: {$percent}% ({$downloadNow}/{$downloadTotal} bytes)";
        }
        return 0; // Return non-zero to abort
    },
]);

$data = curl_exec($ch);
curl_close($ch);

Практический пример: REST API клиент

<?php
declare(strict_types=1);

final class HttpClient
{
    private string $baseUrl;
    /** @var array<string, string> */
    private array $defaultHeaders;
    private int $timeout;

    public function __construct(
        string $baseUrl,
        string $bearerToken = '',
        int $timeout = 30,
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->timeout = $timeout;
        $this->defaultHeaders = [
            'Accept: application/json',
            'Content-Type: application/json',
        ];

        if ($bearerToken !== '') {
            $this->defaultHeaders[] = "Authorization: Bearer $bearerToken";
        }
    }

    /** @param array<string, mixed> $query */
    public function get(string $path, array $query = []): array
    {
        $url = $this->baseUrl . $path;
        if ($query !== []) {
            $url .= '?' . http_build_query($query);
        }

        return $this->request('GET', $url);
    }

    /** @param array<string, mixed> $data */
    public function post(string $path, array $data): array
    {
        return $this->request('POST', $this->baseUrl . $path, $data);
    }

    /** @param array<string, mixed> $data */
    public function put(string $path, array $data): array
    {
        return $this->request('PUT', $this->baseUrl . $path, $data);
    }

    public function delete(string $path): array
    {
        return $this->request('DELETE', $this->baseUrl . $path);
    }

    /** @param array<string, mixed>|null $data */
    private function request(string $method, string $url, ?array $data = null): array
    {
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_HTTPHEADER     => $this->defaultHeaders,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_ENCODING       => '',
        ]);

        if ($data !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_THROW_ON_ERROR));
        }

        $response = curl_exec($ch);

        if ($response === false) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException("HTTP request failed: $error");
        }

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

        $decoded = json_decode($response, true, flags: JSON_THROW_ON_ERROR);

        if ($httpCode >= 400) {
            throw new RuntimeException(
                sprintf('HTTP %d: %s', $httpCode, $decoded['message'] ?? 'Unknown error')
            );
        }

        return $decoded;
    }
}

// Usage
$api = new HttpClient('https://api.example.com/v1', bearerToken: 'my-token');
$users = $api->get('/users', ['page' => 1]);
$user = $api->post('/users', ['name' => 'John', 'email' => '[email protected]']);
$updated = $api->put('/users/42', ['name' => 'Jane']);
$api->delete('/users/42');

PHP 8.5: curl_share_init_persistent

<?php
declare(strict_types=1);

// PHP 8.5+: Persistent share handles for connection reuse across requests
// DNS cache, SSL sessions, and connections shared between cURL handles

$share = curl_share_init_persistent('my-app-share');
curl_share_setopt($share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
curl_share_setopt($share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
curl_share_setopt($share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);

// First request: establishes connection, DNS lookup, TLS handshake
$ch1 = curl_init('https://api.example.com/users');
curl_setopt($ch1, CURLOPT_SHARE, $share);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
$response1 = curl_exec($ch1);
curl_close($ch1);

// Subsequent requests: reuse DNS cache, SSL session, connection
$ch2 = curl_init('https://api.example.com/orders');
curl_setopt($ch2, CURLOPT_SHARE, $share);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
$response2 = curl_exec($ch2);
curl_close($ch2);
// Much faster — no DNS lookup, no TLS handshake

// Persistent share survives across PHP requests (in FPM)
// Name 'my-app-share' links to same shared data in next request

Тесты

Вопросы с экзамена ZCE

Проверь себя

5 из 13

Какое из следующих расширений НЕ было встроено в PHP до PHP 5?

Как загрузить файл через cURL в PHP?

Какую директиву php.ini следует отключить для предотвращения выполнения удалённого PHP-скрипта через include или require?

Для чего нужен CURLOPT_FOLLOWLOCATION?

Для чего служит curl_multi?