Guide Complet des Proxies HTTP en PHP : cURL, Guzzle, Symfony et Laravel

Apprenez à configurer et utiliser des proxies HTTP en PHP avec cURL, Guzzle, Symfony HTTP Client et Laravel. Guide pratique avec exemples de code pour le scraping et l'intégration d'API tierces.

Guide Complet des Proxies HTTP en PHP : cURL, Guzzle, Symfony et Laravel

Les développeurs PHP qui travaillent sur des projets de scraping, d'intégration d'API tierces ou d'automatisation rencontrent inévitablement des limitations : rate limits, blocages géographiques, restrictions par IP. La solution ? Configurer correctement un proxy PHP pour router vos requêtes HTTP. Ce guide vous montre comment implémenter des proxies avec cURL natif, Guzzle, Symfony HTTP Client, et dans un contexte Laravel.

Pourquoi Utiliser un Proxy en PHP ?

Lorsque vous effectuez des requêtes HTTP répétées vers un même service, plusieurs problèmes surgissent :

  • Rate limiting : Les API limitent souvent le nombre de requêtes par IP
  • Blocages géographiques : Certains contenus ne sont accessibles que depuis certains pays
  • Anti-bot detection : Les sites modernes bloquent les comportements automatisés
  • IP bans : Une activité intense peut mener à un bannissement

Un proxy résout ces problèmes en masquant votre IP d'origine et en distribuant les requêtes across un pool d'adresses. Pour le Laravel proxy scraping ou l'intégration d'API tierces, c'est souvent indispensable.

cURL Natif : Configuration Basique avec CURLOPT_PROXY

L'extension cURL de PHP reste la méthode la plus directe pour configurer un proxy. Les options clés sont CURLOPT_PROXY pour l'adresse du proxy et CURLOPT_PROXYUSERPWD pour l'authentification.

Voici un exemple complet avec gestion d'erreurs :

<?php

function fetchWithProxy(string $url, string $proxyHost, int $proxyPort, string $username, string $password): string
{
    $ch = curl_init();
    
    // Configuration de base
    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, 30);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    
    // Configuration du proxy
    curl_setopt($ch, CURLOPT_PROXY, $proxyHost);
    curl_setopt($ch, CURLOPT_PROXYPORT, $proxyPort);
    curl_setopt($ch, CURLOPT_PROXYUSERPWD, "$username:$password");
    
    // TLS/SSL - Important pour HTTPS
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/ca-bundle.crt');
    
    // Headers pour paraître plus légitime
    $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: fr-FR,fr;q=0.9,en;q=0.8',
    ];
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    
    $response = curl_exec($ch);
    
    if (curl_errno($ch)) {
        $error = curl_error($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        throw new RuntimeException("cURL Error: $error (HTTP $httpCode)");
    }
    
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode >= 400) {
        throw new RuntimeException("HTTP Error: $httpCode");
    }
    
    return $response;
}

// Utilisation avec ProxyHat
$url = 'https://httpbin.org/ip';
$proxyHost = 'gate.proxyhat.com';
$proxyPort = 8080;
$username = 'user-country-FR';
$password = 'your-password';

try {
    $result = fetchWithProxy($url, $proxyHost, $proxyPort, $username, $password);
    echo $result;
} catch (RuntimeException $e) {
    echo "Erreur: " . $e->getMessage();
}

Cette approche donne un contrôle total mais nécessite de gérer manuellement chaque aspect de la requête. Pour des projets plus complexes, un client HTTP comme Guzzle simplifie grandement le code.

Guzzle : Configuration Proxy et Rotation par Requête

Guzzle est le client HTTP le plus populaire dans l'écosystème PHP. La configuration du Guzzle proxy se fait via les options de requête, permettant même une rotation par requête.

Configuration Basique Guzzle

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class ProxyClient
{
    private Client $client;
    private string $proxyUrl;
    
    public function __construct(string $username, string $password, string $host = 'gate.proxyhat.com', int $port = 8080)
    {
        $this->proxyUrl = "http://{$username}:{$password}@{$host}:{$port}";
        
        $this->client = new Client([
            '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',
            ],
        ]);
    }
    
    public function get(string $url, array $options = []): string
    {
        $options = array_merge($options, ['proxy' => $this->proxyUrl]);
        
        try {
            $response = $this->client->get($url, $options);
            return (string) $response->getBody();
        } catch (RequestException $e) {
            $response = $e->getResponse();
            $code = $response ? $response->getStatusCode() : 'N/A';
            throw new RuntimeException("Request failed: {$e->getMessage()} (HTTP $code)");
        }
    }
    
    public function post(string $url, array $data, array $options = []): string
    {
        $options = array_merge($options, [
            'proxy' => $this->proxyUrl,
            'form_params' => $data,
        ]);
        
        $response = $this->client->post($url, $options);
        return (string) $response->getBody();
    }
}

// Utilisation
$client = new ProxyClient('user-country-US', 'your-password');
$response = $client->get('https://httpbin.org/ip');
echo $response;

Rotation de Proxy par Requête

Pour le scraping à grande échelle, vous devez faire pivoter les IPs entre les requêtes. Avec les proxies résidentiels ProxyHat, utilisez des sessions sticky ou changez le paramètre de géolocalisation :

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Promise;

class RotatingProxyClient
{
    private Client $client;
    private string $baseUsername;
    private string $password;
    private string $host;
    private int $port;
    
    public function __construct(
        string $baseUsername,
        string $password,
        string $host = 'gate.proxyhat.com',
        int $port = 8080
    ) {
        $this->baseUsername = $baseUsername;
        $this->password = $password;
        $this->host = $host;
        $this->port = $port;
        
        $this->client = new Client([
            'timeout' => 30,
            'connect_timeout' => 10,
            'verify' => true,
        ]);
    }
    
    private function generateProxyUrl(?string $country = null, ?string $session = null): string
    {
        $username = $this->baseUsername;
        
        if ($country) {
            $username .= "-country-$country";
        }
        
        if ($session) {
            $username .= "-session-$session";
        }
        
        return "http://{$username}:{$this->password}@{$this->host}:{$this->port}";
    }
    
    public function fetchWithRotation(array $urls, array $countries = ['US', 'FR', 'DE', 'GB']): array
    {
        $results = [];
        $promises = [];
        
        foreach ($urls as $index => $url) {
            $country = $countries[$index % count($countries)];
            $sessionId = uniqid('sess_', true);
            
            $promises[$url] = $this->client->getAsync($url, [
                'proxy' => $this->generateProxyUrl($country, $sessionId),
                'headers' => [
                    'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                ],
            ]);
        }
        
        // Résoudre toutes les promesses
        $responses = Promise\Utils::settle($promises)->wait();
        
        foreach ($responses as $url => $result) {
            if ($result['state'] === 'fulfilled') {
                $results[$url] = [
                    'success' => true,
                    'body' => (string) $result['value']->getBody(),
                ];
            } else {
                $results[$url] = [
                    'success' => false,
                    'error' => $result['reason']->getMessage(),
                ];
            }
        }
        
        return $results;
    }
}

// Exemple d'utilisation
$client = new RotatingProxyClient('user', 'your-password');
$urls = [
    'https://httpbin.org/ip',
    'https://httpbin.org/user-agent',
    'https://httpbin.org/headers',
];

$results = $client->fetchWithRotation($urls);
print_r($results);

Symfony HTTP Client : Proxy et Réponses Asynchrones

Le Symfony HTTP Client est particulièrement intéressant pour son support natif des réponses asynchrones et son intégration avec les composants Symfony. Il utilise cURL en interne mais avec une API plus élégante.

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;

class SymfonyProxyClient
{
    private $client;
    private string $proxyUrl;
    
    public function __construct(string $username, string $password, string $host = 'gate.proxyhat.com', int $port = 8080)
    {
        $this->proxyUrl = "http://{$username}:{$password}@{$host}:{$port}";
        
        $this->client = HttpClient::create([
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_host' => true,
            'verify_peer' => true,
        ]);
    }
    
    public function fetch(string $url, string $method = 'GET', array $options = []): array
    {
        $defaultOptions = [
            'proxy' => $this->proxyUrl,
            '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',
            ],
        ];
        
        $options = array_merge($defaultOptions, $options);
        
        try {
            $response = $this->client->request($method, $url, $options);
            
            //getStatusCode() ne lance pas d'exception, on vérifie manuellement
            $statusCode = $response->getStatusCode();
            
            if ($statusCode >= 400) {
                return [
                    'success' => false,
                    'status_code' => $statusCode,
                    'error' => "HTTP Error: $statusCode",
                    'headers' => $response->getHeaders(),
                ];
            }
            
            return [
                'success' => true,
                'status_code' => $statusCode,
                'body' => $response->getContent(),
                'headers' => $response->getHeaders(),
            ];
        } catch (TransportException $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    public function fetchConcurrent(array $urls, int $maxConcurrent = 5): array
    {
        $responses = [];
        $results = [];
        
        // Initialiser toutes les requêtes (non-bloquant)
        foreach ($urls as $key => $url) {
            $responses[$key] = $this->client->request('GET', $url, [
                'proxy' => $this->proxyUrl,
                'headers' => [
                    'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                ],
            ]);
        }
        
        // Collecter les réponses avec contrôle de concurrence
        $activeRequests = 0;
        $index = 0;
        $keys = array_keys($responses);
        
        foreach ($responses as $key => $response) {
            try {
                $statusCode = $response->getStatusCode();
                $content = $response->getContent();
                
                $results[$key] = [
                    'success' => true,
                    'status_code' => $statusCode,
                    'body' => $content,
                ];
            } catch (TransportException $e) {
                $results[$key] = [
                    'success' => false,
                    'error' => $e->getMessage(),
                ];
            }
        }
        
        return $results;
    }
}

// Utilisation
$client = new SymfonyProxyClient('user-country-FR', 'your-password');

// Requête simple
$result = $client->fetch('https://httpbin.org/ip');
print_r($result);

// Requêtes concurrentes
$urls = [
    'ip1' => 'https://httpbin.org/ip',
    'ua' => 'https://httpbin.org/user-agent',
    'headers' => 'https://httpbin.org/headers',
];

$results = $client->fetchConcurrent($urls);
print_r($results);

Symfony HTTP Client offre également des fonctionnalités de streaming utiles pour les réponses volumineuses.

Intégration Laravel : Service Wrapper pour Pool de Proxies

Dans un projet Laravel, vous voulez encapsuler la logique de proxy dans un service réutilisable, utilisable depuis les controllers, les jobs, ou les commands. Voici une implémentation complète :

Configuration

Dans config/services.php, ajoutez :

// config/services.php

return [
    // ... autres services
    
    'proxy' => [
        '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'),
        'default_country' => env('PROXY_DEFAULT_COUNTRY', 'US'),
    ],
];

Service Class

<?php

namespace App\Services;

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

class ResidentialProxyService
{
    private Client $client;
    private string $host;
    private int $port;
    private string $baseUsername;
    private string $password;
    private string $defaultCountry;
    
    // Pool de pays disponibles pour rotation
    private array $countryPool = ['US', 'FR', 'DE', 'GB', 'CA', 'AU', 'JP', 'BR'];
    
    public function __construct()
    {
        $config = config('services.proxy');
        
        $this->host = $config['host'];
        $this->port = $config['http_port'];
        $this->baseUsername = $config['username'];
        $this->password = $config['password'];
        $this->defaultCountry = $config['default_country'];
        
        $this->client = new Client([
            'timeout' => config('services.proxy.timeout', 30),
            'connect_timeout' => 10,
            'verify' => true,
            'headers' => $this->getDefaultHeaders(),
        ]);
    }
    
    private function getDefaultHeaders(): array
    {
        $userAgents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        ];
        
        return [
            'User-Agent' => $userAgents[array_rand($userAgents)],
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language' => 'fr-FR,fr;q=0.9,en;q=0.8',
            'Accept-Encoding' => 'gzip, deflate, br',
            'DNT' => '1',
            'Connection' => 'keep-alive',
        ];
    }
    
    private function buildProxyUrl(?string $country = null, ?string $sessionId = null): string
    {
        $username = $this->baseUsername;
        
        // Ajouter le pays
        $country = $country ?? $this->defaultCountry;
        $username .= "-country-$country";
        
        // Ajouter une session sticky si spécifiée
        if ($sessionId) {
            $username .= "-session-$sessionId";
        }
        
        return "http://{$username}:{$this->password}@{$this->host}:{$this->port}";
    }
    
    /**
     * Effectuer une requête GET via proxy
     */
    public function get(string $url, array $options = []): array
    {
        $country = $options['country'] ?? null;
        $sessionId = $options['session'] ?? null;
        
        $proxyUrl = $this->buildProxyUrl($country, $sessionId);
        
        $requestOptions = array_merge([
            'proxy' => $proxyUrl,
            'headers' => $this->getDefaultHeaders(),
        ], $options);
        
        unset($requestOptions['country'], $requestOptions['session']);
        
        $startTime = microtime(true);
        
        try {
            $response = $this->client->get($url, $requestOptions);
            
            $duration = round((microtime(true) - $startTime) * 1000, 2);
            
            Log::info('Proxy request successful', [
                'url' => $url,
                'country' => $country ?? $this->defaultCountry,
                'duration_ms' => $duration,
                'status' => $response->getStatusCode(),
            ]);
            
            return [
                'success' => true,
                'status_code' => $response->getStatusCode(),
                'body' => (string) $response->getBody(),
                'headers' => $response->getHeaders(),
                'duration_ms' => $duration,
            ];
        } catch (RequestException $e) {
            $duration = round((microtime(true) - $startTime) * 1000, 2);
            $response = $e->getResponse();
            
            Log::error('Proxy request failed', [
                'url' => $url,
                'error' => $e->getMessage(),
                'status' => $response ? $response->getStatusCode() : null,
                'duration_ms' => $duration,
            ]);
            
            return [
                'success' => false,
                'error' => $e->getMessage(),
                'status_code' => $response ? $response->getStatusCode() : null,
                'duration_ms' => $duration,
            ];
        }
    }
    
    /**
     * Requête POST via proxy
     */
    public function post(string $url, array $data, array $options = []): array
    {
        $country = $options['country'] ?? null;
        $sessionId = $options['session'] ?? null;
        
        $proxyUrl = $this->buildProxyUrl($country, $sessionId);
        
        $requestOptions = array_merge([
            'proxy' => $proxyUrl,
            'headers' => $this->getDefaultHeaders(),
            'form_params' => $data,
        ], $options);
        
        unset($requestOptions['country'], $requestOptions['session']);
        
        try {
            $response = $this->client->post($url, $requestOptions);
            
            return [
                'success' => true,
                'status_code' => $response->getStatusCode(),
                'body' => (string) $response->getBody(),
            ];
        } catch (RequestException $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    /**
     * Rotation automatique entre plusieurs pays
     */
    public function fetchWithRotation(array $urls, ?array $countries = null): array
    {
        $countries = $countries ?? $this->countryPool;
        $results = [];
        
        foreach ($urls as $index => $url) {
            $country = $countries[$index % count($countries)];
            $sessionId = uniqid('sess_', true);
            
            $results[$url] = $this->get($url, [
                'country' => $country,
                'session' => $sessionId,
            ]);
            
            // Petit délai entre les requêtes pour éviter le rate limiting
            usleep(500000); // 500ms
        }
        
        return $results;
    }
    
    /**
     * Récupérer l'IP actuelle du proxy
     */
    public function getCurrentIp(?string $country = null): ?string
    {
        $result = $this->get('https://httpbin.org/ip', ['country' => $country]);
        
        if ($result['success']) {
            $data = json_decode($result['body'], true);
            return $data['origin'] ?? null;
        }
        
        return null;
    }
}

Utilisation dans un Job Laravel

<?php

namespace App\Jobs;

use App\Services\ResidentialProxyService;
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 ScrapeWebPageJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public int $tries = 3;
    public int $backoff = 60;
    
    private string $url;
    private ?string $country;
    
    public function __construct(string $url, ?string $country = null)
    {
        $this->url = $url;
        $this->country = $country;
    }
    
    public function handle(ResidentialProxyService $proxyService): void
    {
        Log::info("Starting scrape for: {$this->url}");
        
        $result = $proxyService->get($this->url, [
            'country' => $this->country,
            'session' => 'job-' . $this->job->getJobId(),
        ]);
        
        if (!$result['success']) {
            Log::error("Scrape failed: {$result['error']}");
            
            // Retry avec un autre pays si spécifié
            if ($this->attempts() < $this->tries) {
                $this->release(30);
                return;
            }
            
            throw new \Exception("Failed after {$this->tries} attempts: {$result['error']}");
        }
        
        // Traiter le contenu scrapé
        $this->processContent($result['body']);
        
        Log::info("Scrape completed in {$result['duration_ms']}ms");
    }
    
    private function processContent(string $html): void
    {
        // Logique de traitement du contenu
        // Par exemple: parser le HTML, extraire des données, sauvegarder en DB
        
        // Exemple avec DOMDocument
        $dom = new \DOMDocument();
        @$dom->loadHTML($html);
        
        $xpath = new \DOMXPath($dom);
        $titles = $xpath->query('//h1');
        
        foreach ($titles as $title) {
            Log::info("Found title: " . $title->textContent);
        }
    }
}

// Dispatch du job
ScrapeWebPageJob::dispatch('https://example.com/page-to-scrape', 'FR');

multi_curl : Requêtes Concurrentes Haute Performance

Pour des scénarios nécessitant un débit maximal, curl_multi_* permet d'exécuter plusieurs requêtes cURL en parallèle. C'est particulièrement utile pour le PHP proxy scraping à grande échelle.

<?php

class ConcurrentProxyFetcher
{
    private string $proxyHost;
    private int $proxyPort;
    private string $username;
    private string $password;
    private int $maxConcurrent;
    
    public function __construct(
        string $username,
        string $password,
        int $maxConcurrent = 10,
        string $host = 'gate.proxyhat.com',
        int $port = 8080
    ) {
        $this->proxyHost = $host;
        $this->proxyPort = $port;
        $this->username = $username;
        $this->password = $password;
        $this->maxConcurrent = $maxConcurrent;
    }
    
    private function createHandle(string $url, ?string $country = null): array
    {
        $ch = curl_init();
        
        // Générer un username avec rotation de pays
        $username = $this->username;
        if ($country) {
            $username .= "-country-$country";
        }
        $username .= "-session-" . uniqid();
        
        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 => "$username:{$this->password}",
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            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: fr-FR,fr;q=0.9,en;q=0.8',
            ],
        ]);
        
        return [
            'handle' => $ch,
            'url' => $url,
            'country' => $country,
        ];
    }
    
    public function fetchAll(array $urls, array $countries = ['US', 'FR', 'DE', 'GB']): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];
        
        // Créer et ajouter les handles
        foreach ($urls as $index => $url) {
            $country = $countries[$index % count($countries)];
            $handleData = $this->createHandle($url, $country);
            $handles[] = $handleData;
            curl_multi_add_handle($mh, $handleData['handle']);
            $results[$url] = null;
        }
        
        // Exécuter les requêtes
        $active = null;
        do {
            $status = curl_multi_exec($mh, $active);
            
            if ($status === CURLM_OK) {
                // Attendre qu'un handle soit prêt
                curl_multi_select($mh, 1.0);
            }
        } while ($status === CURLM_OK && $active > 0);
        
        // Collecter les résultats
        foreach ($handles as $handleData) {
            $ch = $handleData['handle'];
            $url = $handleData['url'];
            
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            $totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
            
            if ($error) {
                $results[$url] = [
                    'success' => false,
                    'error' => $error,
                    'http_code' => $httpCode,
                ];
            } else {
                $content = curl_multi_getcontent($ch);
                $results[$url] = [
                    'success' => true,
                    'http_code' => $httpCode,
                    'body' => $content,
                    'total_time' => $totalTime,
                    'country' => $handleData['country'],
                ];
            }
            
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }
        
        curl_multi_close($mh);
        
        return $results;
    }
    
    public function fetchBatches(array $urls, int $batchSize = null, array $countries = ['US', 'FR', 'DE', 'GB']): array
    {
        $batchSize = $batchSize ?? $this->maxConcurrent;
        $batches = array_chunk($urls, $batchSize);
        $allResults = [];
        
        foreach ($batches as $batchIndex => $batch) {
            echo "Processing batch " . ($batchIndex + 1) . "/" . count($batches) . "\n";
            
            $batchResults = $this->fetchAll($batch, $countries);
            $allResults = array_merge($allResults, $batchResults);
            
            // Délai entre les batches pour éviter le rate limiting
            if ($batchIndex < count($batches) - 1) {
                usleep(1000000); // 1 seconde
            }
        }
        
        return $allResults;
    }
}

// Utilisation
$fetcher = new ConcurrentProxyFetcher('user', 'your-password', maxConcurrent: 5);

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

$results = $fetcher->fetchAll($urls);

foreach ($results as $url => $result) {
    echo "URL: $url\n";
    if ($result['success']) {
        echo "  Status: {$result['http_code']}\n";
        echo "  Time: {$result['total_time']}s\n";
        echo "  Country: {$result['country']}\n";
    } else {
        echo "  Error: {$result['error']}\n";
    }
}

TLS/SSL et Gestion du CA Bundle

La configuration TLS/SSL est critique pour la sécurité et la fiabilité. Une configuration incorrecte peut entraîner des erreurs de connexion ou des failles de sécurité.

Configuration du CA Bundle

<?php

class SecureProxyClient
{
    private string $caBundlePath;
    private string $proxyHost;
    private int $proxyPort;
    private string $proxyUser;
    private string $proxyPass;
    
    public function __construct(
        string $proxyUser,
        string $proxyPass,
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080
    ) {
        $this->proxyUser = $proxyUser;
        $this->proxyPass = $proxyPass;
        $this->proxyHost = $proxyHost;
        $this->proxyPort = $proxyPort;
        
        // Détection du CA bundle selon l'environnement
        $this->caBundlePath = $this->detectCaBundle();
    }
    
    private function detectCaBundle(): string
    {
        // Ordre de priorité pour trouver le CA bundle
        $candidates = [
            // Certificat personnalisé
            env('SSL_CA_BUNDLE'),
            // Composer CA bundle (si installé)
            base_path('vendor/cacert.pem'),
            base_path('storage/certs/cacert.pem'),
            // Chemins Linux courants
            '/etc/ssl/certs/ca-certificates.crt',
            '/etc/ssl/certs/ca-bundle.crt',
            '/etc/pki/tls/certs/ca-bundle.crt',
            // Chemins macOS
            '/usr/local/etc/openssl/cert.pem',
            '/usr/local/etc/openssl@1.1/cert.pem',
            // Chemin Windows (XAMPP, WampServer)
            'C:\xampp\apache\conf\extra\cacert.pem',
            'C:\wamp\bin\php\php7.4.0\extras\ssl\cacert.pem',
        ];
        
        foreach ($candidates as $path) {
            if ($path && file_exists($path)) {
                return $path;
            }
        }
        
        // Fallback: télécharger le CA bundle Mozilla
        return $this->downloadCaBundle();
    }
    
    private function downloadCaBundle(): string
    {
        $storagePath = sys_get_temp_dir() . '/cacert-proxy.pem';
        
        if (!file_exists($storagePath) || filemtime($storagePath) < strtotime('-30 days')) {
            $caContent = file_get_contents('https://curl.se/ca/cacert.pem');
            if ($caContent === false) {
                throw new RuntimeException('Impossible de télécharger le CA bundle');
            }
            file_put_contents($storagePath, $caContent);
        }
        
        return $storagePath;
    }
    
    public function fetch(string $url, array $options = []): array
    {
        $ch = curl_init();
        
        // Configuration SSL stricte
        $sslOptions = [
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CAINFO => $this->caBundlePath,
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
            CURLOPT_SSLCERTTYPE => 'PEM',
        ];
        
        // Configuration du proxy
        $proxyOptions = [
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$this->proxyUser}:{$this->proxyPass}",
            CURLOPT_HTTPPROXYTUNNEL => true,
        ];
        
        // Options de base
        $baseOptions = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => $options['timeout'] ?? 30,
            CURLOPT_CONNECTTIMEOUT => $options['connect_timeout'] ?? 10,
            CURLOPT_USERAGENT => $options['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        ];
        
        curl_setopt_array($ch, $sslOptions + $proxyOptions + $baseOptions);
        
        // Headers personnalisés
        if (!empty($options['headers'])) {
            $headers = [];
            foreach ($options['headers'] as $key => $value) {
                $headers[] = "$key: $value";
            }
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        }
        
        // Debug info (à désactiver en production)
        if ($options['debug'] ?? false) {
            curl_setopt($ch, CURLOPT_VERBOSE, true);
            curl_setopt($ch, CURLOPT_STDERR, fopen('php://stderr', 'w'));
        }
        
        $response = curl_exec($ch);
        
        $info = [
            'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
            'total_time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
            'size_download' => curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD),
            'ssl_verify_result' => curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT),
            'primary_ip' => curl_getinfo($ch, CURLINFO_PRIMARY_IP),
        ];
        
        if (curl_errno($ch)) {
            $error = [
                'code' => curl_errno($ch),
                'message' => curl_error($ch),
            ];
            curl_close($ch);
            
            return [
                'success' => false,
                'error' => $error,
                'info' => $info,
            ];
        }
        
        curl_close($ch);
        
        return [
            'success' => true,
            'body' => $response,
            'info' => $info,
        ];
    }
    
    /**
     * Vérifier la configuration SSL
     */
    public function testSslConfiguration(): array
    {
        $testUrls = [
            'https://httpbin.org/get',
            'https://www.google.com',
            'https://api.ipify.org?format=json',
        ];
        
        $results = [];
        
        foreach ($testUrls as $url) {
            $result = $this->fetch($url, ['debug' => true]);
            $results[$url] = [
                'success' => $result['success'],
                'ssl_verify' => $result['info']['ssl_verify_result'] ?? null,
                'http_code' => $result['info']['http_code'] ?? null,
            ];
        }
        
        return $results;
    }
}

// Utilisation
$client = new SecureProxyClient('user-country-US', 'your-password');

// Test de la configuration SSL
$sslTest = $client->testSslConfiguration();
print_r($sslTest);

// Requête sécurisée
$result = $client->fetch('https://httpbin.org/ip');
if ($result['success']) {
    echo "Response received in {$result['info']['total_time']}s\n";
    echo $result['body'];
}

Comparatif des Approches

Méthode Complexité Performance Cas d'Usage
cURL natif Moyenne Excellente Scripts simples, contrôle total
Guzzle Faible Bonne Projets modernes, API clients
Symfony HTTP Client Faible Excellente Applications Symfony, async
multi_curl Élevée Maximale Scraping haute performance
Laravel Service Faible Bonne Intégration Laravel, Jobs

Bonnes Pratiques pour le Proxy PHP

Gestion des Erreurs et Retry

<?php

class RobustProxyClient
{
    private int $maxRetries;
    private array $retryDelays = [1000, 3000, 5000, 10000]; // ms
    
    public function __construct(int $maxRetries = 3)
    {
        $this->maxRetries = $maxRetries;
    }
    
    public function fetchWithRetry(string $url, callable $fetchFn): array
    {
        $lastException = null;
        
        for ($attempt = 0; $attempt < $this->maxRetries; $attempt++) {
            try {
                $result = $fetchFn($url);
                
                // Vérifier les codes HTTP d'erreur serveur
                if (isset($result['status_code']) && $result['status_code'] >= 500) {
                    throw new RuntimeException("Server error: {$result['status_code']}");
                }
                
                return $result;
            } catch (RuntimeException $e) {
                $lastException = $e;
                
                // Backoff exponentiel
                $delay = $this->retryDelays[$attempt] ?? 10000;
                usleep($delay * 1000);
            }
        }
        
        throw $lastException;
    }
}

Rate Limiting

Implémentez toujours un rate limiting pour éviter de saturer les serveurs cibles :

<?php

class RateLimitedProxyClient
{
    private float $minInterval;
    private ?float $lastRequestTime = null;
    
    public function __construct(float $requestsPerSecond = 2.0)
    {
        $this->minInterval = 1.0 / $requestsPerSecond;
    }
    
    private function waitForRateLimit(): void
    {
        if ($this->lastRequestTime === null) {
            return;
        }
        
        $elapsed = microtime(true) - $this->lastRequestTime;
        $remaining = $this->minInterval - $elapsed;
        
        if ($remaining > 0) {
            usleep((int)($remaining * 1000000));
        }
    }
    
    public function fetch(string $url, callable $fetchFn): mixed
    {
        $this->waitForRateLimit();
        
        $result = $fetchFn($url);
        $this->lastRequestTime = microtime(true);
        
        return $result;
    }
}

Conseil : Pour le scraping sérieux, combinez rotation d'IP, rate limiting, et gestion des retry. Un proxy résidentiel de qualité comme ProxyHat réduit les risques de blocage, mais une bonne architecture de code reste essentielle.

Points Clés à Retenir

  • cURL natif offre un contrôle maximal mais demande plus de code. Utilisez CURLOPT_PROXY, CURLOPT_PROXYUSERPWD et configurez correctement SSL.
  • Guzzle simplifie l'implémentation avec des options de proxy par requête. Idéal pour la plupart des projets PHP modernes.
  • Symfony HTTP Client excelle pour les requêtes asynchrones et s'intègre parfaitement dans l'écosystème Symfony.
  • multi_curl est le choix optimal pour le scraping haute performance avec des centaines de requêtes concurrentes.
  • Laravel Service encapsule la logique proxy dans une classe réutilisable, utilisable depuis les jobs, commands et controllers.
  • Toujours configurer TLS/SSL correctement avec un CA bundle à jour pour éviter les erreurs de certificat.
  • Implémentez retry avec backoff exponentiel et rate limiting pour une production fiable.

Pour vos projets de Laravel proxy scraping ou d'intégration d'API tierces, ProxyHat offre des proxies résidentiels fiables avec rotation automatique et géo-ciblage. Consultez notre page de tarification pour les options disponibles.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog