دليل استخدام بروكسي HTTP في PHP: من cURL إلى Laravel

دليل تقني شامل لاستخدام بروكسي HTTP في PHP. تعلم إعداد cURL وGuzzle وSymfony HTTP Client، مع أمثلة عملية على التدوير والتوافق مع Laravel.

دليل استخدام بروكسي HTTP في PHP: من cURL إلى Laravel

إذا كنت مطور PHP يعمل على مشاريع تجميع بيانات أو تكامل مع واجهات برمجية خارجية، فمن المرجح أنك واجهت حظراً بسبب معدل الطلبات أو قيود جغرافية. بروكسي HTTP ليس مجرد أداة لإخفاء الهوية — إنه مكون أساسي في أي بنية تحتية تعتمد على جمع البيانات بشكل موثوق.

في هذا الدليل، سنتناول ست طرق مختلفة لاستخدام البروكسي في PHP، من cURL الخام إلى تكامل Laravel الكامل. كل مثال قابل للتشغيل ويتبع أفضل الممارسات الإنتاجية.

لماذا تحتاج بروكسي HTTP في PHP؟

عند إرسال طلبات HTTP من خادمك مباشرة، يرى الموقع المستهدف عنوان IP الخاص بك. إذا تجاوزت حدود المعدل أو قمت بطلبات من منطقة جغرافية غير مدعومة، يتم حظرك. البروكسي يعمل كوسيط:

  • توزيع الحمل: توزيع الطلبات عبر عناوين IP متعددة لتجنب حدود المعدل.
  • الوصول الجغرافي: محاكاة الطلبات من دول أو مدن محددة.
  • الاستمرارية: إذا تم حظر IP واحد، تستمر باقي البروكسيات في العمل.
  • الخصوصية: إخفاء عنوان IP الأصلي للخادم.

للحالات الاستخدام المتقدمة مثل تجميع الويب أو تتبع نتائج البحث، البروكسي السكني يوفر موثوقية أعلى من بروكسي مركز البيانات لأنه يبدو كحركة مرور مستخدم حقيقي.

الطريقة الأولى: cURL الخام مع CURLOPT_PROXY

cURL هي المكتبة الأساسية في PHP للتعامل مع HTTP. إضافة البروكسي تتطلب ضبط عدد من الخيارات:

<?php

/**
 * مثال أساسي على استخدام البروكسي مع cURL
 * يتضمن المصادقة ومعالجة الأخطاء
 */
function fetchWithProxy(string $url, string $proxyUrl, int $timeout = 30): string
{
    $ch = curl_init();
    
    // الخيارات الأساسية
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_TIMEOUT => $timeout,
        CURLOPT_CONNECTTIMEOUT => 10,
        
        // إعداد البروكسي
        CURLOPT_PROXY => 'gate.proxyhat.com',
        CURLOPT_PROXYPORT => 8080,
        CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
        
        // المصادقة على البروكسي
        CURLOPT_PROXYUSERPWD => 'user-country-US:PASSWORD',
        
        // إعدادات TLS/SSL
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
        
        // Headers إضافية
        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',
        ],
    ]);
    
    $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;
}

// الاستخدام
try {
    $html = fetchWithProxy(
        'https://httpbin.org/ip',
        'http://user-country-US:PASSWORD@gate.proxyhat.com:8080'
    );
    echo $html;
} catch (RuntimeException $e) {
    echo "Error: " . $e->getMessage();
}

تدوير البروكسي مع cURL

لتنفيذ تدوير عشوائي للبروكسي، يمكنك تغيير بيانات الاعتماد في كل طلب:

<?php

class ProxyRotator
{
    private array $proxyConfigs;
    private int $currentIndex = 0;
    
    public function __construct(array $configs)
    {
        $this->proxyConfigs = $configs;
    }
    
    /**
     * الحصول على البروكسي التالي (Round-robin)
     */
    public function getNext(): array
    {
        $config = $this->proxyConfigs[$this->currentIndex];
        $this->currentIndex = ($this->currentIndex + 1) % count($this->proxyConfigs);
        return $config;
    }
    
    /**
     * الحصول على بروكسي عشوائي
     */
    public function getRandom(): array
    {
        return $this->proxyConfigs[array_rand($this->proxyConfigs)];
    }
    
    /**
     * تنفيذ طلب مع بروكسي محدد
     */
    public function fetch(string $url, bool $randomize = false): string
    {
        $proxy = $randomize ? $this->getRandom() : $this->getNext();
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $proxy['host'],
            CURLOPT_PROXYPORT => $proxy['port'],
            CURLOPT_PROXYUSERPWD => $proxy['auth'],
            CURLOPT_TIMEOUT => 30,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        
        $response = curl_exec($ch);
        
        if (curl_errno($ch)) {
            // تسجيل الخطأ والمحاولة مع بروكسي آخر
            error_log("Proxy error: " . curl_error($ch));
            curl_close($ch);
            throw new RuntimeException("Proxy request failed");
        }
        
        curl_close($ch);
        return $response;
    }
}

// الاستخدام مع ProxyHat
$rotator = new ProxyRotator([
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-US:PASSWORD'],
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-DE:PASSWORD'],
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-GB:PASSWORD'],
]);

// تدوير بين البروكسيات
foreach ($urls as $url) {
    try {
        $content = $rotator->fetch($url, randomize: true);
        // معالجة المحتوى
    } catch (RuntimeException $e) {
        continue; // تخطي الطلبات الفاشلة
    }
}

الطريقة الثانية: Guzzle HTTP Client

Guzzle هو عميل HTTP الأكثر شعبية في ecosystem PHP. يوفر واجهة برمجية أنظف ودعم مدمج للبروكسي:

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;

class GuzzleProxyClient
{
    private Client $client;
    private array $proxyPool;
    
    public function __construct(string $proxyUser, string $proxyPassword)
    {
        $this->proxyPool = [
            "http://{$proxyUser}-country-US:{$proxyPassword}@gate.proxyhat.com:8080",
            "http://{$proxyUser}-country-DE:{$proxyPassword}@gate.proxyhat.com:8080",
            "http://{$proxyUser}-country-GB:{$proxyPassword}@gate.proxyhat.com:8080",
        ];
        
        // إنشاء Handler مع middleware للتدوير
        $stack = HandlerStack::create();
        $stack->push($this->proxyRotationMiddleware());
        
        $this->client = new Client([
            'handler' => $stack,
            'timeout' => 30,
            'connect_timeout' => 10,
            'verify' => __DIR__ . '/cacert.pem',
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            ],
        ]);
    }
    
    /**
     * Middleware لتدوير البروكسي تلقائياً
     */
    private function proxyRotationMiddleware(): callable
    {
        return function (callable $handler) {
            return function (RequestInterface $request, array $options) use ($handler) {
                // اختيار بروكسي عشوائي
                $options['proxy'] = $this->proxyPool[array_rand($this->proxyPool)];
                
                return $handler($request, $options);
            };
        };
    }
    
    /**
     * طلب GET مع بروكسي
     */
    public function get(string $url, ?string $specificProxy = null): string
    {
        $options = [];
        
        // استخدام بروكسي محدد إذا تم تمريره
        if ($specificProxy !== null) {
            $options['proxy'] = $specificProxy;
        }
        
        try {
            $response = $this->client->get($url, $options);
            return (string) $response->getBody();
        } catch (RequestException $e) {
            // معالجة الأخطاء مع إعادة المحاولة
            if ($e->hasResponse()) {
                $statusCode = $e->getResponse()->getStatusCode();
                
                // إعادة المحاولة مع بروكسي مختلف للخطأ 429
                if ($statusCode === 429) {
                    return $this->get($url);
                }
            }
            throw $e;
        }
    }
    
    /**
     * طلب POST مع بروكسي
     */
    public function post(string $url, array $data): string
    {
        try {
            $response = $this->client->post($url, [
                'json' => $data,
            ]);
            return (string) $response->getBody();
        } catch (RequestException $e) {
            throw new RuntimeException("POST request failed: " . $e->getMessage());
        }
    }
}

// الاستخدام
$client = new GuzzleProxyClient('user', 'PASSWORD');

// طلب بسيط مع تدوير تلقائي
$content = $client->get('https://httpbin.org/ip');

// طلب مع بروكسي محدد
$content = $client->get('https://httpbin.org/ip', 
    'http://user-country-JP:PASSWORD@gate.proxyhat.com:8080'
);

الجلسات الثابتة (Sticky Sessions)

بعض مهام السكرابينج تتطلب نفس IP لعدة طلبات متتالية. مع ProxyHat، استخدم معرف الجلسة:

<?php

class StickySessionClient
{
    private Client $client;
    private string $sessionId;
    
    public function __construct(string $user, string $password, string $country = 'US')
    {
        // إنشاء معرف جلسة فريد
        $this->sessionId = bin2hex(random_bytes(8));
        
        $proxyUrl = sprintf(
            'http://%s-session-%s-country-%s:%s@gate.proxyhat.com:8080',
            $user,
            $this->sessionId,
            $country,
            $password
        );
        
        $this->client = new Client([
            'proxy' => $proxyUrl,
            'timeout' => 30,
            'verify' => true,
        ]);
    }
    
    /**
     * الحصول على معرف الجلسة الحالي
     */
    public function getSessionId(): string
    {
        return $this->sessionId;
    }
    
    /**
     * طلبات متعددة بنفس IP
     */
    public function requestSequence(array $urls): array
    {
        $results = [];
        
        foreach ($urls as $url) {
            try {
                $response = $this->client->get($url);
                $results[$url] = (string) $response->getBody();
            } catch (Exception $e) {
                $results[$url] = null;
                error_log("Failed for $url: " . $e->getMessage());
            }
        }
        
        return $results;
    }
}

// الاستخدام: تسجيل الدخول ثم تصفح صفحات متعددة
$session = new StickySessionClient('user', 'PASSWORD', 'US');

$pages = [
    'https://example.com/login',
    'https://example.com/dashboard',
    'https://example.com/profile',
];

$results = $session->requestSequence($pages);
// جميع الطلبات تستخدم نفس IP

الطريقة الثالثة: Symfony HTTP Client

Symfony HTTP Client يوفر دعماً مدمجاً للطلبات غير المتزامنة والبروكسي:

<?php

require 'vendor/autoload.php';

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

class SymfonyProxyClient
{
    private $client;
    private string $proxyAuth;
    
    public function __construct(string $user, string $password, string $country = 'US')
    {
        $this->proxyAuth = sprintf(
            '%s-country-%s:%s',
            $user,
            $country,
            $password
        );
        
        $this->client = HttpClient::create([
            'proxy' => sprintf(
                'http://%s@gate.proxyhat.com:8080',
                $this->proxyAuth
            ),
            'timeout' => 30,
            'verify_peer' => true,
            'verify_host' => true,
            'max_redirects' => 5,
        ]);
    }
    
    /**
     * طلب متزامن بسيط
     */
    public function fetch(string $url): string
    {
        try {
            $response = $this->client->request('GET', $url);
            
            $statusCode = $response->getStatusCode();
            
            if ($statusCode >= 400) {
                throw new RuntimeException("HTTP Error: $statusCode");
            }
            
            return $response->getContent();
        } catch (TransportException $e) {
            throw new RuntimeException("Transport error: " . $e->getMessage());
        }
    }
    
    /**
     * طلبات غير متزامنة متعددة
     */
    public function fetchConcurrent(array $urls, int $maxConcurrent = 10): array
    {
        $responses = [];
        $results = [];
        
        // إنشاء جميع الطلبات
        foreach ($urls as $key => $url) {
            $responses[$key] = $this->client->request('GET', $url);
        }
        
        // جمع النتائج
        foreach ($responses as $key => $response) {
            try {
                $results[$key] = $response->getContent();
            } catch (TransportException $e) {
                $results[$key] = null;
                error_log("Failed for key $key: " . $e->getMessage());
            }
        }
        
        return $results;
    }
    
    /**
     * طلبات متزامنة مع Stream
     */
    public function fetchWithStream(array $urls, callable $onComplete): void
    {
        $responses = [];
        
        foreach ($urls as $url) {
            $responses[$url] = $this->client->request('GET', $url);
        }
        
        // معالجة الاستجابات كلما وصلت
        foreach ($this->client->stream($responses) as $url => $chunk) {
            if ($chunk->isLast()) {
                $content = $responses[$url]->getContent();
                $onComplete($url, $content);
            }
        }
    }
}

// الاستخدام
$client = new SymfonyProxyClient('user', 'PASSWORD', 'DE');

// طلب واحد
$content = $client->fetch('https://httpbin.org/ip');

// طلبات متزامنة
$urls = [
    'page1' => 'https://httpbin.org/ip',
    'page2' => 'https://httpbin.org/headers',
    'page3' => 'https://httpbin.org/user-agent',
];

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

// مع Stream callback
$client->fetchWithStream($urls, function($url, $content) {
    echo "Completed: $url\n";
    file_put_contents('cache/' . md5($url), $content);
});

الطريقة الرابعة: تكامل Laravel مع Service Class

في تطبيقات Laravel، من الأفضل تغليف منطق البروكسي في Service Class قابل للحقن:

<?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 array $config;
    private int $retryCount = 3;
    
    public function __construct()
    {
        $this->config = [
            'username' => config('proxyhat.username'),
            'password' => config('proxyhat.password'),
            'gateway' => config('proxyhat.gateway', 'gate.proxyhat.com'),
            'port' => config('proxyhat.port', 8080),
            'default_country' => config('proxyhat.default_country', 'US'),
        ];
        
        $this->client = new Client([
            'timeout' => config('proxyhat.timeout', 30),
            'connect_timeout' => config('proxyhat.connect_timeout', 10),
            'verify' => storage_path('app/cacert.pem'),
        ]);
    }
    
    /**
     * إنشاء URL البروكسي
     */
    private function buildProxyUrl(?string $country = null, ?string $session = null): string
    {
        $country = $country ?? $this->config['default_country'];
        $username = $this->config['username'];
        
        // إضافة معرف الجلسة إذا تم تحديده
        if ($session) {
            $username .= "-session-{$session}";
        }
        
        $username .= "-country-{$country}";
        
        return sprintf(
            'http://%s:%s@%s:%d',
            $username,
            $this->config['password'],
            $this->config['gateway'],
            $this->config['port']
        );
    }
    
    /**
     * طلب GET مع تدوير البروكسي
     */
    public function get(string $url, array $options = []): string
    {
        $country = $options['country'] ?? null;
        $session = $options['session'] ?? null;
        $retry = $options['retry'] ?? true;
        
        $proxyUrl = $this->buildProxyUrl($country, $session);
        
        $attempts = 0;
        $lastException = null;
        
        while ($attempts < $this->retryCount) {
            $attempts++;
            
            try {
                $response = $this->client->get($url, [
                    'proxy' => $proxyUrl,
                    'headers' => $options['headers'] ?? $this->getDefaultHeaders(),
                ]);
                
                return (string) $response->getBody();
                
            } catch (RequestException $e) {
                $lastException = $e;
                $statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0;
                
                Log::warning("Proxy request failed", [
                    'url' => $url,
                    'attempt' => $attempts,
                    'status' => $statusCode,
                    'error' => $e->getMessage(),
                ]);
                
                // عدم إعادة المحاولة للأخطاء 4xx (عدا 429)
                if ($statusCode >= 400 && $statusCode < 500 && $statusCode !== 429) {
                    break;
                }
                
                // تغيير البروكسي للمحاولة التالية
                if ($retry && $attempts < $this->retryCount) {
                    $proxyUrl = $this->buildProxyUrl($country);
                    sleep(1); // انتظار قبل إعادة المحاولة
                }
            }
        }
        
        throw new \RuntimeException(
            "All proxy attempts failed: " . $lastException?->getMessage()
        );
    }
    
    /**
     * طلب POST
     */
    public function post(string $url, array $data, array $options = []): string
    {
        $proxyUrl = $this->buildProxyUrl(
            $options['country'] ?? null,
            $options['session'] ?? null
        );
        
        try {
            $response = $this->client->post($url, [
                'proxy' => $proxyUrl,
                'json' => $data,
                'headers' => $options['headers'] ?? $this->getDefaultHeaders(),
            ]);
            
            return (string) $response->getBody();
        } catch (RequestException $e) {
            Log::error("POST request failed", [
                'url' => $url,
                'error' => $e->getMessage(),
            ]);
            throw $e;
        }
    }
    
    /**
     * إنشاء جلسة ثابتة
     */
    public function createSession(string $country = 'US'): string
    {
        return bin2hex(random_bytes(8));
    }
    
    /**
     * التحقق من صحة البروكسي
     */
    public function testConnection(string $country = 'US'): bool
    {
        try {
            $response = $this->get('https://httpbin.org/ip', [
                'country' => $country,
                'retry' => false,
            ]);
            
            $data = json_decode($response, true);
            Log::info("Proxy test successful", ['ip' => $data['origin'] ?? 'unknown']);
            
            return true;
        } catch (\Exception $e) {
            Log::error("Proxy test failed", ['error' => $e->getMessage()]);
            return false;
        }
    }
    
    /**
     * Headers افتراضية
     */
    private function getDefaultHeaders(): array
    {
        return [
            '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.5',
        ];
    }
}

استخدام Service في Laravel Jobs

<?php

namespace App\Jobs;

use App\Services\ResidentialProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

class ScrapeUrlJob implements ShouldQueue
{
    use Queueable;
    
    public function __construct(
        public string $url,
        public ?string $country = null,
        public ?string $sessionId = null,
    ) {}
    
    public function handle(ResidentialProxyService $proxy): void
    {
        try {
            $content = $proxy->get($this->url, [
                'country' => $this->country,
                'session' => $this->sessionId,
            ]);
            
            // معالجة المحتوى
            $this->processContent($content);
            
        } catch (\Exception $e) {
            Log::error("Scraping failed", [
                'url' => $this->url,
                'error' => $e->getMessage(),
            ]);
            
            // إعادة المحاولة بعد تأخير
            $this->release(60);
        }
    }
    
    private function processContent(string $content): void
    {
        // منطق المعالجة
    }
}

// إرسال Jobs من Controller
namespace App\Http\Controllers;

use App\Jobs\ScrapeUrlJob;
use Illuminate\Support\Facades\Bus;

class ScrapeController extends Controller
{
    public function batch()
    {
        $urls = [
            'https://example.com/page1',
            'https://example.com/page2',
            'https://example.com/page3',
        ];
        
        $sessionId = bin2hex(random_bytes(8));
        
        // إنشاء batch من Jobs
        $batch = Bus::batch(
            collect($urls)->map(fn($url) => new ScrapeUrlJob(
                $url,
                'US',
                $sessionId // نفس الجلسة لجميع Jobs
            ))
        )->then(function () use ($sessionId) {
            // تنفيذ بعد اكتمال جميع Jobs
            Log::info("Batch completed for session: $sessionId");
        })->catch(function () use ($sessionId) {
            Log::error("Batch failed for session: $sessionId");
        })->dispatch();
        
        return response()->json(['batch_id' => $batch->id]);
    }
}

تسجيل Service في Container

<?php

// App\Providers\AppServiceProvider.php

namespace App\Providers;

use App\Services\ResidentialProxyService;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ResidentialProxyService::class, function ($app) {
            return new ResidentialProxyService();
        });
    }
}

// config/proxyhat.php
return [
    'username' => env('PROXYHAT_USERNAME'),
    'password' => env('PROXYHAT_PASSWORD'),
    'gateway' => env('PROXYHAT_GATEWAY', 'gate.proxyhat.com'),
    'port' => env('PROXYHAT_PORT', 8080),
    'default_country' => env('PROXYHAT_DEFAULT_COUNTRY', 'US'),
    'timeout' => env('PROXYHAT_TIMEOUT', 30),
    'connect_timeout' => env('PROXYHAT_CONNECT_TIMEOUT', 10),
];

الطريقة الخامسة: Multi-cURL للطلبات المتزامنة

عند الحاجة لجمع آلاف الصفحات، curl_multi_* يوفر أداءً أفضل بكثير من الطلبات المتسلسلة:

<?php

class MultiCurlProxyClient
{
    private array $proxyPool;
    private int $maxConnections;
    private int $timeout;
    
    public function __construct(array $proxyConfig, int $maxConnections = 50, int $timeout = 30)
    {
        $this->proxyPool = $proxyConfig;
        $this->maxConnections = $maxConnections;
        $this->timeout = $timeout;
    }
    
    /**
     * تنفيذ طلبات متزامنة مع تدوير البروكسي
     */
    public function fetchBatch(array $urls): array
    {
        $results = [];
        $handles = [];
        $mh = curl_multi_init();
        
        // إعداد الاتصالات
        $urlChunks = array_chunk($urls, $this->maxConnections, true);
        
        foreach ($urlChunks as $chunk) {
            // إنشاء handles لكل URL
            foreach ($chunk as $key => $url) {
                $ch = $this->createHandle($url, $key);
                $handles[$key] = $ch;
                curl_multi_add_handle($mh, $ch);
            }
            
            // تنفيذ الاتصالات
            $this->executeMulti($mh);
            
            // جمع النتائج
            foreach ($handles as $key => $ch) {
                $results[$key] = $this->processResult($ch, $key);
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
            }
            
            $handles = [];
        }
        
        curl_multi_close($mh);
        return $results;
    }
    
    /**
     * إنشاء cURL handle مع بروكسي
     */
    private function createHandle(string $url, $key): CurlHandle
    {
        $proxy = $this->proxyPool[array_rand($this->proxyPool)];
        
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            
            // البروكسي
            CURLOPT_PROXY => $proxy['host'],
            CURLOPT_PROXYPORT => $proxy['port'],
            CURLOPT_PROXYUSERPWD => $proxy['auth'],
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            
            // SSL
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
            
            // Headers
            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',
            ],
            
            // تتبع المعلومات
            CURLOPT_PRIVATE => json_encode([
                'key' => $key,
                'proxy' => $proxy['auth'],
            ]),
        ]);
        
        return $ch;
    }
    
    /**
     * تنفيذ multi-handle
     */
    private function executeMulti($mh): void
    {
        $active = null;
        
        do {
            $status = curl_multi_exec($mh, $active);
            
            if ($status === CURLM_CALL_MULTI_PERFORM) {
                continue;
            }
            
            if ($status !== CURLM_OK) {
                break;
            }
            
            // انتظار النشاط
            if ($active > 0) {
                curl_multi_select($mh, 1.0);
            }
            
        } while ($active > 0);
    }
    
    /**
     * معالجة النتيجة
     */
    private function processResult($ch, $key): array
    {
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        $errno = curl_errno($ch);
        $private = json_decode(curl_getinfo($ch, CURLINFO_PRIVATE), true);
        $content = curl_multi_getcontent($ch);
        
        return [
            'key' => $key,
            'success' => $errno === 0 && $httpCode >= 200 && $httpCode < 400,
            'http_code' => $httpCode,
            'content' => $content,
            'error' => $error ?: null,
            'proxy_used' => $private['proxy'] ?? 'unknown',
            'total_time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
        ];
    }
}

// الاستخدام
$proxyPool = [
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-US:PASSWORD'],
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-DE:PASSWORD'],
    ['host' => 'gate.proxyhat.com', 'port' => 8080, 'auth' => 'user-country-GB:PASSWORD'],
];

$client = new MultiCurlProxyClient($proxyPool, maxConnections: 20);

$urls = [
    'page1' => 'https://httpbin.org/ip',
    'page2' => 'https://httpbin.org/headers',
    'page3' => 'https://httpbin.org/delay/1',
    // ... المزيد من URLs
];

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

foreach ($results as $key => $result) {
    if ($result['success']) {
        echo "[$key] Success ({$result['http_code']}) - {$result['total_time']}s\n";
    } else {
        echo "[$key] Failed: {$result['error']}\n";
    }
}

الطريقة السادسة: TLS/SSL وCA Bundle

التواصل مع مواقع HTTPS يتطلب التحقق من الشهادات. PHP يحتاج إلى ملف CA bundle محدث:

<?php

class SecureProxyClient
{
    private string $caBundlePath;
    private array $proxyConfig;
    
    public function __construct(array $proxyConfig)
    {
        $this->proxyConfig = $proxyConfig;
        $this->caBundlePath = $this->ensureCaBundle();
    }
    
    /**
     * التأكد من وجود CA bundle محدث
     */
    private function ensureCaBundle(): string
    {
        // مسارات محتملة لـ CA bundle
        $possiblePaths = [
            // مسار مخصص
            __DIR__ . '/cacert.pem',
            // مسار النظام (Linux)
            '/etc/ssl/certs/ca-certificates.crt',
            // مسار النظام (macOS)
            '/usr/local/etc/openssl/cert.pem',
            // مسار cURL المُجمّع
            dirname(ini_get('curl.cainfo')) . '/cacert.pem',
        ];
        
        foreach ($possiblePaths as $path) {
            if (file_exists($path) && is_readable($path)) {
                return $path;
            }
        }
        
        // تنزيل Mozilla CA bundle إذا لم يوجد
        return $this->downloadCaBundle();
    }
    
    /**
     * تنزيل CA bundle من cURL website
     */
    private function downloadCaBundle(): string
    {
        $path = sys_get_temp_dir() . '/cacert.pem';
        
        if (!file_exists($path) || filemtime($path) < strtotime('-1 month')) {
            $url = 'https://curl.se/ca/cacert.pem';
            $content = file_get_contents($url);
            
            if ($content === false) {
                throw new RuntimeException('Failed to download CA bundle');
            }
            
            file_put_contents($path, $content);
        }
        
        return $path;
    }
    
    /**
     * طلب آمن مع التحقق الكامل من SSL
     */
    public function secureGet(string $url): string
    {
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            
            // البروكسي
            CURLOPT_PROXY => $this->proxyConfig['host'],
            CURLOPT_PROXYPORT => $this->proxyConfig['port'],
            CURLOPT_PROXYUSERPWD => $this->proxyConfig['auth'],
            
            // SSL/TLS صارم
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CAINFO => $this->caBundlePath,
            
            // TLS version minimum
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
            
            // التحقق من OCSP stapling
            CURLOPT_SSL_OPTIONS => CURLSSLOPT_NO_REVOKE,
            
            // شهادة العميل (إذا لزم الأمر)
            // CURLOPT_SSLCERT => '/path/to/client.crt',
            // CURLOPT_SSLKEY => '/path/to/client.key',
        ]);
        
        $response = curl_exec($ch);
        
        if (curl_errno($ch)) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException("SSL request failed: $error");
        }
        
        curl_close($ch);
        return $response;
    }
    
    /**
     * التحقق من شهادة الموقع
     */
    public function inspectCertificate(string $url): array
    {
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_NOBODY => true,
            CURLOPT_CERTINFO => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_CAINFO => $this->caBundlePath,
        ]);
        
        curl_exec($ch);
        
        $certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);
        $sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);
        
        curl_close($ch);
        
        return [
            'cert_info' => $certInfo,
            'ssl_verify_result' => $sslVerifyResult,
            'valid' => $sslVerifyResult === 0,
        ];
    }
}

// حل مشاكل SSL الشائعة

// 1. تحديث php.ini
// curl.cainfo = /path/to/cacert.pem
// openssl.cafile = /path/to/cacert.pem

// 2. أو برمجياً
ini_set('curl.cainfo', __DIR__ . '/cacert.pem');
ini_set('openssl.cafile', __DIR__ . '/cacert.pem');

// 3. في Guzzle
$client = new GuzzleHttp\Client([
    'verify' => __DIR__ . '/cacert.pem',
]);

// 4. تحذير: NEVER تعطيل التحقق في الإنتاج!
// هذا يعرضك لـ MITM attacks
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // خطير!

مقارنة الطرق المختلفة

الطريقة التعقيد الأداء حالة الاستخدام
cURL الخام منخفض متوسط طلبات بسيطة، سكربتات سريعة
Guzzle متوسط متوسط تطبيقات حديثة، middleware
Symfony HTTP Client متوسط عالي طلبات غير متزامنة، streaming
Laravel Service متوسط متوسط تطبيقات Laravel، Jobs
Multi-cURL عالي عالي جداً جمع آلاف الصفحات، scraping مكثف

أفضل الممارسات

  1. إعادة المحاولة الذكية: لا تعيد المحاولة فوراً. استخدم exponential backoff.
  2. تسجيل الأخطاء: سجل كل من البروكسي المستخدم والخطأ الناتج.
  3. مراقبة الأداء: تتبع وقت الاستجابة ومعدل النجاح لكل بروكسي.
  4. احترام robots.txt: تحقق من القواعد قبل السكرابينج.
  5. التحكم في المعدل: أضف تأخيراً بين الطلبات لتجنب الحظر.
  6. إدارة الجلسات: استخدم sticky sessions عند الحاجة للحفاظ على الحالة.

نصيحة: عند استخدام بروكسي سكني من ProxyHat، استخدم معرف الجلسة (session-XXXXX) للحفاظ على نفس IP عبر طلبات متعددة. هذا ضروري للعمليات التي تتطلب تسجيل دخول أو تصفح متسلسل.

نقاط رئيسية

  • cURL الخام مناسب للطلبات البسيطة ويعطي تحكماً كاملاً في كل خيار.
  • Guzzle يوفر واجهة أنظف مع middleware للتدوير التلقائي للبروكسي.
  • Symfony HTTP Client يدعم الطلبات غير المتزامنة بشكل طبيعي.
  • Laravel Service يسهّل الحقن والاختبار والاستخدام في Jobs.
  • Multi-cURL هو الخيار الأفضل للأداء العالي مع آلاف الطلبات.
  • SSL/TLS يجب أن يكون مفعلاً دائماً في الإنتاج مع CA bundle محدث.

للمزيد من التفاصيل حول أنواع البروكسي المتاحة، راجع صفحة الأسعار أو صفحة المواقع لمعرفة الدول المدعومة.

الأسئلة الشائعة

كيف أختار بين بروكسي HTTP وSOCKS5؟

بروكسي HTTP أسرع وأبسط لطلبات الويب العادية. SOCKS5 أفضل عندما تحتاج لتوجيه أنواع أخرى من الحركة (مثل FTP أو SMTP) أو عندما تحتاج لمصافحة TCP خام. مع ProxyHat، استخدم المنفذ 8080 لـ HTTP و1080 لـ SOCKS5.

ما الفرق بين الجلسة الثابتة والدوران لكل طلب؟

الدوران لكل طلب يغير IP تلقائياً مع كل طلب HTTP، مما يوزع الحمل. الجلسة الثابتة تحافظ على نفس IP لفترة محددة (عادة 1-30 دقيقة)، وهو ضروري للعمليات التي تتطلب تسجيل دخول أو تصفح متسلسل للصفحات.

كيف أتعامل مع أخطاء المهلة (timeout)؟

اضبط CURLOPT_TIMEOUT للوقت الإجمالي وCURLOPT_CONNECTTIMEOUT لوقت الاتصال. في Guzzle، استخدم خيارات 'timeout' و'connect_timeout'. أعد المحاولة مع بروكسي مختلف وزد المهلة تدريجياً (exponential backoff).

هل يمكن استخدام البروكسي مع واجهات برمجية محمية بـ Cloudflare؟

نعم، لكن تحتاج بروكسي سكني (residential) مع جلسة ثابتة وUser-Agent صالح. قد تحتاج أيضاً لمعالجة JavaScript إذا كان الموقع يستخدم تحديات Cloudflare التفاعلية. في هذه الحالات، فكر في استخدام متصفح headless مثل Puppeteer أو Playwright مع البروكسي.

كيف أتحقق من أن البروكسي يعمل؟

أرسل طلباً إلى خدمة تعيد عنوان IP مثل httpbin.org/ip. قارن الـ IP المستلم مع IP الخادم الأصلي. إذا اختلف، البروكسي يعمل بشكل صحيح. يمكنك أيضاً التحقق من الموقع الجغرافي للـ IP للتأكد من أن الاستهداف الجغرافي يعمل.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog