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_PROXYUSERPWDet 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.






