Introducción: ¿Por qué usar proxies en PHP?
Cuando desarrollas aplicaciones PHP que realizan scraping, consumen APIs de terceros o automatizan tareas, inevitablemente te enfrentarás a bloqueos de IP, rate limits y restricciones geográficas. Los proxies HTTP resuelven estos problemas actuando como intermediarios entre tu aplicación y los servidores destino.
Como desarrollador PHP, tienes múltiples herramientas a tu disposición: desde la extensión cURL nativa hasta clientes HTTP modernos como Guzzle y Symfony HTTP Client. Esta guía te mostrará cómo implementar proxies en cada uno de estos escenarios, con código listo para producción.
Los casos de uso más comunes incluyen:
- Web scraping: distribuir solicitudes entre múltiples IPs para evitar bloqueos
- Integraciones de APIs: mantener sesiones estables con IPs confiables
- Testing geolocalizado: simular usuarios desde diferentes países o ciudades
- Automatización: ejecutar jobs en cola con rotación automática de proxies
cURL nativo: configuración básica de proxy
La extensión cURL de PHP es la forma más directa de realizar solicitudes HTTP con soporte para proxies. Los parámetros clave son CURLOPT_PROXY para el hostname y CURLOPT_PROXYUSERPWD para las credenciales de autenticación.
Veamos un ejemplo completo con manejo de errores y timeouts:
<?php
function fetchWithProxy(string $url, string $username, string $password): ?string
{
$ch = curl_init();
// Configuración del proxy ProxyHat
$proxyHost = 'gate.proxyhat.com';
$proxyPort = 8080;
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $proxyHost,
CURLOPT_PROXYPORT => $proxyPort,
CURLOPT_PROXYUSERPWD => "{$username}:{$password}",
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
// Opciones de respuesta
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
// Timeouts críticos para producción
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 30,
// TLS/SSL
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
// Headers para parecer un navegador real
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',
'Accept-Language: es-ES,es;q=0.8,en;q=0.5',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($error) {
error_log("cURL Error [{$errno}]: {$error}");
return null;
}
if ($httpCode >= 400) {
error_log("HTTP Error: {$httpCode} para URL {$url}");
return null;
}
return $response;
}
// Uso con proxy residencial de ProxyHat
$username = 'user-country-US-session-abc123';
$password = 'tu_password';
$html = fetchWithProxy('https://httpbin.org/ip', $username, $password);
if ($html) {
echo "Respuesta recibida: " . $html . PHP_EOL;
}
Este patrón es ideal para scripts CLI, tareas cron o integraciones simples. La función devuelve null en caso de error, permitiendo reintentos con diferentes proxies.
Geo-targeting con cURL
Para simular solicitudes desde países específicos, incluye el código de país en el nombre de usuario:
<?php
function createGeoProxyUrl(string $country, string $city = null, string $session = null): array
{
$username = 'user';
if ($country) {
$username .= "-country-{$country}";
}
if ($city) {
$username .= "-city-{$city}";
}
if ($session) {
$username .= "-session-{$session}";
}
return [
'host' => 'gate.proxyhat.com',
'port' => 8080,
'user' => $username,
'pass' => 'tu_password',
];
}
// Solicitar desde Berlín, Alemania
$proxy = createGeoProxyUrl('DE', 'berlin', 'session-xyz789');
echo "Proxy configurado para: {$proxy['user']}@{$proxy['host']}:{$proxy['port']}" . PHP_EOL;
Guzzle: cliente HTTP moderno con soporte de proxy
Guzzle es el cliente HTTP más popular en el ecosistema PHP. Ofrece una API limpia, manejo de promesas y middleware para casos avanzados. La configuración de proxy se pasa en el array de opciones de cada solicitud.
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
class ProxyScraper
{
private Client $client;
private array $proxyConfig;
private int $retryCount = 3;
public function __construct(string $username, string $password)
{
$this->proxyConfig = [
'proxy' => [
'http' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
'https' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
],
'timeout' => 30,
'connect_timeout' => 15,
'verify' => '/etc/ssl/certs/ca-certificates.crt',
'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',
],
];
$this->client = new Client($this->proxyConfig);
}
public function fetch(string $url, array $options = []): ?string
{
$attempts = 0;
$lastException = null;
while ($attempts < $this->retryCount) {
try {
$response = $this->client->get($url, $options);
return (string) $response->getBody();
} catch (RequestException $e) {
$lastException = $e;
$attempts++;
// Rotar proxy en cada reintento
$newSession = bin2hex(random_bytes(8));
$this->rotateProxy($newSession);
usleep(500000 * $attempts); // Backoff exponencial
}
}
error_log("Guzzle error después de {$attempts} intentos: " . $lastException?->getMessage());
return null;
}
private function rotateProxy(string $sessionId): void
{
// Reconstruir cliente con nueva sesión
$username = "user-country-US-session-{$sessionId}";
$this->proxyConfig['proxy'] = [
'http' => "http://{$username}:tu_password@gate.proxyhat.com:8080",
'https' => "http://{$username}:tu_password@gate.proxyhat.com:8080",
];
$this->client = new Client($this->proxyConfig);
}
public function fetchJson(string $url): ?array
{
$body = $this->fetch($url, [
'headers' => ['Accept' => 'application/json'],
]);
return $body ? json_decode($body, true) : null;
}
}
// Uso del scraper
$scraper = new ProxyScraper('user-country-ES', 'tu_password');
// Obtener IP visible
$ipData = $scraper->fetchJson('https://httpbin.org/ip');
if ($ipData) {
echo "IP visible: " . $ipData['origin'] . PHP_EOL;
}
// Scraping de página HTML
$html = $scraper->fetch('https://example.com/product/123');
if ($html) {
// Procesar con DOMDocument o librería similar
$dom = new DOMDocument();
@$dom->loadHTML($html);
echo "Página cargada correctamente" . PHP_EOL;
}
Middleware para logging y métricas
<?php
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\MessageFormatter;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
function createLoggedClient(array $proxyConfig): Client
{
$logger = new Logger('proxy-requests');
$logger->pushHandler(new StreamHandler('logs/proxy.log', Logger::DEBUG));
$stack = HandlerStack::create();
// Middleware de logging
$stack->push(
Middleware::log(
$logger,
new MessageFormatter('{method} {uri} - {code} - {req_header_User-Agent}')
)
);
// Middleware para métricas de tiempo
$stack->push(Middleware::timer());
return new Client(array_merge($proxyConfig, [
'handler' => $stack,
'on_stats' => function (GuzzleHttp\TransferStats $stats) {
error_log(sprintf(
'Tiempo total: %.2fs - DNS: %.2fs - Connect: %.2fs',
$stats->getTransferTime(),
$stats->getHandlerStat('namelookup_time'),
$stats->getHandlerStat('connect_time')
));
},
]));
}
Symfony HTTP Client: asíncrono y alto rendimiento
Symfony HTTP Client ofrece soporte nativo para solicitudes asíncronas y concurrentes, ideal para scraping a escala. Su API de responses asíncronas permite procesar múltiples solicitudes en paralelo sin bloquear.
<?php
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class AsyncProxyClient
{
private $client;
private string $proxyUrl;
public function __construct(string $username, string $password)
{
$this->proxyUrl = "http://{$username}:{$password}@gate.proxyhat.com:8080";
$this->client = HttpClient::create([
'proxy' => $this->proxyUrl,
'timeout' => 30,
'max_redirects' => 5,
'verify_peer' => true,
'verify_host' => true,
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
}
public function fetchConcurrent(array $urls, callable $onSuccess, callable $onError): void
{
$responses = [];
// Iniciar todas las solicitudes sin bloquear
foreach ($urls as $url) {
try {
$responses[$url] = $this->client->request('GET', $url);
} catch (TransportExceptionInterface $e) {
$onError($url, $e->getMessage());
}
}
// Procesar respuestas a medida que llegan
foreach ($responses as $url => $response) {
try {
$statusCode = $response->getStatusCode();
if ($statusCode >= 200 && $statusCode < 300) {
$content = $response->getContent();
$onSuccess($url, $content);
} else {
$onError($url, "HTTP {$statusCode}");
}
} catch (TransportExceptionInterface $e) {
$onError($url, $e->getMessage());
}
}
}
public function fetchWithRetry(string $url, int $maxRetries = 3): ?string
{
$attempt = 0;
while ($attempt < $maxRetries) {
try {
$response = $this->client->request('GET', $url);
if ($response->getStatusCode() === 200) {
return $response->getContent();
}
} catch (TransportExceptionInterface $e) {
$attempt++;
usleep(1000000 * $attempt); // Backoff
}
}
return null;
}
}
// Uso con procesamiento concurrente
$asyncClient = new AsyncProxyClient('user-country-FR', 'tu_password');
$urls = [
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/user-agent',
];
$asyncClient->fetchConcurrent(
$urls,
function (string $url, string $content) {
echo "✓ {$url} - " . strlen($content) . " bytes" . PHP_EOL;
},
function (string $url, string $error) {
echo "✗ {$url} - Error: {$error}" . PHP_EOL;
}
);
Laravel: servicio de proxy para jobs en cola
En aplicaciones Laravel, es común ejecutar scraping como jobs en cola. Un servicio dedicado encapsula la lógica del proxy y permite inyección de dependencias.
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class ResidentialProxyService
{
private Client $client;
private string $baseUsername;
private string $password;
private string $gateway = 'gate.proxyhat.com';
private int $port = 8080;
// Pool de países disponibles
private array $countryPool = ['US', 'ES', 'DE', 'FR', 'GB', 'IT'];
public function __construct()
{
$this->baseUsername = config('proxyhat.username', 'user');
$this->password = config('proxyhat.password', '');
$this->client = new Client([
'timeout' => config('proxyhat.timeout', 30),
'connect_timeout' => config('proxyhat.connect_timeout', 15),
'verify' => config('proxyhat.ca_bundle', '/etc/ssl/certs/ca-certificates.crt'),
]);
}
/**
* Construye URL de proxy con opciones de geo-targeting
*/
private function buildProxyUrl(?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->gateway}:{$this->port}";
}
/**
* Obtiene una sesión sticky (misma IP para múltiples requests)
*/
public function getStickySession(?string $country = null): array
{
$sessionId = bin2hex(random_bytes(8));
$proxyUrl = $this->buildProxyUrl($country, $sessionId);
return [
'session_id' => $sessionId,
'proxy_url' => $proxyUrl,
'expires_at' => now()->addMinutes(30),
];
}
/**
* Realiza solicitud con rotación automática en caso de error
*/
public function request(string $method, string $url, array $options = [], int $maxRetries = 3): ?string
{
$attempt = 0;
$lastError = null;
while ($attempt < $maxRetries) {
$country = $this->countryPool[array_rand($this->countryPool)];
$proxyUrl = $this->buildProxyUrl($country);
try {
$response = $this->client->request($method, $url, array_merge($options, [
'proxy' => $proxyUrl,
'headers' => array_merge([
'User-Agent' => config('proxyhat.user_agent', 'ProxyHat-Scraper/1.0'),
], $options['headers'] ?? []),
]));
Log::info('Proxy request successful', [
'url' => $url,
'country' => $country,
'status' => $response->getStatusCode(),
]);
return (string) $response->getBody();
} catch (RequestException $e) {
$lastError = $e->getMessage();
$attempt++;
Log::warning('Proxy request failed, retrying', [
'url' => $url,
'attempt' => $attempt,
'error' => $lastError,
]);
usleep(500000 * $attempt);
}
}
Log::error('Proxy request failed after retries', [
'url' => $url,
'attempts' => $attempt,
'error' => $lastError,
]);
return null;
}
/**
* Verifica disponibilidad del proxy
*/
public function healthCheck(): bool
{
try {
$response = $this->client->get('https://httpbin.org/status/200', [
'proxy' => $this->buildProxyUrl(),
'timeout' => 10,
]);
return $response->getStatusCode() === 200;
} catch (\Exception $e) {
return false;
}
}
}
Ahora el Job que utiliza este servicio:
<?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 ScrapeProductJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
private string $productUrl;
private ?string $country;
public function __construct(string $productUrl, ?string $country = null)
{
$this->productUrl = $productUrl;
$this->country = $country;
}
public function handle(ResidentialProxyService $proxyService): void
{
// Obtener sesión sticky para mantener IP consistente
$session = $proxyService->getStickySession($this->country);
Log::info('Starting scrape job', [
'url' => $this->productUrl,
'session' => $session['session_id'],
]);
// Primera solicitud: obtener página del producto
$html = $proxyService->request('GET', $this->productUrl, [
'proxy' => $session['proxy_url'],
]);
if (!$html) {
$this->fail(new \Exception('Failed to fetch product page'));
return;
}
// Parsear HTML y extraer datos
$productData = $this->parseProduct($html);
// Segunda solicitud: verificar stock (usando misma sesión)
if (isset($productData['stockUrl'])) {
$stockData = $proxyService->request('GET', $productData['stockUrl'], [
'proxy' => $session['proxy_url'],
]);
$productData['stock'] = $this->parseStock($stockData);
}
// Guardar o procesar datos
$this->processProductData($productData);
Log::info('Scrape job completed', [
'product' => $productData['name'] ?? 'unknown',
]);
}
private function parseProduct(string $html): array
{
// Implementar parsing según estructura del sitio
$dom = new \DOMDocument();
@$dom->loadHTML($html);
$xpath = new \DOMXPath($dom);
return [
'name' => $xpath->query('//h1[@class="product-title"]')?->item(0)?->textContent,
'price' => $xpath->query('//span[@class="price"]')?->item(0)?->textContent,
'stockUrl' => $xpath->query('//a[@data-stock]')?->item(0)?->getAttribute('href'),
];
}
private function parseStock(?string $json): array
{
if (!$json) return [];
$data = json_decode($json, true);
return [
'available' => $data['available'] ?? false,
'quantity' => $data['quantity'] ?? 0,
];
}
private function processProductData(array $data): void
{
// Guardar en base de datos o enviar a otro servicio
// \App\Models\Product::updateOrCreate(['url' => $this->productUrl], $data);
}
}
// Despachar el job
ScrapeProductJob::dispatch('https://example.com/product/123', 'US')
->onQueue('scraping');
multi_curl: concurrencia máxima para scraping
Cuando necesitas realizar cientos de solicitudes simultáneas, curl_multi_* ofrece el mejor rendimiento. Este enfoque gestiona múltiples handles cURL en paralelo sin bloquear.
<?php
class ConcurrentProxyScraper
{
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
private string $username;
private string $password;
private int $maxConnections = 50;
private int $timeout = 30;
public function __construct(string $username, string $password)
{
$this->username = $username;
$this->password = $password;
}
/**
* Fetch múltiple URLs concurrentemente
*/
public function fetchMultiple(array $urls, ?callable $processor = null): array
{
$results = [];
$handles = [];
$mh = curl_multi_init();
// Crear handles para cada URL (hasta maxConnections)
$chunks = array_chunk($urls, $this->maxConnections);
foreach ($chunks as $chunk) {
$results = array_merge($results, $this->processChunk($chunk, $processor));
}
return $results;
}
private function processChunk(array $urls, ?callable $processor): array
{
$results = [];
$handles = [];
$mh = curl_multi_init();
// Configurar cada handle
foreach ($urls as $i => $url) {
$ch = curl_init();
// Generar sesión única para cada solicitud (rotación)
$session = bin2hex(random_bytes(4));
$userWithSession = "{$this->username}-session-{$session}";
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $this->proxyHost,
CURLOPT_PROXYPORT => $this->proxyPort,
CURLOPT_PROXYUSERPWD => "{$userWithSession}:{$this->password}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
CURLOPT_HTTPHEADER => [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
curl_multi_add_handle($mh, $ch);
$handles[$i] = ['handle' => $ch, 'url' => $url];
}
// Ejecutar todas las solicitudes
$active = null;
do {
$status = curl_multi_exec($mh, $active);
if ($status === CURLM_CALL_MULTI_PERFORM) {
continue;
}
// Esperar actividad
if ($status === CURLM_OK) {
curl_multi_select($mh, 1.0);
}
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
// Recoger resultados
foreach ($handles as $i => $data) {
$ch = $data['handle'];
$url = $data['url'];
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$content = curl_multi_getcontent($ch);
if ($error) {
$results[$url] = [
'success' => false,
'error' => $error,
'code' => $httpCode,
];
} else {
$results[$url] = [
'success' => true,
'content' => $processor ? $processor($content) : $content,
'code' => $httpCode,
];
}
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
}
// Uso para scraping masivo
$scraper = new ConcurrentProxyScraper('user-country-US', 'tu_password');
$urls = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
// ... cientos de URLs
];
$results = $scraper->fetchMultiple($urls, function ($html) {
// Procesar HTML extraído
$dom = new DOMDocument();
@$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
$titles = [];
foreach ($xpath->query('//h2[@class="title"]') as $node) {
$titles[] = trim($node->textContent);
}
return $titles;
});
// Contar éxitos y fallos
$successCount = count(array_filter($results, fn($r) => $r['success']));
echo "Exitosos: {$successCount} / " . count($results) . PHP_EOL;
TLS/SSL y manejo de certificados CA
Las conexiones HTTPS a través de proxies requieren verificación correcta de certificados. Configurar incorrectamente SSL puede exponer tu aplicación a ataques man-in-the-middle.
Configuración correcta de CA bundle
<?php
class TlsProxyClient
{
private string $caBundlePath;
private array $proxyConfig;
public function __construct(array $proxyConfig)
{
$this->proxyConfig = $proxyConfig;
$this->caBundlePath = $this->detectCaBundle();
}
/**
* Detecta el path correcto del CA bundle según el sistema
*/
private function detectCaBundle(): string
{
// Opciones comunes según sistema operativo
$candidates = [
// Linux (Debian/Ubuntu)
'/etc/ssl/certs/ca-certificates.crt',
// Linux (RHEL/CentOS)
'/etc/pki/tls/certs/ca-bundle.crt',
// macOS (Homebrew)
'/usr/local/etc/openssl/cert.pem',
'/usr/local/etc/openssl@1.1/cert.pem',
// Windows (XAMPP)
'C:\xampp\apache\conf\extra\cacert.pem',
// Composer CA bundle (fallback)
__DIR__ . '/../../../composer/ca-bundle/res/cacert.pem',
];
foreach ($candidates as $path) {
if (file_exists($path) && is_readable($path)) {
return $path;
}
}
// Fallback: usar el bundle de Composer
$composerBundle = $this->downloadComposerCaBundle();
return $composerBundle;
}
private function downloadComposerCaBundle(): string
{
$cachePath = sys_get_temp_dir() . '/cacert.pem';
if (!file_exists($cachePath) || filemtime($cachePath) < strtotime('-30 days')) {
$url = 'https://curl.se/ca/cacert.pem';
$content = file_get_contents($url);
if ($content === false) {
throw new RuntimeException('Cannot download CA bundle');
}
file_put_contents($cachePath, $content);
}
return $cachePath;
}
/**
* Verifica la validez del certificado del servidor destino
*/
public function verifyPeer(string $url): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PROXY => $this->proxyConfig['host'],
CURLOPT_PROXYPORT => $this->proxyConfig['port'],
CURLOPT_PROXYUSERPWD => $this->proxyConfig['auth'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $this->caBundlePath,
CURLOPT_CERTINFO => true,
CURLOPT_VERBOSE => true,
]);
$result = curl_exec($ch);
$info = curl_getinfo($ch);
$error = curl_error($ch);
curl_close($ch);
return [
'success' => $result !== false,
'ssl_verify_result' => $info['ssl_verify_result'],
'certinfo' => $info['certinfo'] ?? [],
'error' => $error,
'ca_bundle' => $this->caBundlePath,
];
}
/**
* Cliente HTTP con TLS estricto
*/
public function createSecureClient(): GuzzleHttp\Client
{
return new GuzzleHttp\Client([
'proxy' => $this->buildProxyUrl(),
'timeout' => 30,
'verify' => $this->caBundlePath,
'version' => 2.0, // HTTP/2 si está disponible
'config' => [
'curl' => [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
],
],
]);
}
private function buildProxyUrl(): string
{
return sprintf(
'http://%s:%s@%s:%d',
$this->proxyConfig['username'],
$this->proxyConfig['password'],
$this->proxyConfig['host'],
$this->proxyConfig['port']
);
}
}
// Verificar configuración TLS
$client = new TlsProxyClient([
'host' => 'gate.proxyhat.com',
'port' => 8080,
'username' => 'user-country-US',
'password' => 'tu_password',
'auth' => 'user-country-US:tu_password',
]);
$verification = $client->verifyPeer('https://example.com');
echo "CA Bundle: " . $verification['ca_bundle'] . PHP_EOL;
echo "SSL Verify Result: " . $verification['ssl_verify_result'] . PHP_EOL;
echo "Success: " . ($verification['success'] ? 'Yes' : 'No') . PHP_EOL;
if (!$verification['success']) {
echo "Error: " . $verification['error'] . PHP_EOL;
}
Comparativa de clientes HTTP para proxies
| Característica | cURL nativo | Guzzle | Symfony HTTP Client |
|---|---|---|---|
| Complejidad | Alta | Media | Media |
| Concurrencia | multi_curl (manual) | Promise-based | Nativa asíncrona |
| Integración Laravel | Manual | Excelente | Buena |
| Middleware/Plugins | No | Sí (PSR-15) | Sí |
| Curva de aprendizaje | Baja | Media | Media |
| Mejor uso | Scripts simples | Aplicaciones completas | Scraping concurrente |
Buenas prácticas para producción
Regla de oro: Nunca desactives la verificación SSL (
CURLOPT_SSL_VERIFYPEER = false) en producción. Esto expone tu aplicación a ataques MITM y compromete la seguridad de los datos transmitidos.
- Rotación de sesiones: Genera IDs de sesión únicos para cada solicitud o grupo de solicitudes relacionadas.
- Backoff exponencial: Implementa reintentos con delays crecientes (1s, 2s, 4s...) para evitar sobrecargar el proxy.
- Logging detallado: Registra tiempo de respuesta, código de estado y proxy usado para cada solicitud.
- Circuit breaker: Si un proxy falla repetidamente, márcalo como no disponible temporalmente.
- Rate limiting: Respeta los límites del sitio destino con delays entre solicitudes.
Puntos clave
- cURL nativo es ideal para scripts simples y CLI, con control total sobre cada parámetro del proxy.
- Guzzle ofrece la mejor integración con frameworks modernos y middleware para logging/métricas.
- Symfony HTTP Client destaca en escenarios de alta concurrencia con su modelo asíncrono nativo.
- Laravel Jobs permiten encapsular la lógica de scraping en servicios reutilizables con reintentos automáticos.
- multi_curl es la opción de máximo rendimiento para scraping masivo con cientos de solicitudes paralelas.
- TLS/SSL debe configurarse correctamente con un CA bundle actualizado para conexiones seguras.
FAQ
¿Cómo configuro un proxy SOCKS5 en PHP?
Para usar SOCKS5, cambia el puerto a 1080 y añade CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5 en cURL. En Guzzle, usa el esquema socks5:// en la URL del proxy: 'proxy' => 'socks5://user:pass@gate.proxyhat.com:1080'.
¿Cuál es la diferencia entre proxy residencial y datacenter?
Los proxies residenciales usan IPs de dispositivos reales conectados por ISP, siendo más difíciles de detectar. Los datacenter usan IPs de servidores en centros de datos, más rápidos pero más fáciles de bloquear. Para scraping agresivo, residencial es preferible.
¿Cómo mantengo la misma IP entre solicitudes?
Usa el parámetro session-<id> en el nombre de usuario: user-country-US-session-abc123. Todas las solicitudes con el mismo ID de sesión usarán la misma IP mientras la sesión esté activa (típicamente 1-30 minutos).
¿Puedo usar proxies en tests automatizados de Laravel?
Sí, inyecta el servicio de proxy en tus tests con $this->app->make(ResidentialProxyService::class). Puedes crear un mock para tests unitarios y usar proxies reales solo en tests de integración.
¿Cómo manejo CAPTCHAs al hacer scraping?
Los proxies residenciales reducen la frecuencia de CAPTCHAs. Para casos donde aparezcan, necesitas integrar un servicio de resolución de CAPTCHAs. Implementa detección de respuestas CAPTCHA (códigos 403, contenido específico) y reintentos con diferentes IPs.






