Kompletny przewodnik po proxy HTTP w PHP: cURL, Guzzle, Symfony i Laravel

Naucz się konfigurować proxy HTTP w PHP — od surowego cURL przez Guzzle aż po integrację z Laravel. Przykłady kodu dla scrapingu i integracji API z rotacją IP.

Kompletny przewodnik po proxy HTTP w PHP: cURL, Guzzle, Symfony i Laravel

Wprowadzenie: dlaczego PHP potrzebuje proxy?

PHP pozostaje jednym z najpopularniejszych języków do scrapingu stron, integracji z zewnętrznymi API i automatyzacji procesów backendowych. Niezależnie od tego, czy budujesz system monitoringu cen, czy pobierasz dane z publicznych rejestrów — prędzej czy później napotkasz limity zapytań, blokady geograficzne lub rate limiting.

Proxy HTTP w PHP rozwiązuje te problemy, maskując adres IP źródłowy i pozwalając na dystrybucję zapytań przez pulę adresów. W tym przewodniku pokażę Ci, jak skonfigurować proxy w najpopularniejszych bibliotekach PHP: od surowego cURL, przez Guzzle i Symfony HTTP Client, aż po gotowe rozwiązanie dla Laravel.

Podstawy: surowy cURL z CURLOPT_PROXY

Rozpocznijmy od fundamentów. Rozszerzenie cURL w PHP oferuje pełną kontrolę nad połączeniem HTTP, w tym konfigurację proxy. Dwie kluczowe opcje to CURLOPT_PROXY (adres serwera proxy) oraz CURLOPT_PROXYUSERPWD (dane uwierzytelniające).

Poniżej kompletny przykład produkcyjny z obsługą błędów, timeoutami i weryfikacją SSL:

<?php

declare(strict_types=1);

/**
 * Klasa do wykonywania zapytań HTTP przez proxy z pełną obsługą błędów.
 */
class ProxyHttpClient
{
    private string $proxyHost;
    private int $proxyPort;
    private string $proxyUser;
    private string $proxyPass;
    private int $timeout;
    private bool $verifySsl;

    public function __construct(
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080,
        string $proxyUser = 'user-country-US',
        string $proxyPass = 'PASSWORD',
        int $timeout = 30,
        bool $verifySsl = true
    ) {
        $this->proxyHost = $proxyHost;
        $this->proxyPort = $proxyPort;
        $this->proxyUser = $proxyUser;
        $this->proxyPass = $proxyPass;
        $this->timeout = $timeout;
        $this->verifySsl = $verifySsl;
    }

    /**
     * Wykonuje zapytanie GET przez proxy.
     *
     * @param string $url URL docelowy
     * @param array<string, string> $headers Dodatkowe nagłówki
     * @return array{status: int, body: string, headers: array}
     * @throws RuntimeException Gdy zapytanie się nie powiedzie
     */
    public function get(string $url, array $headers = []): array
    {
        $ch = curl_init();

        if ($ch === false) {
            throw new RuntimeException('Nie udało się zainicjalizować cURL');
        }

        try {
            // Podstawowa konfiguracja
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
            curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);

            // Konfiguracja proxy
            curl_setopt($ch, CURLOPT_PROXY, $this->proxyHost);
            curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxyPort);
            curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxyUser . ':' . $this->proxyPass);
            curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);

            // Konfiguracja SSL/TLS
            if ($this->verifySsl) {
                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
                // Użyj systemowego bundle CA lub określ ścieżkę
                $caBundle = $this->getCaBundlePath();
                if ($caBundle !== null) {
                    curl_setopt($ch, CURLOPT_CAINFO, $caBundle);
                }
            } else {
                // TYLKO dla developmentu!
                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
            }

            // Nagłówki
            if (!empty($headers)) {
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            }

            // Zapisz nagłówki odpowiedzi
            $responseHeaders = [];
            curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
                $len = strlen($header);
                $header = explode(':', $header, 2);
                if (count($header) < 2) {
                    return $len;
                }
                $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
                return $len;
            });

            // Wykonaj zapytanie
            $body = curl_exec($ch);
            $error = curl_error($ch);
            $errno = curl_errno($ch);
            $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

            if ($errno !== CURLE_OK) {
                throw new RuntimeException(
                    sprintf('Błąd cURL [%d]: %s', $errno, $error)
                );
            }

            return [
                'status' => (int) $status,
                'body' => (string) $body,
                'headers' => $responseHeaders,
            ];
        } finally {
            curl_close($ch);
        }
    }

    /**
     * Zwraca ścieżkę do bundle CA.
     */
    private function getCaBundlePath(): ?string
    {
        // Sprawdź typowe lokalizacje
        $paths = [
            '/etc/ssl/certs/ca-certificates.crt', // Debian/Ubuntu
            '/etc/pki/tls/certs/ca-bundle.crt',   // RHEL/CentOS
            '/usr/local/etc/openssl/cert.pem',    // macOS Homebrew
            __DIR__ . '/cacert.pem',              // Lokalny bundle
        ];

        foreach ($paths as $path) {
            if (file_exists($path)) {
                return $path;
            }
        }

        return null;
    }

    /**
     * Tworzy instancję z geo-targetingiem.
     */
    public static function withGeoTargeting(string $country, string $password): self
    {
        return new self(
            proxyUser: 'user-country-' . strtoupper($country),
            proxyPass: $password
        );
    }
}

// Przykład użycia
try {
    $client = ProxyHttpClient::withGeoTargeting('DE', 'twoje_haslo');
    $response = $client->get('https://httpbin.org/ip');
    
    echo "Status: " . $response['status'] . "\n";
    echo "Body: " . $response['body'] . "\n";
} catch (RuntimeException $e) {
    echo "Błąd: " . $e->getMessage() . "\n";
}

Kluczowe opcje cURL dla proxy

OpcjaOpisPrzykład wartości
CURLOPT_PROXYAdres hosta proxygate.proxyhat.com
CURLOPT_PROXYPORTPort proxy8080
CURLOPT_PROXYUSERPWDDane logowania (user:pass)user-country-US:haslo
CURLOPT_PROXYAUTHMetoda autoryzacjiCURLAUTH_BASIC
CURLOPT_PROXYTYPETyp proxy (HTTP/SOCKS)CURLPROXY_HTTP lub CURLPROXY_SOCKS5

Guzzle HTTP Client: nowoczesne podejście

Guzzle to najpopularniejszy klient HTTP w ekosystemie PHP. Oferuje czystszy API, obsługę asynchroniczną i łatwą integrację z PSR-7. Konfiguracja proxy jest prosta — wystarczy przekazać opcję proxy w tablicy konfiguracyjnej.

<?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;

/**
 * Serwis do scrapingu z rotacją proxy per zapytanie.
 */
class GuzzleProxyScraper
{
    private Client $client;
    private array $proxyCredentials;
    private array $usedSessions = [];

    public function __construct(string $password, array $countries = ['US', 'DE', 'GB'])
    {
        $this->proxyCredentials = [
            'host' => 'gate.proxyhat.com',
            'port' => 8080,
            'password' => $password,
            'countries' => $countries,
        ];

        // Handler z retry middleware
        $handlerStack = HandlerStack::create();
        
        // Dodaj middleware do retry z exponential backoff
        $handlerStack->push(Middleware::retry(
            function (int $retries, RequestInterface $request, ?ResponseInterface $response, ?RequestException $exception) {
                // Retry przy błędach 5xx lub problemach sieciowych
                if ($retries >= 3) {
                    return false;
                }
                if ($exception !== null) {
                    return true;
                }
                if ($response !== null && $response->getStatusCode() >= 500) {
                    return true;
                }
                return false;
            },
            function (int $retries) {
                return 1000 * (2 ** $retries); // Exponential backoff: 1s, 2s, 4s
            }
        ));

        $this->client = new Client([
            'handler' => $handlerStack,
            'timeout' => 30,
            'connect_timeout' => 10,
            'http_errors' => true,
            'verify' => true, // Weryfikacja SSL
        ]);
    }

    /**
     * Generuje losowe dane proxy z geo-targetingiem.
     */
    private function getRandomProxy(): array
    {
        $country = $this->proxyCredentials['countries'][array_rand($this->proxyCredentials['countries'])];
        $sessionId = bin2hex(random_bytes(8));
        
        return [
            'url' => sprintf(
                'http://%s:%s@%s:%d',
                'user-country-' . $country . '-session-' . $sessionId,
                $this->proxyCredentials['password'],
                $this->proxyCredentials['host'],
                $this->proxyCredentials['port']
            ),
            'country' => $country,
            'session' => $sessionId,
        ];
    }

    /**
     * Pobiera stronę przez losowe proxy.
     *
     * @throws RequestException
     */
    public function fetch(string $url, array $options = []): array
    {
        $proxy = $this->getRandomProxy();
        
        $defaultOptions = [
            'proxy' => $proxy['url'],
            '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',
                'Accept-Language' => 'en-US,en;q=0.9',
            ],
        ];

        $options = array_merge($defaultOptions, $options);
        
        $startTime = microtime(true);
        $response = $this->client->get($url, $options);
        $duration = microtime(true) - $startTime;

        return [
            'status' => $response->getStatusCode(),
            'body' => (string) $response->getBody(),
            'headers' => $response->getHeaders(),
            'proxy_country' => $proxy['country'],
            'proxy_session' => $proxy['session'],
            'duration' => $duration,
        ];
    }

    /**
     * Pobiera wiele URL-i z różnymi proxy (równolegle z Promise'ami).
     */
    public function fetchMultiple(array $urls, int $concurrency = 5): array
    {
        $promises = [];
        $results = [];

        foreach ($urls as $key => $url) {
            $proxy = $this->getRandomProxy();
            $promises[$key] = $this->client->getAsync($url, [
                'proxy' => $proxy['url'],
            ]);
        }

        // Czekaj na wszystkie zakończone Promise'y
        $responses = \GuzzleHttp\Promise\Utils::settle($promises)->wait();

        foreach ($responses as $key => $result) {
            if ($result['state'] === 'fulfilled') {
                $results[$key] = [
                    'status' => $result['value']->getStatusCode(),
                    'body' => (string) $result['value']->getBody(),
                ];
            } else {
                $results[$key] = [
                    'error' => $result['reason']->getMessage(),
                ];
            }
        }

        return $results;
    }
}

// Przykład użycia
$scraper = new GuzzleProxyScraper('twoje_haslo', ['US', 'DE', 'FR', 'PL']);

try {
    $result = $scraper->fetch('https://httpbin.org/ip');
    echo "IP przez proxy ({$result['proxy_country']}): " . $result['body'] . "\n";
    echo "Czas: " . round($result['duration'] * 1000, 2) . "ms\n";
} catch (RequestException $e) {
    echo "Błąd: " . $e->getMessage() . "\n";
}

Zalety Guzzle nad surowym cURL

  • Middleware — łatwe dodawanie retry, logowania, autoryzacji
  • Promise/Async — asynchroniczne zapytania bez blokowania
  • PSR-7 — standardowe obiekty Request/Response
  • Testowalność — mock handler dla testów jednostkowych

Symfony HTTP Client: wydajność i asynchroniczność

Symfony HTTP Client to nowoczesny komponent z natywną obsługą asynchroniczności, AMPHP i jednoczesnego przetwarzania wielu zapytań. Idealny dla aplikacji wymagających wysokiej wydajności.

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * Asynchroniczny klient HTTP z proxy dla Symfony.
 */
class SymfonyProxyClient
{
    private HttpClientInterface $client;
    private string $proxyUrl;

    public function __construct(string $password, string $country = 'US')
    {
        // Buduj URL proxy
        $this->proxyUrl = sprintf(
            'http://user-country-%s:%s@gate.proxyhat.com:8080',
            strtoupper($country),
            $password
        );

        $this->client = HttpClient::create([
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_peer' => true,
            'verify_host' => true,
            'cafile' => '/etc/ssl/certs/ca-certificates.crt', // Dostosuj do systemu
        ]);
    }

    /**
     * Tworzy instancję ze sticky session.
     */
    public static function withStickySession(string $password, string $sessionId, string $country = 'US'): self
    {
        $instance = new self($password, $country);
        $instance->proxyUrl = sprintf(
            'http://user-country-%s-session-%s:%s@gate.proxyhat.com:8080',
            strtoupper($country),
            $sessionId,
            $password
        );
        return $instance;
    }

    /**
     * Wykonuje pojedyncze zapytanie przez proxy.
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $options['proxy'] = $this->proxyUrl;
        
        return $this->client->request($method, $url, $options);
    }

    /**
     * Pobiera wiele URL-i równolegle z streaming.
     *
     * @param array<string> $urls Lista URL-i
     * @param callable|null $onProgress Callback postępu
     * @return array<string, array> Wyniki zmapowane URL => dane
     */
    public function fetchConcurrently(array $urls, ?callable $onProgress = null): array
    {
        $responses = [];
        $results = [];

        // Inicjalizuj wszystkie zapytania
        foreach ($urls as $url) {
            $responses[$url] = $this->client->request('GET', $url, [
                'proxy' => $this->proxyUrl,
                'on_progress' => $onProgress,
            ]);
        }

        // Przetwarzaj odpowiedzi w miarę ich nadejścia
        foreach ($responses as $url => $response) {
            try {
                $statusCode = $response->getStatusCode();
                $content = $response->getContent();
                $headers = $response->getHeaders();

                $results[$url] = [
                    'status' => $statusCode,
                    'body' => $content,
                    'headers' => $headers,
                ];
            } catch (\Exception $e) {
                $results[$url] = [
                    'error' => $e->getMessage(),
                    'status' => 0,
                ];
            }
        }

        return $results;
    }

    /**
     * Streaming z chunk'ami — idealne dla dużych plików.
     */
    public function streamWithCallback(
        string $url,
        callable $chunkCallback,
        int $bufferSize = 8192
    ): void {
        $response = $this->client->request('GET', $url, [
            'proxy' => $this->proxyUrl,
            'buffer' => false, // Wyłącz buforowanie w pamięci
        ]);

        foreach ($this->client->stream($response) as $chunk) {
            if ($chunk->isFirst()) {
                // Nagłówki dostępne
                $headers = $response->getHeaders();
                $chunkCallback(null, $headers, 'headers');
            } elseif ($chunk->isLast()) {
                $chunkCallback(null, null, 'complete');
            } else {
                $chunkCallback($chunk->getContent(), null, 'data');
            }
        }
    }
}

// Przykład użycia — pobieranie równoległe
$client = new SymfonyProxyClient('twoje_haslo', 'DE');

$urls = [
    'https://httpbin.org/ip',
    'https://httpbin.org/headers',
    'https://httpbin.org/user-agent',
];

$results = $client->fetchConcurrently($urls);

foreach ($results as $url => $data) {
    echo "URL: $url\n";
    if (isset($data['error'])) {
        echo "  Error: {$data['error']}\n";
    } else {
        echo "  Status: {$data['status']}\n";
        echo "  Body: " . substr($data['body'], 0, 100) . "...\n";
    }
}

Laravel: serwis do zarządzania pulą proxy

W aplikacjach Laravel warto wydzielić logikę proxy do osobnego serwisu, który może być wstrzykiwany do Jobów, Commandów i Controllerów. Poniżej kompletna implementacja z pulą residential proxy.

<?php

namespace App\Services\Proxy;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use RuntimeException;

/**
 * Konfiguracja proxy w config/services.php:
 * 
 * 'proxyhat' => [
 *     'host' => env('PROXY_HOST', 'gate.proxyhat.com'),
 *     'port' => env('PROXY_PORT', 8080),
 *     'username' => env('PROXY_USERNAME', 'user'),
 *     'password' => env('PROXY_PASSWORD'),
 *     'countries' => env('PROXY_COUNTRIES', 'US,DE,GB'),
 * ],
 */

class ResidentialProxyPool
{
    private Client $client;
    private array $config;
    private int $maxRetries;
    private int $retryDelay;

    public function __construct(array $config = null, int $maxRetries = 3, int $retryDelay = 1000)
    {
        $this->config = $config ?? config('services.proxyhat');
        $this->maxRetries = $maxRetries;
        $this->retryDelay = $retryDelay;

        $this->client = new Client([
            'timeout' => 60,
            'connect_timeout' => 15,
            'verify' => true,
        ]);
    }

    /**
     * Tworzy URL proxy z określonymi opcjami.
     *
     * @param array{country?: string, city?: string, session?: string} $options
     * @return string
     */
    public function buildProxyUrl(array $options = []): string
    {
        $username = $this->config['username'];
        
        // Dodaj geo-targeting
        if (isset($options['country'])) {
            $username .= '-country-' . strtoupper($options['country']);
        }
        
        if (isset($options['city'])) {
            $username .= '-city-' . strtolower($options['city']);
        }
        
        // Sticky session
        if (isset($options['session'])) {
            $username .= '-session-' . $options['session'];
        } else {
            // Losowa sesja dla rotacji per zapytanie
            $username .= '-session-' . bin2hex(random_bytes(8));
        }

        return sprintf(
            'http://%s:%s@%s:%d',
            $username,
            $this->config['password'],
            $this->config['host'],
            $this->config['port']
        );
    }

    /**
     * Pobiera losowy kraj z puli.
     */
    public function getRandomCountry(): string
    {
        $countries = explode(',', $this->config['countries'] ?? 'US');
        return $countries[array_rand($countries)];
    }

    /**
     * Wykonuje zapytanie GET przez proxy z retry.
     *
     * @param string $url URL docelowy
     * @param array{country?: string, session?: string, headers?: array} $options
     * @return array{status: int, body: string, ip: string}
     * @throws RuntimeException
     */
    public function get(string $url, array $options = []): array
    {
        $lastException = null;
        
        for ($attempt = 0; $attempt < $this->maxRetries; $attempt++) {
            $proxyUrl = $this->buildProxyUrl($options);
            $country = $options['country'] ?? $this->getRandomCountry();
            
            try {
                Log::debug('Proxy request attempt', [
                    'url' => $url,
                    'country' => $country,
                    'attempt' => $attempt + 1,
                ]);

                $response = $this->client->get($url, [
                    'proxy' => $proxyUrl,
                    'headers' => $options['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 [
                    'status' => $response->getStatusCode(),
                    'body' => (string) $response->getBody(),
                    'proxy_country' => $country,
                ];
            } catch (RequestException $e) {
                $lastException = $e;
                
                Log::warning('Proxy request failed', [
                    'url' => $url,
                    'attempt' => $attempt + 1,
                    'error' => $e->getMessage(),
                ]);

                // Czekaj przed ponowną próbą
                if ($attempt < $this->maxRetries - 1) {
                    usleep($this->retryDelay * 1000 * ($attempt + 1));
                }
            }
        }

        throw new RuntimeException(
            'Proxy request failed after ' . $this->maxRetries . ' attempts: ' . $lastException?->getMessage(),
            0,
            $lastException
        );
    }

    /**
     * Pobiera zawartość z cache lub wykonuje zapytanie.
     *
     * @param string $key Klucz cache
     * @param string $url URL do pobrania
     * @param int $ttl Czas życia cache w sekundach
     * @param array $proxyOptions Opcje proxy
     */
    public function getOrCache(string $key, string $url, int $ttl = 3600, array $proxyOptions = []): string
    {
        return Cache::remember($key, $ttl, function () use ($url, $proxyOptions) {
            $result = $this->get($url, $proxyOptions);
            return $result['body'];
        });
    }
}

Teraz przykład Job'a Laravel, który wykorzystuje ten serwis:

<?php

namespace App\Jobs;

use App\Services\Proxy\ResidentialProxyPool;
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;

class ScrapeProductPrice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;

    protected string $productUrl;
    protected string $country;

    public function __construct(string $productUrl, string $country = 'US')
    {
        $this->productUrl = $productUrl;
        $this->country = $country;
    }

    public function handle(ResidentialProxyPool $proxyPool): void
    {
        Log::info('Starting price scrape', [
            'url' => $this->productUrl,
            'country' => $this->country,
        ]);

        try {
            $result = $proxyPool->get($this->productUrl, [
                'country' => $this->country,
                'session' => 'price-' . md5($this->productUrl), // Sticky session
            ]);

            // Parsuj HTML i wyekstrahuj cenę
            $price = $this->extractPrice($result['body']);

            // Zapisz do bazy lub cache
            // Price::updateOrCreate(['url' => $this->productUrl], ['price' => $price]);

            Log::info('Price scraped successfully', [
                'url' => $this->productUrl,
                'price' => $price,
                'proxy_country' => $result['proxy_country'],
            ]);
        } catch (\Exception $e) {
            Log::error('Price scrape failed', [
                'url' => $this->productUrl,
                'error' => $e->getMessage(),
            ]);

            throw $e; // Retry job
        }
    }

    private function extractPrice(string $html): ?float
    {
        // Implementacja parsowania HTML
        if (preg_match('/"price":\s*([0-9.]+)/', $html, $matches)) {
            return (float) $matches[1];
        }
        return null;
    }
}

// W kontrolerze lub Service
use App\Jobs\ScrapeProductPrice;

class PriceMonitorController extends Controller
{
    public function queueScrape(Request $request)
    {
        $urls = $request->input('urls', []);
        $country = $request->input('country', 'US');

        foreach ($urls as $url) {
            ScrapeProductPrice::dispatch($url, $country)
                ->onQueue('scraping');
        }

        return response()->json([
            'queued' => count($urls),
            'message' => 'Jobs dispatched successfully',
        ]);
    }
}

multi_curl: równoległe pobieranie bez frameworków

Dla aplikacji bez Guzzle czy Symfony, natywny curl_multi_* pozwala na równoległe pobieranie wielu stron. To najszybsza metoda w czystym PHP.

<?php

declare(strict_types=1);

/**
 * Równoległy klient HTTP z proxy dla wielu zapytań.
 */
class MultiCurlProxyClient
{
    private string $proxyHost;
    private int $proxyPort;
    private string $proxyPassword;
    private int $maxConnections;
    private int $timeout;

    public function __construct(
        string $proxyPassword,
        int $maxConnections = 10,
        int $timeout = 30
    ) {
        $this->proxyHost = 'gate.proxyhat.com';
        $this->proxyPort = 8080;
        $this->proxyPassword = $proxyPassword;
        $this->maxConnections = $maxConnections;
        $this->timeout = $timeout;
    }

    /**
     * Pobiera wiele URL-i równolegle z różnymi proxy.
     *
     * @param array<string> $urls Lista URL-i do pobrania
     * @param array<string> $countries Lista krajów do dystrybucji
     * @return array<string, array> URL => wynik
     */
    public function fetchAll(array $urls, array $countries = ['US', 'DE', 'GB']): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];

        if ($mh === false) {
            throw new RuntimeException('Nie udało się zainicjalizować curl_multi');
        }

        try {
            // Inicjalizuj wszystkie uchwyty cURL
            foreach ($urls as $key => $url) {
                $country = $countries[$key % count($countries)];
                $sessionId = bin2hex(random_bytes(4));
                
                $ch = curl_init($url);
                if ($ch === false) {
                    continue;
                }

                // Konfiguracja proxy
                $proxyUser = 'user-country-' . $country . '-session-' . $sessionId;
                
                curl_setopt_array($ch, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_FOLLOWLOCATION => true,
                    CURLOPT_MAXREDIRS => 5,
                    CURLOPT_TIMEOUT => $this->timeout,
                    CURLOPT_CONNECTTIMEOUT => 10,
                    CURLOPT_PROXY => $this->proxyHost,
                    CURLOPT_PROXYPORT => $this->proxyPort,
                    CURLOPT_PROXYUSERPWD => $proxyUser . ':' . $this->proxyPassword,
                    CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
                    CURLOPT_SSL_VERIFYPEER => true,
                    CURLOPT_SSL_VERIFYHOST => 2,
                    CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                ]);

                curl_multi_add_handle($mh, $ch);
                $handles[$key] = [
                    'handle' => $ch,
                    'url' => $url,
                    'country' => $country,
                ];
            }

            // Wykonaj wszystkie zapytania
            $active = null;
            do {
                $status = curl_multi_exec($mh, $active);
                
                if ($active) {
                    // Czekaj na aktywność z timeout
                    curl_multi_select($mh, 1.0);
                }
            } while ($status === CURLM_CALL_MULTI_PERFORM || $active);

            // Pobierz wyniki
            foreach ($handles as $key => $data) {
                $ch = $data['handle'];
                
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $error = curl_error($ch);
                $errno = curl_errno($ch);
                $content = curl_multi_getcontent($ch);
                
                if ($errno !== CURLE_OK) {
                    $results[$key] = [
                        'success' => false,
                        'error' => $error,
                        'errno' => $errno,
                        'url' => $data['url'],
                    ];
                } else {
                    $results[$key] = [
                        'success' => true,
                        'status' => $httpCode,
                        'body' => $content,
                        'url' => $data['url'],
                        'country' => $data['country'],
                    ];
                }
                
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
            }
        } finally {
            curl_multi_close($mh);
        }

        return $results;
    }

    /**
     * Pobiera z batch'ingiem dla bardzo dużej liczby URL-i.
     *
     * @param array<string> $urls Lista URL-i
     * @param callable $onProgress Callback postępu
     */
    public function fetchBatched(
        array $urls,
        callable $onProgress = null,
        array $countries = ['US', 'DE', 'GB', 'FR', 'PL']
    ): array {
        $batches = array_chunk($urls, $this->maxConnections, true);
        $allResults = [];
        $completed = 0;
        $total = count($urls);

        foreach ($batches as $batch) {
            $results = $this->fetchAll($batch, $countries);
            $allResults = array_merge($allResults, $results);
            $completed += count($batch);

            if ($onProgress !== null) {
                $onProgress($completed, $total, $results);
            }

            // Krótka pauza między batch'ami
            usleep(100000); // 100ms
        }

        return $allResults;
    }
}

// Przykład użycia
$client = new MultiCurlProxyClient('twoje_haslo', maxConnections: 20);

$urls = [];
for ($i = 1; $i <= 50; $i++) {
    $urls[] = "https://httpbin.org/delay/1?n={$i}";
}

$startTime = microtime(true);

$results = $client->fetchBatched($urls, function ($completed, $total, $batchResults) {
    echo "Postęp: {$completed}/{$total}\n";
}, ['US', 'DE', 'GB', 'FR', 'PL']);

$duration = microtime(true) - $startTime;

$successCount = count(array_filter($results, fn($r) => $r['success'] ?? false));

echo "Pobrano {$successCount}/" . count($urls) . " URL-i w " . round($duration, 2) . "s\n";
echo "Średnio " . round($duration / count($urls) * 1000, 0) . "ms na URL\n";

TLS/SSL: bezpieczna konfiguracja i CA bundle

Bezpieczeństwo połączeń przez proxy jest krytyczne — dane przesyłane przez proxy mogą być przechwytywane, jeśli nie użyjesz HTTPS z poprawną weryfikacją certyfikatów.

Typowe problemy z SSL w PHP

  • Brak bundle CA — PHP nie zawsze ma dostęp do aktualnych certyfikatów root
  • Przestarzałe wersje TLS — niektóre serwery wymagają TLS 1.2+
  • SNI (Server Name Indication) — wymagane dla wirtualnych hostów HTTPS
<?php

declare(strict_types=1);

/**
 * Konfiguracja TLS/SSL dla bezpiecznych połączeń przez proxy.
 */
class SecureProxyClient
{
    private ?string $caBundlePath;
    private int $minTlsVersion;
    private bool $verifyPeer;
    private bool $verifyHost;

    public function __construct(
        ?string $caBundlePath = null,
        int $minTlsVersion = CURL_SSLVERSION_TLSv1_2,
        bool $verifyPeer = true,
        bool $verifyHost = true
    ) {
        $this->caBundlePath = $caBundlePath ?? $this->findCaBundle();
        $this->minTlsVersion = $minTlsVersion;
        $this->verifyPeer = $verifyPeer;
        $this->verifyHost = $verifyHost;
    }

    /**
     * Znajduje bundle CA w systemie.
     */
    private function findCaBundle(): ?string
    {
        // Sprawdź zmienną środowiskową
        $envBundle = getenv('CURL_CA_BUNDLE');
        if ($envBundle && file_exists($envBundle)) {
            return $envBundle;
        }

        // Sprawdź typowe lokalizacje
        $paths = [
            // Linux
            '/etc/ssl/certs/ca-certificates.crt',
            '/etc/pki/tls/certs/ca-bundle.crt',
            '/etc/ssl/ca-bundle.pem',
            // macOS
            '/usr/local/etc/openssl/cert.pem',
            '/usr/local/etc/openssl@1.1/cert.pem',
            '/opt/homebrew/etc/openssl@3/cert.pem',
            // Windows (XAMPP, WAMP)
            'C:\xampp\php\extras\openssl\cacert.pem',
            // Composer bundle
            dirname(__DIR__) . '/vendor/composer/ca-bundle/res/cacert.pem',
        ];

        foreach ($paths as $path) {
            if (file_exists($path)) {
                return $path;
            }
        }

        return null;
    }

    /**
     * Pobiera bundle CA z mozilli (fallback).
     */
    public static function downloadMozBundle(string $targetPath): bool
    {
        $url = 'https://curl.se/ca/cacert.pem';
        
        $ch = curl_init($url);
        if ($ch === false) {
            return false;
        }

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_TIMEOUT => 60,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);

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

        if ($data === false || !empty($error)) {
            return false;
        }

        return file_put_contents($targetPath, $data) !== false;
    }

    /**
     * Konfiguruje opcje SSL dla cURL.
     */
    public function configureSsl($ch): void
    {
        // Weryfikacja certyfikatu serwera
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->verifyHost ? 2 : 0);

        // Minimalna wersja TLS
        curl_setopt($ch, CURLOPT_SSLVERSION, $this->minTlsVersion);

        // Bundle CA
        if ($this->caBundlePath !== null) {
            curl_setopt($ch, CURLOPT_CAINFO, $this->caBundlePath);
            curl_setopt($ch, CURLOPT_CAPATH, dirname($this->caBundlePath));
        }

        // SNI (Server Name Indication) — domyślnie włączone w nowym cURL
        curl_setopt($ch, CURLOPT_SSL_ENABLE_SNI, true);

        // Lista szyfrów (cipher suites)
        // curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256');
    }

    /**
     * Bezpieczne zapytanie przez proxy z pełną weryfikacją SSL.
     */
    public function secureGet(string $url, string $proxyUser, string $proxyPass): array
    {
        $ch = curl_init($url);
        
        if ($ch === false) {
            throw new RuntimeException('Nie udało się zainicjalizować cURL');
        }

        try {
            // Podstawowa konfiguracja
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 5,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_CONNECTTIMEOUT => 10,
                // Proxy
                CURLOPT_PROXY => 'gate.proxyhat.com',
                CURLOPT_PROXYPORT => 8080,
                CURLOPT_PROXYUSERPWD => $proxyUser . ':' . $proxyPass,
                CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
            ]);

            // Konfiguracja SSL
            $this->configureSsl($ch);

            // Nagłówki
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                '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',
            ]);

            // Wykonaj
            $body = curl_exec($ch);
            $error = curl_error($ch);
            $errno = curl_errno($ch);
            $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);

            if ($errno !== CURLE_OK) {
                throw new RuntimeException("Błąd cURL [{$errno}]: {$error}");
            }

            if ($this->verifyPeer && $sslVerifyResult !== 0) {
                throw new RuntimeException("Weryfikacja SSL nie powiodła się: {$sslVerifyResult}");
            }

            return [
                'status' => (int) $status,
                'body' => (string) $body,
                'ssl_verified' => $this->verifyPeer,
                'ca_bundle' => $this->caBundlePath,
            ];
        } finally {
            curl_close($ch);
        }
    }

    /**
     * Sprawdza konfigurację SSL.
     */
    public function diagnoseSsl(): array
    {
        $version = curl_version();
        
        return [
            'curl_version' => $version['version'],
            'ssl_version' => $version['ssl_version'],
            'ca_bundle' => $this->caBundlePath,
            'ca_exists' => $this->caBundlePath !== null && file_exists($this->caBundlePath),
            'supports_tls_1_2' => defined('CURL_SSLVERSION_TLSv1_2'),
            'supports_tls_1_3' => defined('CURL_SSLVERSION_TLSv1_3'),
            'sni_support' => $version['features'] & CURL_VERSION_SSL,
        ];
    }
}

// Inicjalizacja i test
$client = new SecureProxyClient();

// Diagnoza
$diag = $client->diagnoseSsl();
echo "=== Diagnoza SSL ===\n";
echo "cURL: {$diag['curl_version']}\n";
echo "SSL: {$diag['ssl_version']}\n";
echo "CA Bundle: {$diag['ca_bundle']}\n";
echo "CA Exists: " . ($diag['ca_exists'] ? 'Yes' : 'No') . "\n";

// Pobierz bundle CA jeśli brakuje
if (!$diag['ca_exists']) {
    echo "Pobieranie CA bundle...\n";
    SecureProxyClient::downloadMozBundle(__DIR__ . '/cacert.pem');
}

Porównanie metod

MetodaZaletyWadyNajlepsze zastosowanie
cURLPełna kontrola, brak zależności, najwyższa wydajnośćVerbose API, ręczna obsługa błędówProste skrypty, CLI tools
GuzzleCzyste API, middleware, Promise, PSR-7Dodatkowa zależnośćAplikacje Symfony/Laravel, API clients
Symfony HTTPNative async, streaming, AMPHP integrationWiększa złożonośćWysoka wydajność, streaming
multi_curlRównoległość bez frameworkówSkomplikowany kod, brak abstrakcjiBatch processing, scraping dużej skali
Laravel ServiceDI, queue integration, cachingTylko dla projektów LaravelAplikacje Laravel, background jobs

Wskazówka: Dla nowych projektów polecam Guzzle jako domyślny wybór — oferuje najlepszy stosunek prostoty do możliwości. Używaj multi_curl tylko gdy potrzebujesz maksymalnej wydajności bez zależności frameworkowych.

Kluczowe wnioski

  • Zawsze weryfikuj SSL — nigdy nie wyłączaj verify_peer w produkcji. Pobierz aktualny CA bundle jeśli brakuje w systemie.
  • Używaj sticky sessions — dla scrapingu wieloetapowego (logowanie, koszyk) używaj parametru session w username, by zachować ten sam IP.
  • Implementuj retry z backoff — proxy residential może mieć przejściowe problemy; exponential backoff zwiększa sukces bez DDoS'owania.
  • Rozdziel zapytania — używaj różnych krajów i sesji dla równoległych zapytań, by uniknąć rate limitów.
  • Loguj wszystko — zapisuj kraj proxy, session ID i czas odpowiedzi dla każdego zapytania — to kluczowe dla debugowania.
  • Cache'uj agresywnie — używaj cache dla danych, które rzadko się zmieniają (np. HTML kategorii, listy produktów).

Dla profesjonalnego scrapingu w PHP, residential proxy od ProxyHat z geo-targetingiem i sticky sessions to fundament stabilnej infrastruktury. Połącz to z retry logic i monitoringiem, a Twój scraper będzie działać niezawodnie nawet przy dużej skali.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga