PHP geliştiricileri için web scraping, üçüncü parti API entegrasyonları ve otomasyon süreçlerinde proxy kullanımı kritik bir gereklilik haline gelmiştir. Hedef siteler IP tabanlı rate limiting uygular, coğrafi kısıtlamalar getirir veya şüpheli trafiği engeller. Bu rehberde, PHP ekosisteminde HTTP proxy kullanımının her katmanını — raw cURL'den modern framework entegrasyonlarına kadar — pratik kod örnekleriyle ele alacağız.
Neden PHP Projesinde Proxy Kullanmalısınız?
Web scraping veya yoğun API kullanımı yapan PHP uygulamaları, kısa sürede hedef sunucular tarafından engellenebilir. Tek bir IP'den gelen binlerce istek, anormal davranış olarak algılanır ve HTTP 429 (Too Many Requests) veya HTTP 403 (Forbidden) yanıtlarıyla karşılaşırsınız.
Proxy kullanımının temel faydaları:
- IP rotasyonu: Engellenen IP'leri atlayarak kesintisiz erişim
- Coğrafi hedefleme: Ülke/bazlı içerik kısıtlamalarını aşma
- Rate limit yönetimi: İstekleri birden fazla IP'ye dağıtma
- Anonimlik: Gerçek sunucu IP'nizi gizleme
Bu rehberde ProxyHat residential proxy servisini örnek olarak kullanacağız. Gateway adresi: gate.proxyhat.com, HTTP portu: 8080, SOCKS5 portu: 1080.
Raw cURL ile Proxy Kullanımı
PHP'nin yerleşik cURL eklentisi, proxy desteği için en temel ve en hızlı yöntemdir. CURLOPT_PROXY ve CURLOPT_PROXYUSERPWD seçenekleriyle proxy konfigürasyonu yapılır.
<?php
declare(strict_types=1);
/**
* Basit cURL proxy isteği örneği
* ProxyHat residential proxy kullanarak HTTP isteği yapar
*/
function fetchWithProxy(string $url, string $username, string $password): string
{
$ch = curl_init();
// Proxy konfigürasyonu
$proxyHost = 'gate.proxyhat.com';
$proxyPort = 8080;
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// Proxy ayarları
CURLOPT_PROXY => $proxyHost,
CURLOPT_PROXYPORT => $proxyPort,
CURLOPT_PROXYUSERPWD => "$username:$password",
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
// SSL/TLS konfigürasyonu
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt', // Linux yolı
// User-Agent (önemli: bot tespitini zorlaştırır)
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
// Header'ları al
CURLOPT_HEADER => false,
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.5',
'Accept-Encoding: gzip, deflate',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0) {
throw new RuntimeException("cURL Error [$errno]: $error");
}
if ($httpCode >= 400) {
throw new RuntimeException("HTTP Error: $httpCode");
}
return $response;
}
// Kullanım örneği
try {
$html = fetchWithProxy(
'https://httpbin.org/ip',
'user-country-US', // ProxyHat username (geo-targeting)
'your-password' // ProxyHat password
);
echo $html;
} catch (RuntimeException $e) {
error_log("Proxy request failed: " . $e->getMessage());
}
ProxyHat'ta geo-targeting ve sticky session kullanıcı adı içinde belirtilir. Örneğin:
user-country-US:pass— ABD IP'siuser-country-DE-city-berlin:pass— Berlin, Almanya IP'siuser-session-abc123:pass— Sticky session (aynı IP)
cURL ile SOCKS5 Proxy
SOCKS5 proxy kullanımı için CURLOPT_PROXYTYPE değerini değiştirmeniz yeterlidir:
<?php
function fetchWithSocks5(string $url, string $username, string $password): string
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 1080, // SOCKS5 portu
CURLOPT_PROXYUSERPWD => "$username:$password",
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new RuntimeException('SOCKS5 proxy error: ' . curl_error($ch));
}
curl_close($ch);
return $response;
}
Guzzle HTTP Client ile Proxy Konfigürasyonu
Guzzle, PHP'de en yaygın kullanılan HTTP client kütüphanesidir. Laravel ve Symfony projelerinde standart haline gelmiştir. Proxy konfigürasyonu request options üzerinden yapılır.
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
class ProxyScraper
{
private Client $client;
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
public function __construct(
private string $proxyUser,
private string $proxyPass
) {
$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',
],
]);
}
/**
* Tek istek - proxy ile
*/
public function fetch(string $url, ?string $country = null): string
{
$username = $this->buildUsername($country);
try {
$response = $this->client->get($url, [
'proxy' => [
'http' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
'https' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
],
]);
return $response->getBody()->getContents();
} catch (RequestException $e) {
// Retry logic veya fallback
if ($e->hasResponse()) {
$statusCode = $e->getResponse()->getStatusCode();
throw new RuntimeException("HTTP {$statusCode} error for {$url}");
}
throw new RuntimeException("Network error: " . $e->getMessage());
}
}
/**
* Per-request IP rotasyonu ile çoklu istek
*/
public function fetchMultiple(array $urls, ?string $country = null): array
{
$results = [];
$promises = [];
foreach ($urls as $i => $url) {
// Her istek için farklı session ID = farklı IP
$sessionId = 'session-' . uniqid($i, true);
$username = $this->buildUsername($country, $sessionId);
$promises[$i] = $this->client->getAsync($url, [
'proxy' => [
'http' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
'https' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
],
]);
}
// Wait for all promises
$responses = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
foreach ($responses as $i => $result) {
if ($result['state'] === 'fulfilled') {
$results[$urls[$i]] = $result['value']->getBody()->getContents();
} else {
$results[$urls[$i]] = ['error' => $result['reason']->getMessage()];
}
}
return $results;
}
/**
* ProxyHat username builder
* Format: user-country-US-session-abc123
*/
private function buildUsername(?string $country = null, ?string $session = null): string
{
$parts = [$this->proxyUser];
if ($country) {
$parts[] = "country-{$country}";
}
if ($session) {
$parts[] = "session-{$session}";
}
return implode('-', $parts);
}
}
// Kullanım
$scraper = new ProxyScraper('user', 'your-password');
// Tek istek (ABD IP'si)
$html = $scraper->fetch('https://httpbin.org/ip', 'US');
// Çoklu istek (her biri farklı IP)
$results = $scraper->fetchMultiple([
'https://httpbin.org/ip',
'https://httpbin.org/user-agent',
'https://httpbin.org/headers',
], 'US');
Guzzle ile Pool ve Concurrency
Yüksek hacimli scraping işlemlerinde Guzzle Pool kullanarak concurrency kontrolü yapabilirsiniz:
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
class ConcurrentScraper
{
public function __construct(
private Client $client,
private string $proxyUser,
private string $proxyPass
) {}
public function scrapeUrls(array $urls, int $concurrency = 10): array
{
$results = [];
$proxyHost = 'gate.proxyhat.com:8080';
$requests = function () use ($urls, $proxyHost) {
foreach ($urls as $i => $url) {
$sessionId = "pool-session-{$i}";
$proxyAuth = "{$this->buildUsername($sessionId)}:{$this->proxyPass}";
yield new Request('GET', $url, [
'Proxy-Authorization' => 'Basic ' . base64_encode($proxyAuth),
]);
}
};
$pool = new Pool($this->client, $requests(), [
'concurrency' => $concurrency,
'fulfilled' => function ($response, $index) use ($urls, &$results) {
$results[$urls[$index]] = $response->getBody()->getContents();
},
'rejected' => function ($reason, $index) use ($urls, &$results) {
$results[$urls[$index]] = ['error' => $reason->getMessage()];
},
'options' => [
'proxy' => "http://{$this->proxyUser}:{$this->proxyPass}@{$proxyHost}",
'timeout' => 30,
],
]);
$promise = $pool->promise();
$promise->wait();
return $results;
}
private function buildUsername(string $sessionId): string
{
return "{$this->proxyUser}-session-{$sessionId}";
}
}
Symfony HTTP Client ile Proxy ve AsyncResponse
Symfony HTTP Client, Guzzle'a modern bir alternatiftir. Async desteği ve curl wrapper olarak çalışır. Laravel projelerinde de kullanılabilir.
<?php
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
class SymfonyProxyClient
{
private HttpClient $client;
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
public function __construct(
private string $proxyUser,
private string $proxyPass
) {
$this->client = HttpClient::create([
'timeout' => 30,
'max_redirects' => 5,
'verify_peer' => true,
'verify_host' => true,
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
]);
}
/**
* Senkron istek
*/
public function fetch(string $url, array $options = []): string
{
$options = array_merge($this->getDefaultProxyOptions(), $options);
try {
$response = $this->client->request('GET', $url, $options);
$statusCode = $response->getStatusCode();
if ($statusCode >= 400) {
throw new RuntimeException("HTTP {$statusCode} error");
}
return $response->getContent();
} catch (TransportException $e) {
throw new RuntimeException("Transport error: " . $e->getMessage());
}
}
/**
* Asenkron istekler - birden fazla URL aynı anda
*/
public function fetchAsync(array $urls, ?string $country = null): array
{
$responses = [];
$results = [];
// İstekleri başlat
foreach ($urls as $i => $url) {
$sessionId = "async-" . bin2hex(random_bytes(8));
$username = $this->buildUsername($country, $sessionId);
$responses[$i] = $this->client->request('GET', $url, [
'proxy' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
]);
}
// Tüm yanıtları bekle
foreach ($responses as $i => $response) {
try {
$results[$urls[$i]] = $response->getContent();
} catch (TransportException $e) {
$results[$urls[$i]] = ['error' => $e->getMessage()];
}
}
return $results;
}
/**
* Streaming response - büyük dosyalar için
*/
public function fetchStream(string $url, string $outputPath): int
{
$response = $this->client->request('GET', $url, $this->getDefaultProxyOptions());
$handle = fopen($outputPath, 'w');
$bytesWritten = 0;
foreach ($this->client->stream($response) as $chunk) {
$bytesWritten += fwrite($handle, $chunk->getContent());
}
fclose($handle);
return $bytesWritten;
}
private function getDefaultProxyOptions(): array
{
$username = $this->buildUsername();
return [
'proxy' => "http://{$username}:{$this->proxyPass}@{$this->proxyHost}:{$this->proxyPort}",
'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',
],
];
}
private function buildUsername(?string $country = null, ?string $session = null): string
{
$parts = [$this->proxyUser];
if ($country) {
$parts[] = "country-{$country}";
}
if ($session) {
$parts[] = "session-{$session}";
}
return implode('-', $parts);
}
}
// Kullanım
$client = new SymfonyProxyClient('user', 'your-password');
// Tek istek
$content = $client->fetch('https://httpbin.org/ip', ['country' => 'DE']);
// Asenkron çoklu istek
$results = $client->fetchAsync([
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/user-agent',
], 'US');
Laravel Entegrasyonu: Residential Proxy Pool Service
Laravel projelerinde proxy yönetimini merkezi bir service class ile ele almak, queue job'larından ve controller'lardan kolay erişim sağlar. Aşağıdaki örnekte, residential proxy pool'u yöneten bir service sınıfı ve bunu job içinde kullanan bir yapı göreceksiniz.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use RuntimeException;
class ResidentialProxyService
{
private Client $client;
private array $config;
// ProxyHat gateway
private const PROXY_HOST = 'gate.proxyhat.com';
private const PROXY_PORT = 8080;
public function __construct()
{
$this->config = [
'username' => config('services.proxyhat.username'),
'password' => config('services.proxyhat.password'),
'default_country' => config('services.proxyhat.default_country', 'US'),
'retry_attempts' => config('services.proxyhat.retry_attempts', 3),
'timeout' => config('services.proxyhat.timeout', 30),
];
$this->client = new Client([
'timeout' => $this->config['timeout'],
'connect_timeout' => 10,
'verify' => true,
]);
}
/**
* Proxy ile istek at
*/
public function request(
string $method,
string $url,
array $options = [],
?string $country = null,
?string $sessionId = null
): array {
$attempts = 0;
$maxAttempts = $this->config['retry_attempts'];
$lastException = null;
while ($attempts < $maxAttempts) {
$attempts++;
// Her denemede yeni session = yeni IP
$currentSession = $sessionId ?? $this->generateSessionId();
$username = $this->buildUsername($country, $currentSession);
try {
$response = $this->client->request($method, $url, array_merge($options, [
'proxy' => $this->getProxyUrl($username),
'headers' => array_merge(
$this->getDefaultHeaders(),
$options['headers'] ?? []
),
]));
return [
'success' => true,
'status' => $response->getStatusCode(),
'body' => $response->getBody()->getContents(),
'session_id' => $currentSession,
'attempts' => $attempts,
];
} catch (RequestException $e) {
$lastException = $e;
$statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0;
Log::warning("Proxy request attempt {$attempts} failed", [
'url' => $url,
'status' => $statusCode,
'session' => $currentSession,
'error' => $e->getMessage(),
]);
// Rate limit veya block - yeni IP ile retry
if (in_array($statusCode, [429, 403, 503])) {
sleep(min($attempts, 5)); // Exponential backoff
continue;
}
// Diğer hatalar - retry yok
break;
}
}
return [
'success' => false,
'error' => $lastException?->getMessage() ?? 'Unknown error',
'attempts' => $attempts,
];
}
/**
* GET isteği shorthand
*/
public function get(string $url, ?string $country = null): array
{
return $this->request('GET', $url, [], $country);
}
/**
* POST isteği shorthand
*/
public function post(string $url, array $data, ?string $country = null): array
{
return $this->request('POST', $url, [
'json' => $data,
], $country);
}
/**
* Sticky session - aynı IP ile birden fazla istek
*/
public function withSession(string $sessionId, callable $callback): mixed
{
return $callback($sessionId, fn($method, $url, $options = []) =>
$this->request($method, $url, $options, null, $sessionId)
);
}
/**
* Batch istekler - her URL için farklı IP
*/
public function batch(array $urls, ?string $country = null): array
{
$results = [];
foreach ($urls as $key => $url) {
$results[$key] = $this->get($url, $country);
// Rate limiting için küçük gecikme
usleep(100000); // 100ms
}
return $results;
}
/**
* Proxy pool istatistikleri
*/
public function getStats(): array
{
return Cache::remember('proxy_pool_stats', 300, function () {
$response = $this->client->get('https://api.proxyhat.com/v1/stats', [
'auth' => [$this->config['username'], $this->config['password']],
]);
return json_decode($response->getBody()->getContents(), true);
});
}
private function buildUsername(?string $country = null, ?string $session = null): string
{
$parts = [$this->config['username']];
$country = $country ?? $this->config['default_country'];
if ($country) {
$parts[] = "country-{$country}";
}
if ($session) {
$parts[] = "session-{$session}";
}
return implode('-', $parts);
}
private function getProxyUrl(string $username): string
{
return "http://{$username}:{$this->config['password']}@" . self::PROXY_HOST . ':' . self::PROXY_PORT;
}
private function generateSessionId(): string
{
return 'laravel-' . bin2hex(random_bytes(8));
}
private function getDefaultHeaders(): array
{
return [
'User-Agent' => config('services.proxyhat.user_agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'),
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
];
}
}
Laravel Job ile Kullanım
<?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 $maxExceptions = 3;
public int $timeout = 120;
public function __construct(
private string $productUrl,
private string $country = 'US'
) {}
public function handle(ResidentialProxyService $proxy): void
{
Log::info("Starting product scrape", ['url' => $this->productUrl]);
$response = $proxy->get($this->productUrl, $this->country);
if (!$response['success']) {
Log::error("Product scrape failed", [
'url' => $this->productUrl,
'error' => $response['error'],
]);
// Retry with different IP
$this->release(30); // 30 saniye sonra retry
return;
}
// HTML parsing
$data = $this->parseProduct($response['body']);
// Veritabanına kaydet
// Product::updateOrCreate(['url' => $this->productUrl], $data);
Log::info("Product scraped successfully", [
'url' => $this->productUrl,
'session_id' => $response['session_id'],
]);
}
private function parseProduct(string $html): array
{
// DOM parsing logic
return [
'title' => 'Product Title', // Extract from HTML
'price' => 99.99,
'availability' => true,
'scraped_at' => now(),
];
}
}
// Dispatch
class ProductController extends Controller
{
public function scrape(Request $request, ResidentialProxyService $proxy)
{
$urls = $request->input('urls', []);
$country = $request->input('country', 'US');
foreach ($urls as $url) {
ScrapeProductJob::dispatch($url, $country);
}
return response()->json([
'message' => 'Scraping jobs queued',
'count' => count($urls),
]);
}
}
Laravel Config Dosyası
config/services.php dosyasına ProxyHat konfigürasyonunu ekleyin:
<?php
// config/services.php
return [
// ... diğer servisler
'proxyhat' => [
'username' => env('PROXYHAT_USERNAME'),
'password' => env('PROXYHAT_PASSWORD'),
'default_country' => env('PROXYHAT_DEFAULT_COUNTRY', 'US'),
'retry_attempts' => env('PROXYHAT_RETRY_ATTEMPTS', 3),
'timeout' => env('PROXYHAT_TIMEOUT', 30),
'user_agent' => env('PROXYHAT_USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'),
],
];
PHP multi_curl ile Eşzamanlı İstekler
Yüksek performans gerektiren scraping işlemlerinde curl_multi_* fonksiyonları kullanarak paralel istekler yapabilirsiniz. Bu yöntem, Guzzle Pool'dan daha düşük seviyeli ve daha fazla kontrol sunar.
<?php
declare(strict_types=1);
class MultiCurlProxyClient
{
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
private string $proxyUser;
private string $proxyPass;
public function __construct(string $proxyUser, string $proxyPass)
{
$this->proxyUser = $proxyUser;
$this->proxyPass = $proxyPass;
}
/**
* Çoklu paralel istek
*
* @param array $urls URL'ler array'i
* @param int $concurrency Eşzamanlı istek sayısı
* @param string|null $country Coğrafi hedefleme
* @return array Her URL için sonuç
*/
public function fetchMultiple(
array $urls,
int $concurrency = 10,
?string $country = null
): array {
$results = [];
$handles = [];
$sessions = [];
// Multi curl başlat
$mh = curl_multi_init();
// İlk batch curl handle'larını oluştur
$urlKeys = array_keys($urls);
$urlValues = array_values($urls);
$urlCount = count($urls);
$running = 0;
// İlk batch'i başlat
for ($i = 0; $i < min($concurrency, $urlCount); $i++) {
$key = $urlKeys[$i];
$url = $urlValues[$i];
$sessionId = $this->generateSessionId();
$sessions[$key] = $sessionId;
$ch = $this->createCurlHandle($url, $sessionId, $country);
$handles[$key] = $ch;
curl_multi_add_handle($mh, $ch);
}
$currentIndex = $concurrency;
// İstekleri çalıştır
do {
curl_multi_exec($mh, $running);
// Tamamlanan istekleri kontrol et
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$key = array_search($ch, $handles, true);
if ($key !== false) {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
if ($info['result'] === CURLE_OK && $httpCode >= 200 && $httpCode < 400) {
$results[$key] = [
'success' => true,
'body' => curl_multi_getcontent($ch),
'http_code' => $httpCode,
'session_id' => $sessions[$key],
];
} else {
$results[$key] = [
'success' => false,
'error' => $error ?: "HTTP {$httpCode}",
'http_code' => $httpCode,
];
}
// Handle'ı temizle
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
unset($handles[$key]);
// Yeni URL ekle (varsa)
if ($currentIndex < $urlCount) {
$newKey = $urlKeys[$currentIndex];
$newUrl = $urlValues[$currentIndex];
$sessionId = $this->generateSessionId();
$sessions[$newKey] = $sessionId;
$newCh = $this->createCurlHandle($newUrl, $sessionId, $country);
$handles[$newKey] = $newCh;
curl_multi_add_handle($mh, $newCh);
$currentIndex++;
}
}
}
// Kısa bekleme (CPU kullanımını azalt)
curl_multi_select($mh, 0.1);
} while ($running > 0 || !empty($handles));
curl_multi_close($mh);
return $results;
}
/**
* Curl handle oluştur
*/
private function createCurlHandle(
string $url,
string $sessionId,
?string $country
): \CurlHandle {
$ch = curl_init();
$username = $this->buildUsername($country, $sessionId);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// Proxy
CURLOPT_PROXY => $this->proxyHost,
CURLOPT_PROXYPORT => $this->proxyPort,
CURLOPT_PROXYUSERPWD => "{$username}:{$this->proxyPass}",
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
// SSL
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
// Headers
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.9',
],
]);
return $ch;
}
private function buildUsername(?string $country, string $sessionId): string
{
$parts = [$this->proxyUser];
if ($country) {
$parts[] = "country-{$country}";
}
$parts[] = "session-{$sessionId}";
return implode('-', $parts);
}
private function generateSessionId(): string
{
return bin2hex(random_bytes(8));
}
}
// Kullanım
$client = new MultiCurlProxyClient('user', 'your-password');
$urls = [
'page1' => 'https://httpbin.org/ip',
'page2' => 'https://httpbin.org/user-agent',
'page3' => 'https://httpbin.org/headers',
'page4' => 'https://httpbin.org/get',
'page5' => 'https://httpbin.org/xml',
];
$results = $client->fetchMultiple($urls, concurrency: 5, country: 'DE');
foreach ($results as $key => $result) {
echo "[$key] ";
echo $result['success'] ? "OK ({$result['http_code']})" : "FAILED: {$result['error']}";
echo "\n";
}
TLS/SSL Konfigürasyonu ve CA Bundle Yönetimi
Proxy kullanırken SSL/TLS doğrulaması kritik bir güvenlik katmanıdır. Özellikle man-in-the-middle saldırılarına karşı CA bundle doğrulaması şarttır. Ancak bazı durumlarda (eski sistemler, self-signed sertifikalar) CA bundle yönetimi gerekir.
CA Bundle Konfigürasyonu
<?php
declare(strict_types=1);
class TlsProxyClient
{
private string $proxyHost = 'gate.proxyhat.com';
private int $proxyPort = 8080;
// CA bundle yolları (önce gelen ilk bulunan kullanılır)
private array $caPaths = [
'/etc/ssl/certs/ca-certificates.crt', // Debian/Ubuntu
'/etc/pki/tls/certs/ca-bundle.crt', // RHEL/CentOS
'/etc/ssl/ca-bundle.pem', // Alpine
'/usr/local/etc/openssl/cert.pem', // macOS Homebrew
'/etc/openssl/certs/ca-certificates.crt', // FreeBSD
];
private ?string $caBundlePath = null;
public function __construct(
private string $proxyUser,
private string $proxyPass
) {
$this->detectCaBundle();
}
/**
* Sistem CA bundle'ını tespit et
*/
private function detectCaBundle(): void
{
foreach ($this->caPaths as $path) {
if (file_exists($path) && is_readable($path)) {
$this->caBundlePath = $path;
break;
}
}
// Composer CA bundle fallback
if (!$this->caBundlePath) {
$composerCa = dirname(__DIR__) . '/vendor/composer/ca-bundle/res/cacert.pem';
if (file_exists($composerCa)) {
$this->caBundlePath = $composerCa;
}
}
}
/**
* Guzzle client oluştur (TLS sertifikalı)
*/
public function createGuzzleClient(): \GuzzleHttp\Client
{
$options = [
'timeout' => 30,
'connect_timeout' => 10,
];
if ($this->caBundlePath) {
$options['verify'] = $this->caBundlePath;
} else {
// CA bundle bulunamadı - production'da önerilmez
trigger_error(
'CA bundle not found. SSL verification may fail. ' .
'Install ca-certificates or use composer/ca-bundle.',
E_USER_WARNING
);
$options['verify'] = true; // Sistem default'u dene
}
return new \GuzzleHttp\Client($options);
}
/**
* cURL handle oluştur (TLS sertifikalı)
*/
public function createCurlHandle(string $url): \CurlHandle
{
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
// Proxy
CURLOPT_PROXY => $this->proxyHost,
CURLOPT_PROXYPORT => $this->proxyPort,
CURLOPT_PROXYUSERPWD => "{$this->proxyUser}:{$this->proxyPass}",
// TLS/SSL - SNI (Server Name Indication)
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
// TLS 1.3 desteği (PHP 7.4+, cURL 7.52+)
CURLOPT_TLS13_CIPHERS => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
];
if ($this->caBundlePath) {
$options[CURLOPT_CAINFO] = $this->caBundlePath;
// CURLOPT_CAPATH de ekle (dizin bazlı)
$caDir = dirname($this->caBundlePath);
if (is_dir($caDir)) {
$options[CURLOPT_CAPATH] = $caDir;
}
}
curl_setopt_array($ch, $options);
return $ch;
}
/**
* SSL sertifikası bilgilerini al (debug için)
*/
public function getSSLCertificateInfo(string $url): ?array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_NOBODY => true, // HEAD request
CURLOPT_CERTINFO => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CAINFO => $this->caBundlePath,
]);
curl_exec($ch);
if (curl_errno($ch)) {
curl_close($ch);
return null;
}
$certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);
$sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);
curl_close($ch);
return [
'cert_info' => $certInfo,
'ssl_verify_result' => $sslVerifyResult,
'ca_bundle_path' => $this->caBundlePath,
];
}
/**
* TLS bağlantı testi
*/
public function testTLSConnection(string $url): array
{
$ch = $this->createCurlHandle($url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_exec($ch);
$info = [
'ssl_verify_result' => curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT),
'primary_ip' => curl_getinfo($ch, CURLINFO_PRIMARY_IP),
'primary_port' => curl_getinfo($ch, CURLINFO_PRIMARY_PORT),
'scheme' => curl_getinfo($ch, CURLINFO_SCHEME),
'error' => curl_error($ch),
'errno' => curl_errno($ch),
];
curl_close($ch);
return $info;
}
}
// Kullanım
$client = new TlsProxyClient('user', 'your-password');
// TLS bağlantı testi
$testResult = $client->testTLSConnection('https://httpbin.org/get');
print_r($testResult);
// Sertifika bilgileri
$certInfo = $client->getSSLCertificateInfo('https://httpbin.org');
print_r($certInfo);
Sertifika Doğrulama Hatalarını Yönetme
<?php
/**
* SSL hata yönetimi ve retry logic
*/
class RobustProxyClient extends TlsProxyClient
{
public function fetchWithSSLErrorHandling(string $url, int $maxRetries = 3): array
{
$lastError = null;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$ch = parent::createCurlHandle($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Başarılı
if ($errno === 0 && $httpCode >= 200 && $httpCode < 400) {
return [
'success' => true,
'body' => $response,
'http_code' => $httpCode,
'attempts' => $attempt,
];
}
$lastError = $error ?: "HTTP {$httpCode}";
// SSL hata kodları
$sslErrors = [
CURLE_SSL_CACERT, // 60
CURLE_SSL_CACERT_BADFILE, // 77
CURLE_SSL_CONNECT_ERROR, // 35
CURLE_SSL_CERTPROBLEM, // 58
];
if (in_array($errno, $sslErrors, true)) {
// SSL hatası - CA bundle sorunu olabilir
error_log("SSL error on attempt {$attempt}: [{$errno}] {$error}");
// Fallback: sistem CA bundle'ını dene
$ch = parent::createCurlHandle($url);
curl_setopt_array($ch, [
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
]);
$response = curl_exec($ch);
if (curl_errno($ch) === 0) {
curl_close($ch);
return [
'success' => true,
'body' => $response,
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'attempts' => $attempt,
];
}
curl_close($ch);
}
// Exponential backoff
if ($attempt < $maxRetries) {
usleep($attempt * 500000); // 0.5s, 1s, 1.5s...
}
}
return [
'success' => false,
'error' => $lastError,
'attempts' => $maxRetries,
];
}
}
PHP HTTP Client Karşılaştırması
| Özellik | Raw cURL | Guzzle | Symfony HTTP Client |
|---|---|---|---|
| Kurulum | Yerleşik (eklenti gerekli) | Composer ile | Composer ile |
| Proxy Desteği | Manuel (CURLOPT_PROXY) | Otomatik (options array) | Otomatik (options array) |
| Async/Concurrent | curl_multi_* (karmaşık) | Pool ile (orta) | Native async (kolay) |
| Laravel Entegrasyonu | Doğrudan kullanım | Yerleşik destek | Ek paket gerekli |
| Performans | En yüksek | Yüksek | Yüksek |
| Hata Yönetimi | Manuel | Exception-based | Exception-based |
| Öğrenme Eğrisi | Düşük (basit) | Orta | Orta |
Best Practices ve Production İpuçları
Logging ve Monitoring
<?php
use Psr\Log\LoggerInterface;
class LoggedProxyClient
{
public function __construct(
private ResidentialProxyService $proxy,
private LoggerInterface $logger
) {}
public function fetchWithLogging(string $url, ?string $country = null): array
{
$startTime = microtime(true);
$result = $this->proxy->request('GET', $url, [], $country);
$duration = round((microtime(true) - $startTime) * 1000, 2);
$this->logger->info('Proxy request completed', [
'url' => $url,
'country' => $country,
'success' => $result['success'],
'http_code' => $result['status'] ?? 0,
'duration_ms' => $duration,
'attempts' => $result['attempts'] ?? 1,
'session_id' => $result['session_id'] ?? null,
]);
return $result;
}
}
Circuit Breaker Pattern
Art arda gelen hatalarda servisi geçici olarak devre dışı bırakarak sistem kaynaklarını koruyun:
<?php
class CircuitBreakerProxyClient
{
private int $failureCount = 0;
private ?int $lastFailureTime = null;
private bool $isOpen = false;
public function __construct(
private ResidentialProxyService $proxy,
private int $failureThreshold = 5,
private int $resetTimeout = 60
) {}
public function request(string $method, string $url, array $options = []): array
{
// Circuit açık mı kontrol et
if ($this->isOpen) {
$elapsed = time() - ($this->lastFailureTime ?? 0);
if ($elapsed < $this->resetTimeout) {
return [
'success' => false,
'error' => 'Circuit breaker is open',
'retry_after' => $this->resetTimeout - $elapsed,
];
}
// Reset
$this->isOpen = false;
$this->failureCount = 0;
}
$result = $this->proxy->request($method, $url, $options);
if ($result['success']) {
// Başarılı - sayacı sıfırla
$this->failureCount = 0;
return $result;
}
// Başarısız - sayacı artır
$this->failureCount++;
$this->lastFailureTime = time();
if ($this->failureCount >= $this->failureThreshold) {
$this->isOpen = true;
}
return $result;
}
public function isOpen(): bool
{
return $this->isOpen;
}
}
Önemli Çıkarımlar
Key Takeaways:
- Raw cURL en düşük seviyeli ve en performanslı seçenektir, ancak daha fazla kod gerektirir.
- Guzzle modern PHP projeleri için önerilen seçenektir; async desteği ve exception-based hata yönetimi sunar.
- Symfony HTTP Client native async desteği ile yüksek performanslı concurrent istekler için idealdir.
- multi_curl maksimum kontrol ve performans gerektiren durumlarda kullanılmalıdır.
- TLS/SSL doğrulaması production ortamlarında asla atlanmamalıdır; CA bundle yönetimi önemlidir.
- Session ID kullanarak sticky sessions ile aynı IP'den birden fazla istek atılabilir (login flows için kritik).
- Geo-targeting ile ülke/bazlı IP'ler kullanarak coğrafi kısıtlamalar aşılabilir.
- Circuit breaker ve retry logic production sistemlerinde zorunludur.
PHP ile proxy kullanımı, modern web scraping ve API entegrasyon projelerinin vazgeçilmez bir parçasıdır. Bu rehberde ele aldığımız teknikleri uygulayarak, residential proxy servisleri ile güvenilir, ölçeklenebilir ve production-ready uygulamalar geliştirebilirsiniz.
Daha fazla kaynak için:






