Полное руководство по использованию HTTP-прокси в PHP: cURL, Guzzle, Symfony и Laravel

Практическое руководство для PHP-разработчиков по работе с HTTP-прокси. Разбираем нативный cURL, Guzzle, Symfony HTTP Client, интеграцию с Laravel и асинхронные запросы — с готовыми примерами кода.

Полное руководство по использованию HTTP-прокси в PHP: cURL, Guzzle, Symfony и Laravel

Если вы занимаетесь скрейпингом, интеграцией с внешними API или автоматизацией в PHP — рано или поздно столкнётесь с блокировками по IP. Провайдеры ограничивают количество запросов, CDN вроде Cloudflare возвращают 403, а популярные сайты требуют обхода rate limit. PHP proxy — это не просто настройка CURLOPT_PROXY, а целый стек решений: ротация IP, обработка ошибок, управление сессиями и TLS-конфигурация.

В этом руководстве мы пройдём путь от базового cURL до production-ready решения на Laravel: сервис-класс с пулом residential-прокси, очередями и повторными попытками. Все примеры используют ProxyHat — провайдера residential, mobile и datacenter прокси.

Базовая настройка: нативный cURL с CURLOPT_PROXY

cURL — стандарт де-факто для HTTP-запросов в PHP. Для работы через прокси используются опции CURLOPT_PROXY (адрес прокси-сервера) и CURLOPT_PROXYUSERPWD (авторизация). Рассмотрим полный пример с обработкой ошибок и таймаутами.

<?php

class CurlProxyClient
{
    private string $proxyHost = 'gate.proxyhat.com';
    private int $proxyPort = 8080;
    private string $username;
    private string $password;

    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
    }

    /**
     * Выполняет HTTP-запрос через прокси с ротацией IP
     * 
     * @param string $url Целевой URL
     * @param string $country Код страны для геотаргетинга (опционально)
     * @param string $session Идентификатор сессии для sticky IP (опционально)
     * @return array ['status' => int, 'body' => string, 'headers' => array]
     * @throws RuntimeException
     */
    public function get(
        string $url,
        ?string $country = null,
        ?string $session = null
    ): array {
        $ch = curl_init();
        
        // Формируем имя пользователя с флагами геотаргетинга и сессии
        $proxyUser = $this->buildProxyUsername($country, $session);
        
        // Базовые опции cURL
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            
            // Настройка прокси
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$proxyUser}:{$this->password}",
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            
            // SSL/TLS конфигурация
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CAINFO => $this->getCaBundlePath(),
            
            // Заголовки
            CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            CURLOPT_HTTPHEADER => [
                'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language: en-US,en;q=0.9',
            ],
            
            // Захват заголовков ответа
            CURLOPT_HEADER => true,
        ]);
        
        $response = curl_exec($ch);
        $error = curl_error($ch);
        $errno = curl_errno($ch);
        $info = curl_getinfo($ch);
        
        curl_close($ch);
        
        if ($errno !== CURLE_OK) {
            throw new RuntimeException(
                "cURL error [{$errno}]: {$error}",
                $errno
            );
        }
        
        // Разделяем заголовки и тело
        $headerSize = $info['header_size'];
        $headers = substr($response, 0, $headerSize);
        $body = substr($response, $headerSize);
        
        return [
            'status' => $info['http_code'],
            'body' => $body,
            'headers' => $this->parseHeaders($headers),
            'effective_url' => $info['url'],
            'total_time' => $info['total_time'],
        ];
    }
    
    /**
     * Строит имя пользователя с флагами ProxyHat
     */
    private function buildProxyUsername(?string $country, ?string $session): string
    {
        $parts = [$this->username];
        
        if ($country !== null) {
            $parts[] = "country-{$country}";
        }
        
        if ($session !== null) {
            $parts[] = "session-{$session}";
        }
        
        return implode('-', $parts);
    }
    
    /**
     * Возвращает путь к CA-бандлу
     */
    private function getCaBundlePath(): string
    {
        // Приоритет: системный бандл > bundled с cURL > локальный файл
        $candidates = [
            '/etc/ssl/certs/ca-certificates.crt', // Debian/Ubuntu
            '/etc/pki/tls/certs/ca-bundle.crt',   // RHEL/CentOS
            '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD
            __DIR__ . '/cacert.pem', // Локальный fallback
        ];
        
        foreach ($candidates as $path) {
            if (file_exists($path)) {
                return $path;
            }
        }
        
        // Если ничего не найдено, отключаем верификацию (только для dev!)
        return '';
    }
    
    /**
     * Парсит заголовки ответа в массив
     */
    private function parseHeaders(string $headerText): array
    {
        $headers = [];
        $lines = explode("\r\n", trim($headerText));
        
        foreach ($lines as $line) {
            if (strpos($line, ':') !== false) {
                [$key, $value] = explode(':', $line, 2);
                $headers[trim($key)] = trim($value);
            }
        }
        
        return $headers;
    }
}

// Использование:
try {
    $client = new CurlProxyClient('user', 'your_password');
    
    // Запрос с ротацией IP (новый IP для каждого запроса)
    $result = $client->get('https://httpbin.org/ip');
    echo "IP: " . json_decode($result['body'], true)['origin'] . "\n";
    
    // Запрос с геотаргетингом (США)
    $result = $client->get('https://httpbin.org/ip', 'US');
    
    // Sticky-сессия (тот же IP на протяжении сессии)
    $sessionId = bin2hex(random_bytes(8));
    $result = $client->get('https://httpbin.org/ip', null, $sessionId);
    
} catch (RuntimeException $e) {
    echo "Ошибка: " . $e->getMessage() . "\n";
}

Ключевые моменты в этом примере:

  • CURLOPT_PROXYUSERPWD — авторизация в формате username:password
  • Геотаргетинг — добавляем country-US к имени пользователя
  • Sticky-сессии — идентификатор session-abc123 гарантирует тот же IP
  • TLS-верификация — указываем путь к CA-бандлу

Guzzle HTTP Client: современный подход к Guzzle proxy

Guzzle — самая популярная HTTP-библиотека для PHP с PSR-7 совместимостью. Настройка прокси в Guzzle делается через опцию proxy в массиве конфигурации клиента или отдельного запроса.

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleProxyClient
{
    private Client $client;
    private string $username;
    private string $password;
    private string $proxyHost = 'gate.proxyhat.com';
    private int $proxyPort = 8080;
    
    // Пул стран для ротации
    private array $countries = ['US', 'DE', 'GB', 'FR', 'JP'];
    
    public function __construct(string $username, string $password, array $config = [])
    {
        $this->username = $username;
        $this->password = $password;
        
        $this->client = $this->createClient($config);
    }
    
    /**
     * Создаёт Guzzle-клиент с middleware для retry и логирования
     */
    private function createClient(array $config = []): Client
    {
        $handlerStack = HandlerStack::create();
        
        // Middleware для повторных попыток
        $handlerStack->push(Middleware::retry(
            function ($retries, RequestInterface $request, ResponseInterface $response = null, $exception = null) {
                // Повторяем до 3 раз
                if ($retries >= 3) {
                    return false;
                }
                
                // Повторяем при 429, 500, 502, 503, 504
                if ($response && in_array($response->getStatusCode(), [429, 500, 502, 503, 504])) {
                    return true;
                }
                
                // Повторяем при сетевых ошибках
                if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
                    return true;
                }
                
                return false;
            },
            function ($retries) {
                // Экспоненциальная задержка: 1s, 2s, 4s
                return 1000 * (2 ** ($retries - 1));
            }
        ));
        
        // Middleware для логирования
        $handlerStack->push(Middleware::tap(
            function (RequestInterface $request, array $options) {
                echo sprintf(
                    "[%s] %s %s\n",
                    date('Y-m-d H:i:s'),
                    $request->getMethod(),
                    $request->getUri()
                );
            }
        ));
        
        $defaultConfig = [
            'handler' => $handlerStack,
            'timeout' => 30,
            'connect_timeout' => 10,
            'verify' => true,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            ],
        ];
        
        return new Client(array_merge($defaultConfig, $config));
    }
    
    /**
     * Выполняет запрос с автоматической ротацией IP
     * 
     * @param string $method HTTP-метод
     * @param string $url Целевой URL
     * @param array $options Дополнительные опции Guzzle
     * @param array $proxyOptions Опции прокси: country, session, rotate
     * @return ResponseInterface
     */
    public function request(
        string $method,
        string $url,
        array $options = [],
        array $proxyOptions = []
    ): ResponseInterface {
        // Формируем конфигурацию прокси
        $proxyConfig = $this->buildProxyConfig($proxyOptions);
        
        // Объединяем с опциями запроса
        $options['proxy'] = $proxyConfig['url'];
        
        // Добавляем кастомные заголовки для отладки
        $options['headers']['X-Proxy-Country'] = $proxyConfig['country'] ?? 'rotating';
        $options['headers']['X-Proxy-Session'] = $proxyConfig['session'] ?? 'none';
        
        try {
            return $this->client->request($method, $url, $options);
        } catch (RequestException $e) {
            // Логируем ошибку с контекстом
            $context = [
                'url' => $url,
                'proxy_country' => $proxyConfig['country'] ?? 'rotating',
                'status_code' => $e->hasResponse() ? $e->getResponse()->getStatusCode() : null,
            ];
            
            throw new ProxyRequestException(
                "Proxy request failed: " . $e->getMessage(),
                $e->getCode(),
                $e,
                $context
            );
        }
    }
    
    /**
     * Строит конфигурацию прокси для Guzzle
     */
    private function buildProxyConfig(array $options): array
    {
        $username = $this->username;
        $session = $options['session'] ?? null;
        $country = $options['country'] ?? null;
        
        // Автоматическая ротация стран
        if ($options['rotate_country'] ?? false) {
            $country = $this->countries[array_rand($this->countries)];
        }
        
        // Формируем имя пользователя с флагами
        if ($country !== null) {
            $username .= "-country-{$country}";
        }
        
        if ($session !== null) {
            $username .= "-session-{$session}";
        }
        
        // URL для Guzzle: 'protocol://user:pass@host:port'
        $proxyUrl = sprintf(
            'http://%s:%s@%s:%d',
            $username,
            $this->password,
            $this->proxyHost,
            $this->proxyPort
        );
        
        return [
            'url' => $proxyUrl,
            'country' => $country,
            'session' => $session,
        ];
    }
    
    /**
     * Выполняет несколько запросов параллельно с разными прокси
     */
    public function batchRequest(array $requests): array
    {
        $promises = [];
        
        foreach ($requests as $key => $request) {
            $proxyOptions = $request['proxy_options'] ?? [];
            $proxyConfig = $this->buildProxyConfig($proxyOptions);
            
            $promises[$key] = $this->client->requestAsync(
                $request['method'] ?? 'GET',
                $request['url'],
                array_merge(
                    $request['options'] ?? [],
                    ['proxy' => $proxyConfig['url']]
                )
            );
        }
        
        // Ждём завершения всех запросов
        $results = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
        
        $responses = [];
        foreach ($results as $key => $result) {
            if ($result['state'] === 'fulfilled') {
                $responses[$key] = [
                    'success' => true,
                    'response' => $result['value'],
                    'body' => $result['value']->getBody()->getContents(),
                ];
            } else {
                $responses[$key] = [
                    'success' => false,
                    'error' => $result['reason']->getMessage(),
                ];
            }
        }
        
        return $responses;
    }
}

// Кастомное исключение с контекстом
class ProxyRequestException extends \RuntimeException
{
    public array $context;
    
    public function __construct(string $message, int $code, \Throwable $previous, array $context)
    {
        parent::__construct($message, $code, $previous);
        $this->context = $context;
    }
}

// Использование:
$client = new GuzzleProxyClient('user', 'your_password');

// Простой запрос с ротацией IP
$response = $client->request('GET', 'https://httpbin.org/ip');
echo $response->getBody()->getContents();

// Запрос с конкретной страной
$response = $client->request('GET', 'https://httpbin.org/ip', [], [
    'country' => 'DE',
]);

// Sticky-сессия для многошагового скрейпинга
$session = uniqid('session-', true);
$client->request('GET', 'https://example.com/login', [], ['session' => $session]);
$client->request('POST', 'https://example.com/login', [
    'form_params' => ['username' => 'user', 'password' => 'pass']
], ['session' => $session]);
$client->request('GET', 'https://example.com/dashboard', [], ['session' => $session]);

// Пакетные запросы с автоматической ротацией стран
$results = $client->batchRequest([
    'us' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'US']],
    'de' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'DE']],
    'jp' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'JP']],
]);

foreach ($results as $region => $result) {
    if ($result['success']) {
        echo "{$region}: " . $result['body'] . "\n";
    }
}

Per-request ротация в Guzzle

Для скрейпинга часто нужен новый IP на каждый запрос. Guzzle позволяет переопределять прокси на уровне отдельного запроса:

<?php

class RotatingProxyPool
{
    private GuzzleProxyClient $client;
    private array $sessions = [];
    
    public function __construct(GuzzleProxyClient $client)
    {
        $this->client = $client;
    }
    
    /**
     * Получает новый IP для каждого запроса
     */
    public function fetchWithRotation(string $url): string
    {
        // Без session-параметра ProxyHat выдаёт новый IP
        return $this->client->request('GET', $url)->getBody()->getContents();
    }
    
    /**
     * Создаёт sticky-сессию для последовательности запросов
     */
    public function createSession(string $country = null): string
    {
        $sessionId = bin2hex(random_bytes(8));
        $this->sessions[$sessionId] = [
            'country' => $country,
            'created_at' => time(),
        ];
        return $sessionId;
    }
    
    /**
     * Выполняет запрос в рамках сессии
     */
    public function fetchWithSession(string $url, string $sessionId): string
    {
        if (!isset($this->sessions[$sessionId])) {
            throw new InvalidArgumentException("Unknown session: {$sessionId}");
        }
        
        $session = $this->sessions[$sessionId];
        
        return $this->client->request('GET', $url, [], [
            'session' => $sessionId,
            'country' => $session['country'],
        ])->getBody()->getContents();
    }
    
    /**
     * Очищает старые сессии (TTL 30 минут)
     */
    public function cleanupSessions(int $ttlSeconds = 1800): int
    {
        $now = time();
        $count = 0;
        
        foreach ($this->sessions as $id => $session) {
            if ($now - $session['created_at'] > $ttlSeconds) {
                unset($this->sessions[$id]);
                $count++;
            }
        }
        
        return $count;
    }
}

Symfony HTTP Client: асинхронность и AsyncResponse

Symfony HTTP Client — современная библиотека с поддержкой асинхронных запросов, AMPHP и реактивного программирования. В отличие от Guzzle, Symfony HttpClient использует curl_multi под капотом и предоставляет нативную поддержку асинхронности без promises.

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

class SymfonyProxyClient
{
    private HttpClient $client;
    private string $username;
    private string $password;
    private string $proxyHost = 'gate.proxyhat.com';
    private int $proxyPort = 8080;
    
    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
        
        $this->client = HttpClient::create([
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_peer' => true,
            'verify_host' => true,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            ],
        ]);
    }
    
    /**
     * Выполняет синхронный запрос через прокси
     */
    public function request(string $method, string $url, array $options = [], array $proxyOptions = []): array
    {
        $proxyUrl = $this->buildProxyUrl($proxyOptions);
        
        $mergedOptions = array_merge($options, [
            'proxy' => $proxyUrl,
        ]);
        
        try {
            $response = $this->client->request($method, $url, $mergedOptions);
            
            return [
                'status' => $response->getStatusCode(),
                'headers' => $response->getHeaders(false),
                'body' => $response->getContent(),
                'info' => $response->getInfo(),
            ];
        } catch (TransportExceptionInterface $e) {
            throw new ProxyTransportException(
                "Request failed: " . $e->getMessage(),
                $e->getCode(),
                $e
            );
        }
    }
    
    /**
     * Асинхронный запрос с колбэками
     */
    public function requestAsync(
        string $method,
        string $url,
        array $options = [],
        array $proxyOptions = [],
        ?callable $onSuccess = null,
        ?callable $onError = null
    ): AsyncResponse {
        $proxyUrl = $this->buildProxyUrl($proxyOptions);
        
        $mergedOptions = array_merge($options, [
            'proxy' => $proxyUrl,
        ]);
        
        return new AsyncResponse(
            $this->client,
            $method,
            $url,
            $mergedOptions,
            function ($chunk, $response) use ($onSuccess, $onError) {
                static $content = '';
                
                try {
                    // Проверяем статус при первом чанке
                    if ($response->getInfo('response_headers')) {
                        $statusCode = $response->getStatusCode();
                        
                        if ($statusCode >= 400) {
                            if ($onError) {
                                $onError(new \Exception("HTTP {$statusCode}"), $response);
                            }
                            return;
                        }
                    }
                    
                    // Накапливаем контент
                    if (is_string($chunk)) {
                        $content .= $chunk;
                    }
                    
                } catch (\Exception $e) {
                    if ($onError) {
                        $onError($e, $response);
                    }
                }
            },
            function ($response) use ($onSuccess) {
                // Вызывается при завершении
                if ($onSuccess) {
                    $onSuccess($response);
                }
            }
        );
    }
    
    /**
     * Пакетные асинхронные запросы с ограничением конкурентности
     */
    public function batchAsync(array $requests, int $concurrency = 5): array
    {
        $responses = [];
        $activeRequests = [];
        $queue = $requests;
        $results = [];
        
        // Инициализируем первые N запросов
        for ($i = 0; $i < min($concurrency, count($queue)); $i++) {
            $key = key($queue);
            $request = array_shift($queue);
            
            $proxyOptions = $request['proxy_options'] ?? [];
            $proxyUrl = $this->buildProxyUrl($proxyOptions);
            
            $responses[$key] = $this->client->request(
                $request['method'] ?? 'GET',
                $request['url'],
                array_merge($request['options'] ?? [], ['proxy' => $proxyUrl])
            );
            
            $activeRequests[] = $key;
        }
        
        // Обрабатываем ответы по мере поступления
        $completed = [];
        foreach (HttpClient::stream($responses) as $key => $chunk) {
            try {
                if ($chunk->isLast()) {
                    $response = $responses[$key];
                    $results[$key] = [
                        'success' => true,
                        'status' => $response->getStatusCode(),
                        'body' => $response->getContent(),
                    ];
                    $completed[] = $key;
                    
                    // Запускаем следующий запрос из очереди
                    if (!empty($queue)) {
                        $nextKey = key($queue);
                        $nextRequest = array_shift($queue);
                        
                        $proxyUrl = $this->buildProxyUrl($nextRequest['proxy_options'] ?? []);
                        
                        $responses[$nextKey] = $this->client->request(
                            $nextRequest['method'] ?? 'GET',
                            $nextRequest['url'],
                            array_merge($nextRequest['options'] ?? [], ['proxy' => $proxyUrl])
                        );
                    }
                }
            } catch (TransportExceptionInterface $e) {
                $results[$key] = [
                    'success' => false,
                    'error' => $e->getMessage(),
                ];
                $completed[] = $key;
            }
        }
        
        return $results;
    }
    
    /**
     * Стриминг больших файлов через прокси
     */
    public function downloadFile(
        string $url,
        string $destination,
        array $proxyOptions = [],
        ?callable $progressCallback = null
    ): int {
        $proxyUrl = $this->buildProxyUrl($proxyOptions);
        
        $response = $this->client->request('GET', $url, [
            'proxy' => $proxyUrl,
        ]);
        
        $handle = fopen($destination, 'w');
        $bytesWritten = 0;
        $contentLength = (int) ($response->getHeaders()['content-length'][0] ?? 0);
        
        foreach ($this->client->stream($response) as $chunk) {
            $bytesWritten += fwrite($handle, $chunk->getContent());
            
            if ($progressCallback) {
                $progressCallback($bytesWritten, $contentLength);
            }
        }
        
        fclose($handle);
        
        return $bytesWritten;
    }
    
    private function buildProxyUrl(array $options): string
    {
        $username = $this->username;
        
        if (isset($options['country'])) {
            $username .= "-country-{$options['country']}";
        }
        
        if (isset($options['session'])) {
            $username .= "-session-{$options['session']}";
        }
        
        return "http://{$username}:{$this->password}@{$this->proxyHost}:{$this->proxyPort}";
    }
}

// Использование:
$client = new SymfonyProxyClient('user', 'your_password');

// Синхронный запрос
$result = $client->request('GET', 'https://httpbin.org/ip');
echo $result['body'];

// Асинхронные запросы с конкурентностью
$results = $client->batchAsync([
    'us' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'US']],
    'de' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'DE']],
    'gb' => ['url' => 'https://httpbin.org/ip', 'proxy_options' => ['country' => 'GB']],
], concurrency: 3);

// Скачивание файла с прогрессом
$client->downloadFile(
    'https://example.com/large-file.zip',
    '/tmp/file.zip',
    ['country' => 'US'],
    fn($bytes, $total) => printf("Downloaded: %.1f%%\r", ($bytes / $total) * 100)
);

Laravel интеграция: сервис-класс для Laravel proxy scraping

В Laravel-приложении прокси-клиент должен быть сервисом с dependency injection, конфигурацией через .env и интеграцией с очередями. Создадим production-ready решение.

<?php

// config/proxy.php

return [
    'default' => env('PROXY_DRIVER', 'proxyhat'),
    
    'drivers' => [
        'proxyhat' => [
            'host' => env('PROXY_HOST', 'gate.proxyhat.com'),
            'http_port' => env('PROXY_HTTP_PORT', 8080),
            'socks_port' => env('PROXY_SOCKS_PORT', 1080),
            'username' => env('PROXY_USERNAME'),
            'password' => env('PROXY_PASSWORD'),
        ],
    ],
    
    'defaults' => [
        'timeout' => env('PROXY_TIMEOUT', 30),
        'connect_timeout' => env('PROXY_CONNECT_TIMEOUT', 10),
        'max_retries' => env('PROXY_MAX_RETRIES', 3),
        'retry_delay' => env('PROXY_RETRY_DELAY', 1000), // ms
    ],
    
    'pool' => [
        'countries' => env('PROXY_COUNTRIES', 'US,DE,GB,FR,JP'),
        'sticky_ttl' => env('PROXY_STICKY_TTL', 1800), // 30 минут
    ],
];
<?php

// app/Services/ProxyService.php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class ProxyService
{
    private Client $client;
    private array $config;
    private array $sessions = [];
    
    public function __construct(array $config = null)
    {
        $this->config = $config ?? config('proxy');
        $this->client = $this->createClient();
    }
    
    /**
     * Создаёт Guzzle-клиент с настройками прокси
     */
    private function createClient(): Client
    {
        $handlerStack = HandlerStack::create();
        
        // Middleware для retry с экспоненциальной задержкой
        $handlerStack->push($this->createRetryMiddleware());
        
        // Middleware для логирования
        if (config('app.debug') || config('proxy.logging.enabled', false)) {
            $handlerStack->push($this->createLoggingMiddleware());
        }
        
        return new Client([
            'handler' => $handlerStack,
            'timeout' => $this->config['defaults']['timeout'],
            'connect_timeout' => $this->config['defaults']['connect_timeout'],
            'verify' => true,
            'headers' => $this->getDefaultHeaders(),
        ]);
    }
    
    /**
     * Middleware для повторных попыток
     */
    private function createRetryMiddleware(): callable
    {
        return Middleware::retry(
            function ($retries, RequestInterface $request, ResponseInterface $response = null, $exception = null) {
                $maxRetries = $this->config['defaults']['max_retries'];
                
                if ($retries >= $maxRetries) {
                    return false;
                }
                
                // Retry на 429, 5xx
                if ($response) {
                    $status = $response->getStatusCode();
                    if (in_array($status, [429, 500, 502, 503, 504])) {
                        Log::warning("Proxy request retrying", [
                            'status' => $status,
                            'attempt' => $retries + 1,
                            'url' => (string) $request->getUri(),
                        ]);
                        return true;
                    }
                }
                
                // Retry на сетевых ошибках
                if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
                    Log::warning("Proxy connection error, retrying", [
                        'attempt' => $retries + 1,
                        'error' => $exception->getMessage(),
                    ]);
                    return true;
                }
                
                return false;
            },
            function ($retries) {
                $baseDelay = $this->config['defaults']['retry_delay'];
                return $baseDelay * (2 ** $retries);
            }
        );
    }
    
    /**
     * Middleware для логирования запросов
     */
    private function createLoggingMiddleware(): callable
    {
        return Middleware::tap(
            function (RequestInterface $request, array $options) {
                Log::debug('Proxy request started', [
                    'method' => $request->getMethod(),
                    'url' => (string) $request->getUri(),
                    'proxy' => $options['proxy'] ?? 'none',
                ]);
            },
            function (RequestInterface $request, array $options, ResponseInterface $response) {
                Log::debug('Proxy request completed', [
                    'status' => $response->getStatusCode(),
                    'url' => (string) $request->getUri(),
                ]);
            }
        );
    }
    
    /**
     * Выполняет HTTP-запрос через residential-прокси
     */
    public function request(
        string $method,
        string $url,
        array $options = [],
        ?string $country = null,
        ?string $sessionId = null
    ): array {
        $proxyUrl = $this->buildProxyUrl($country, $sessionId);
        
        $mergedOptions = array_merge($options, [
            'proxy' => $proxyUrl,
        ]);
        
        $startTime = microtime(true);
        
        try {
            $response = $this->client->request($method, $url, $mergedOptions);
            
            $this->recordMetrics(true, microtime(true) - $startTime, $country);
            
            return [
                'success' => true,
                'status' => $response->getStatusCode(),
                'headers' => $response->getHeaders(),
                'body' => (string) $response->getBody(),
            ];
        } catch (RequestException $e) {
            $this->recordMetrics(false, microtime(true) - $startTime, $country);
            
            $response = $e->getResponse();
            
            return [
                'success' => false,
                'status' => $response?->getStatusCode() ?? 0,
                'error' => $e->getMessage(),
                'body' => $response ? (string) $response->getBody() : null,
            ];
        }
    }
    
    /**
     * Создаёт sticky-сессию (тот же IP для всех запросов)
     */
    public function createSession(?string $country = null): string
    {
        $sessionId = uniqid('sess_', true);
        $cacheKey = "proxy_session:{$sessionId}";
        
        Cache::put($cacheKey, [
            'country' => $country,
            'created_at' => now()->timestamp,
            'request_count' => 0,
        ], now()->addSeconds($this->config['pool']['sticky_ttl']));
        
        return $sessionId;
    }
    
    /**
     * Выполняет запрос в рамках существующей сессии
     */
    public function requestWithSession(
        string $method,
        string $url,
        string $sessionId,
        array $options = []
    ): array {
        $cacheKey = "proxy_session:{$sessionId}";
        $session = Cache::get($cacheKey);
        
        if (!$session) {
            throw new \InvalidArgumentException("Session expired or not found: {$sessionId}");
        }
        
        // Обновляем счётчик запросов
        $session['request_count']++;
        Cache::put($cacheKey, $session, now()->addSeconds($this->config['pool']['sticky_ttl']));
        
        return $this->request($method, $url, $options, $session['country'], $sessionId);
    }
    
    /**
     * Возвращает случайную страну из пула
     */
    public function getRandomCountry(): string
    {
        $countries = explode(',', $this->config['pool']['countries']);
        return $countries[array_rand($countries)];
    }
    
    /**
     * Пакетные запросы с конкурентностью
     */
    public function batch(array $requests, int $concurrency = 5): array
    {
        $results = [];
        $promises = [];
        
        foreach ($requests as $key => $request) {
            $country = $request['country'] ?? $this->getRandomCountry();
            $proxyUrl = $this->buildProxyUrl($country, $request['session'] ?? null);
            
            $promises[$key] = $this->client->requestAsync(
                $request['method'] ?? 'GET',
                $request['url'],
                array_merge($request['options'] ?? [], ['proxy' => $proxyUrl])
            );
        }
        
        // Ждём завершения всех запросов
        $responses = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
        
        foreach ($responses as $key => $result) {
            if ($result['state'] === 'fulfilled') {
                $response = $result['value'];
                $results[$key] = [
                    'success' => true,
                    'status' => $response->getStatusCode(),
                    'body' => (string) $response->getBody(),
                ];
            } else {
                $results[$key] = [
                    'success' => false,
                    'error' => $result['reason']->getMessage(),
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * Строит URL для прокси
     */
    private function buildProxyUrl(?string $country, ?string $sessionId): string
    {
        $driverConfig = $this->config['drivers'][$this->config['default']];
        
        $username = $driverConfig['username'];
        
        if ($country) {
            $username .= "-country-{$country}";
        }
        
        if ($sessionId) {
            $username .= "-session-{$sessionId}";
        }
        
        $host = $driverConfig['host'];
        $port = $driverConfig['http_port'];
        $password = $driverConfig['password'];
        
        return "http://{$username}:{$password}@{$host}:{$port}";
    }
    
    /**
     * Возвращает заголовки по умолчанию
     */
    private function getDefaultHeaders(): array
    {
        return [
            'User-Agent' => config('proxy.user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'),
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language' => 'en-US,en;q=0.9',
        ];
    }
    
    /**
     * Записывает метрики для мониторинга
     */
    private function recordMetrics(bool $success, float $duration, ?string $country): void
    {
        // Интеграция с Laravel Pulse/metrics
        if (function_exists('metrics')) {
            metrics()->increment('proxy.requests', 1, [
                'success' => $success ? 'true' : 'false',
                'country' => $country ?? 'rotating',
            ]);
            
            metrics()->timing('proxy.duration', $duration * 1000, [
                'country' => $country ?? 'rotating',
            ]);
        }
    }
}
<?php

// app/Jobs/ScrapeUrlJob.php

namespace App\Jobs;

use App\Services\ProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Exception;

class ScrapeUrlJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public int $tries = 3;
    public int $backoff = 10; // секунд между попытками
    
    private string $url;
    private ?string $country;
    private ?string $sessionId;
    private array $callbacks;
    
    public function __construct(
        string $url,
        ?string $country = null,
        ?string $sessionId = null,
        array $callbacks = []
    ) {
        $this->url = $url;
        $this->country = $country;
        $this->sessionId = $sessionId;
        $this->callbacks = $callbacks;
    }
    
    public function handle(ProxyService $proxyService): void
    {
        Log::info("Starting scrape job", ['url' => $this->url]);
        
        $result = $proxyService->request(
            'GET',
            $this->url,
            [],
            $this->country,
            $this->sessionId
        );
        
        if (!$result['success']) {
            Log::error("Scrape job failed", [
                'url' => $this->url,
                'error' => $result['error'],
            ]);
            
            throw new Exception($result['error']);
        }
        
        // Обрабатываем результат
        $this->processResult($result);
    }
    
    private function processResult(array $result): void
    {
        // Парсинг HTML, извлечение данных, сохранение в БД
        // ...
        
        Log::info("Scrape job completed", [
            'url' => $this->url,
            'status' => $result['status'],
            'size' => strlen($result['body']),
        ]);
    }
    
    /**
     * Обработка неудачной задачи
     */
    public function failed(Exception $exception): void
    {
        Log::error("Scrape job permanently failed", [
            'url' => $this->url,
            'exception' => $exception->getMessage(),
        ]);
    }
}

// Использование в контроллере или сервисе:

// Простой запрос
$proxyService = app(ProxyService::class);
$result = $proxyService->request('GET', 'https://example.com/data');

// С геотаргетингом
$result = $proxyService->request('GET', 'https://example.com/data', [], 'US');

// Sticky-сессия для многошагового скрейпинга
$sessionId = $proxyService->createSession('DE');
$proxyService->requestWithSession('GET', 'https://example.com/login', $sessionId);
$proxyService->requestWithSession('POST', 'https://example.com/login', $sessionId, [
    'form_params' => ['username' => 'user', 'password' => 'pass']
]);

// Диспетчеризация в очередь
ScrapeUrlJob::dispatch('https://example.com/data', 'US');

// Пакетная обработка
$results = $proxyService->batch([
    'us' => ['url' => 'https://httpbin.org/ip', 'country' => 'US'],
    'de' => ['url' => 'https://httpbin.org/ip', 'country' => 'DE'],
    'gb' => ['url' => 'https://httpbin.org/ip', 'country' => 'GB'],
]);

Multi-curl для конкурентного скрейпинга

Когда нужно выполнить сотни запросов параллельно, multi_curl — самый эффективный способ. Он использует один процесс и не требует внешних зависимостей.

<?php

class MultiCurlProxyClient
{
    private string $proxyHost = 'gate.proxyhat.com';
    private int $proxyPort = 8080;
    private string $username;
    private string $password;
    private int $maxConnections = 10;
    private int $timeout = 30;
    
    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
    
    /**
     * Выполняет массив запросов параллельно
     * 
     * @param array $requests Массив ['key' => ['url' => '...', 'options' => [...]]]
     * @param callable $progressCallback Колбэк прогресса (optional)
     * @return array Результаты по ключам
     */
    public function fetchAll(array $requests, ?callable $progressCallback = null): array
    {
        $results = [];
        $handles = [];
        
        // Создаём multi-handle
        $mh = curl_multi_init();
        
        // Настройка multi-handle
        curl_multi_setopt($mh, CURLMOPT_MAX_TOTAL_CONNECTIONS, $this->maxConnections);
        
        // Создаём индивидуальные handles
        foreach ($requests as $key => $request) {
            $ch = $this->createHandle($request['url'], $request['options'] ?? []);
            $handles[$key] = [
                'handle' => $ch,
                'url' => $request['url'],
            ];
            curl_multi_add_handle($mh, $ch);
        }
        
        // Выполняем запросы
        $active = null;
        $completed = 0;
        $total = count($requests);
        
        do {
            $status = curl_multi_exec($mh, $active);
            
            if ($status === CURLM_OK) {
                // Ждём активности
                curl_multi_select($mh, 1.0);
            }
            
            // Проверяем завершённые запросы
            while (($info = curl_multi_info_read($mh)) !== false) {
                if ($info['msg'] === CURLMSG_DONE) {
                    $ch = $info['handle'];
                    
                    // Находим ключ по handle
                    $key = $this->findKeyByHandle($handles, $ch);
                    
                    if ($key !== null) {
                        $results[$key] = $this->processResult($ch, $info['result']);
                        $completed++;
                        
                        if ($progressCallback) {
                            $progressCallback($completed, $total, $key, $results[$key]);
                        }
                    }
                    
                    curl_multi_remove_handle($mh, $ch);
                    curl_close($ch);
                    unset($handles[$key]);
                }
            }
            
        } while ($status === CURLM_CALL_MULTI_PERFORM || $active > 0);
        
        curl_multi_close($mh);
        
        return $results;
    }
    
    /**
     * Выполняет запросы с ограничением скорости (rate limiting)
     */
    public function fetchWithRateLimit(
        array $requests,
        int $requestsPerSecond = 5,
        ?callable $progressCallback = null
    ): array {
        $results = [];
        $batchSize = $this->maxConnections;
        $delayBetweenBatches = (1000 * $batchSize) / $requestsPerSecond;
        
        $batches = array_chunk($requests, $batchSize, true);
        
        foreach ($batches as $batchIndex => $batch) {
            if ($batchIndex > 0) {
                usleep((int) $delayBetweenBatches * 1000);
            }
            
            $batchResults = $this->fetchAll($batch, $progressCallback);
            $results = array_merge($results, $batchResults);
        }
        
        return $results;
    }
    
    /**
     * Создаёт cURL handle для запроса
     */
    private function createHandle(string $url, array $options = []): \CurlHandle
    {
        $ch = curl_init();
        
        // Формируем URL прокси с параметрами
        $proxyUser = $this->username;
        
        if (isset($options['country'])) {
            $proxyUser .= "-country-{$options['country']}";
        }
        
        if (isset($options['session'])) {
            $proxyUser .= "-session-{$options['session']}";
        }
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => $options['timeout'] ?? $this->timeout,
            CURLOPT_CONNECTTIMEOUT => $options['connect_timeout'] ?? 10,
            
            // Прокси
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$proxyUser}:{$this->password}",
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            
            // SSL
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            
            // Заголовки
            CURLOPT_USERAGENT => $options['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            CURLOPT_HTTPHEADER => $options['headers'] ?? [],
            
            // Для захвата заголовков
            CURLOPT_HEADER => true,
        ]);
        
        // POST-данные если есть
        if (isset($options['post_data'])) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $options['post_data']);
        }
        
        // Кастомный метод
        if (isset($options['method'])) {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $options['method']);
        }
        
        return $ch;
    }
    
    /**
     * Обрабатывает результат запроса
     */
    private function processResult(\CurlHandle $ch, int $curlResult): array
    {
        $response = curl_multi_getcontent($ch);
        $info = curl_getinfo($ch);
        $error = curl_error($ch);
        
        if ($curlResult !== CURLE_OK) {
            return [
                'success' => false,
                'error' => $error,
                'error_code' => $curlResult,
            ];
        }
        
        $headerSize = $info['header_size'];
        
        return [
            'success' => true,
            'status' => $info['http_code'],
            'headers' => substr($response, 0, $headerSize),
            'body' => substr($response, $headerSize),
            'info' => [
                'total_time' => $info['total_time'],
                'connect_time' => $info['connect_time'],
                'size_download' => $info['size_download'],
                'effective_url' => $info['url'],
            ],
        ];
    }
    
    /**
     * Находит ключ по handle в массиве
     */
    private function findKeyByHandle(array $handles, \CurlHandle $ch): ?string
    {
        foreach ($handles as $key => $data) {
            if ($data['handle'] === $ch) {
                return $key;
            }
        }
        return null;
    }
}

// Использование:
$client = new MultiCurlProxyClient('user', 'your_password');
$client->setMaxConnections(20);

// Генерируем список URL для скрейпинга
$urls = [];
for ($i = 1; $i <= 100; $i++) {
    $urls["page_{$i}"] = [
        'url' => "https://example.com/page/{$i}",
        'options' => ['country' => 'US'],
    ];
}

// Выполняем с прогрессом
$results = $client->fetchAll($urls, function ($completed, $total, $key, $result) {
    echo sprintf("[%d/%d] %s: %s\n", $completed, $total, $key, 
        $result['success'] ? "OK ({$result['status']})" : "ERROR: {$result['error']}"
    );
});

// С rate limiting (10 запросов в секунду)
$results = $client->fetchWithRateLimit($urls, 10);

// Анализ результатов
$successCount = count(array_filter($results, fn($r) => $r['success']));
echo "Успешных запросов: {$successCount} из " . count($results) . "\n";

TLS/SSL конфигурация и CA-бандлы

При работе через прокси критически важна правильная настройка TLS. Ошибки SSL могут маскироваться под проблемы с прокси, хотя на самом деле это отсутствие актуальных корневых сертификатов.

<?php

class TlsProxyClient
{
    private ?string $caBundlePath = null;
    private array $sslOptions = [];
    
    public function __construct()
    {
        $this->caBundlePath = $this->detectCaBundle();
        $this->configureSsl();
    }
    
    /**
     * Определяет путь к CA-бандлу
     */
    private function detectCaBundle(): string
    {
        // Приоритет поиска CA-бандла
        $candidates = [
            // Переменная окружения
            getenv('SSL_CERT_FILE') ?: null,
            
            // Системные пути (Linux)
            '/etc/ssl/certs/ca-certificates.crt',           // Debian/Ubuntu/Gentoo
            '/etc/pki/tls/certs/ca-bundle.crt',              // RHEL/CentOS/Fedora
            '/etc/ssl/ca-bundle.pem',                        // OpenSUSE
            '/etc/pki/tls/cacert.pem',                       // Alpine
            '/etc/ssl/cert.pem',                             // Alpine (альтернативный)
            '/usr/local/share/certs/ca-root-nss.crt',       // FreeBSD
            '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', // RHEL (новые версии)
            
            // macOS
            '/usr/local/etc/openssl/cert.pem',
            '/usr/local/etc/openssl@1.1/cert.pem',
            '/usr/local/etc/openssl@3/cert.pem',
            
            // Windows (при использовании WAMP/XAMPP)
            'C:\\wamp64\\bin\\php\\php7.4\\extras\\ssl\\cacert.pem',
            'C:\\xampp\\php\\extras\\ssl\\cacert.pem',
            
            // Composer CA-бандл (если установлен)
            dirname(__DIR__) . '/vendor/composer/ca-bundle/res/cacert.pem',
            
            // Локальный fallback
            __DIR__ . '/cacert.pem',
        ];
        
        foreach ($candidates as $path) {
            if ($path && file_exists($path) && is_readable($path)) {
                return $path;
            }
        }
        
        // Скачиваем Mozilla CA bundle если ничего не найдено
        return $this->downloadCaBundle();
    }
    
    /**
     * Скачивает актуальный CA-бандл от Mozilla
     */
    private function downloadCaBundle(): string
    {
        $targetPath = sys_get_temp_dir() . '/cacert-' . date('Y-m') . '.pem';
        
        if (file_exists($targetPath)) {
            return $targetPath;
        }
        
        $source = 'https://curl.se/ca/cacert.pem';
        $tempFile = tempnam(sys_get_temp_dir(), 'cacert');
        
        // Скачиваем без верификации (bootstrapping)
        $ch = curl_init($source);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false, // Только для начального скачивания!
            CURLOPT_FOLLOWLOCATION => true,
        ]);
        
        $data = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($code === 200 && $data) {
            file_put_contents($tempFile, $data);
            rename($tempFile, $targetPath);
            return $targetPath;
        }
        
        throw new RuntimeException('Failed to download CA bundle');
    }
    
    /**
     * Настраивает SSL-опции
     */
    private function configureSsl(): void
    {
        $this->sslOptions = [
            'verify_peer' => true,
            'verify_peer_name' => true,
            'verify_host' => 2,
            'ca_info' => $this->caBundlePath,
            
            // Минимальная версия TLS
            'ssl_version' => CURL_SSLVERSION_TLSv1_2,
            
            // Шифры (приоритет безопасных)
            'ssl_cipher_list' => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256',
        ];
    }
    
    /**
     * Возвращает опции cURL для SSL
     */
    public function getCurlSslOptions(): array
    {
        return [
            CURLOPT_SSL_VERIFYPEER => $this->sslOptions['verify_peer'],
            CURLOPT_SSL_VERIFYHOST => $this->sslOptions['verify_host'],
            CURLOPT_CAINFO => $this->sslOptions['ca_info'],
            CURLOPT_SSLVERSION => $this->sslOptions['ssl_version'],
            CURLOPT_SSL_CIPHER_LIST => $this->sslOptions['ssl_cipher_list'],
        ];
    }
    
    /**
     * Возвращает опции для Guzzle
     */
    public function getGuzzleSslOptions(): array
    {
        return [
            'verify' => $this->sslOptions['ca_info'],
        ];
    }
    
    /**
     * Возвращает опции для Symfony HttpClient
     */
    public function getSymfonySslOptions(): array
    {
        return [
            'verify_peer' => $this->sslOptions['verify_peer'],
            'verify_host' => $this->sslOptions['verify_host'],
            'cafile' => $this->sslOptions['ca_info'],
        ];
    }
    
    /**
     * Проверяет SSL-соединение через прокси
     */
    public function testSslConnection(string $proxyUrl, string $testUrl = 'https://www.google.com'): array
    {
        $ch = curl_init($testUrl);
        
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $proxyUrl,
            CURLOPT_TIMEOUT => 10,
        ] + $this->getCurlSslOptions());
        
        $response = curl_exec($ch);
        $info = curl_getinfo($ch);
        $error = curl_error($ch);
        $errno = curl_errno($ch);
        
        curl_close($ch);
        
        return [
            'success' => $errno === CURLE_OK && $info['http_code'] === 200,
            'http_code' => $info['http_code'],
            'ssl_verify_result' => $info['ssl_verify_result'],
            'error' => $error ?: null,
            'ca_path' => $this->caBundlePath,
            'cert_info' => $info['certinfo'] ?? [],
        ];
    }
    
    /**
     * Отключает верификацию (только для разработки!)
     */
    public function disableVerification(): void
    {
        if (getenv('APP_ENV') === 'production') {
            throw new RuntimeException('Cannot disable SSL verification in production');
        }
        
        $this->sslOptions['verify_peer'] = false;
        $this->sslOptions['verify_peer_name'] = false;
        $this->sslOptions['verify_host'] = 0;
    }
}

// Использование:
$sslClient = new TlsProxyClient();

// Проверяем путь к CA
 echo "CA Bundle: " . $sslClient->caBundlePath . "\n";

// Тестируем SSL через прокси
$proxyUrl = 'http://user-country-US:password@gate.proxyhat.com:8080';
$result = $sslClient->testSslConnection($proxyUrl);

if ($result['success']) {
    echo "SSL-соединение успешно\n";
} else {
    echo "SSL-ошибка: " . $result['error'] . "\n";
}

// Интеграция с Guzzle
$guzzleOptions = $sslClient->getGuzzleSslOptions();
$client = new GuzzleHttp\Client($guzzleOptions);

// Интеграция с Symfony HttpClient
$symfonyOptions = $sslClient->getSymfonySslOptions();
$client = Symfony\Component\HttpClient\HttpClient::create($symfonyOptions);

Сравнение подходов

Выбор библиотеки зависит от требований проекта:

Критерий cURL Guzzle Symfony HttpClient Multi-curl
Сложность Низкая Средняя Средняя Высокая
Асинхронность Нет Через promises Нативная Нативная
Производительность Базовая Хорошая Отличная Максимальная
Интеграция с Laravel Ручная Нативная Через HTTP-клиент Ручная
Retry-логика Ручная Middleware Callback Ручная
Подходит для Простые запросы API-интеграции Реактивные приложения Массовый скрейпинг

Рекомендация: Для Laravel proxy scraping используйте Guzzle с middleware для retry и логирования. Для высоконагруженного массового скрейпинга — multi_curl с пулом соединений.

Лучшие практики для production

  • Всегда включайте верификацию SSL — никогда не отключайте CURLOPT_SSL_VERIFYPEER в production. Используйте актуальный CA-бандл.
  • Настраивайте таймауты — connect_timeout 5-10 секунд, общий timeout 30 секунд для обычных запросов, до 120 для больших файлов.
  • Реализуйте circuit breaker — при серии неудач временно отключайте прокси и переключайтесь на резервный.
  • Логируйте метрики —成功率, latency, ошибки по странам. Это поможет выявить проблемные регионы.
  • Уважайте robots.txt и rate limits — даже с rotating proxies агрессивный скрейпинг приведёт к блокировкам.
  • Используйте sticky-сессии для multi-step — логин, навигация, получение данных должны идти через один IP.
  • Кэшируйте ответы — для SERP и ценовых данных кэш на 1-24 часа существенно снизит нагрузку.

Key Takeaways

  • cURL с CURLOPT_PROXY — базовый вариант для простых скриптов. Настройте CURLOPT_PROXYUSERPWD для авторизации в формате username-country-US-session-abc:password@host:port.
  • Guzzle — лучший выбор для Laravel. Используйте middleware для retry, логирования и метрик. Per-request прокси через опцию proxy в массиве запроса.
  • Symfony HttpClient — нативная асинхронность без promises. Метод stream() для обработки больших ответов и конкурентных запросов.
  • Multi-curl — максимальная производительность для массового скрейпинга. Контролируйте конкурентность через CURLMOPT_MAX_TOTAL_CONNECTIONS.
  • TLS/SSL — критически важна актуальность CA-бандла. Скачивайте Mozilla CA bundle или используйте системный путь.
  • ProxyHat — residential и mobile прокси с геотаргетингом и sticky-сессиями. Формат: http://user-country-XX-session-ID:pass@gate.proxyhat.com:8080.

Готовы начать? Изучите тарифы ProxyHat и получите доступ к пулу residential и mobile прокси для ваших PHP-проектов. Для массового скрейпинга ознакомьтесь с кейсом по сбору данных.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог