PHPでHTTPプロキシを使う完全ガイド:cURL・Guzzle・Laravel実装

PHPでのプロキシ利用を包括的に解説。生cURL、Guzzle、Symfony HTTP Client、Laravel統合、並行処理、TLS/SSL設定まで、実践的なコード例を6つ以上掲載。スクレイピングとAPI統合の現場で使える実装パターンを習得できます。

PHPでHTTPプロキシを使う完全ガイド:cURL・Guzzle・Laravel実装

PHP proxyの設定は、スクレイピング、サードパーティAPI統合、地理的に制限されたコンテンツへのアクセスにおいて必須スキルです。このガイドでは、生cURLからGuzzle、Symfony HTTP Client、Laravelサービスクラスまで、実践的な実装パターンを段階的に解説します。

なぜPHPでプロキシを使うのか

Webスクレイピングや自動化システムを構築する際、単一IPからの大量リクエストはブロックの原因になります。プロキシを使用することで:

  • IPブロックの回避 — リクエストを複数のIPに分散
  • 地理的制限の突破 — 国や地域ごとのコンテンツにアクセス
  • レート制限の回避 — 1IPあたりの制限を迂回
  • 匿名性の確保 — 元のIPを隠蔽

ProxyHatのプロキシサービスを使用する場合、接続情報は以下の通りです:

パラメータ
ゲートウェイgate.proxyhat.com
HTTPポート8080
SOCKS5ポート1080
認証形式USERNAME:PASSWORD@gate.proxyhat.com:8080

生cURLでプロキシを使用する

PHPのcURL拡張機能は、プロキシサポートの基礎となります。CURLOPT_PROXYCURLOPT_PROXYUSERPWDを組み合わせて、認証付きプロキシを設定します。

<?php

class ProxyCurlClient
{
    private string $proxyHost = 'gate.proxyhat.com';
    private int $proxyPort = 8080;
    private string $username;
    private string $password;
    private int $timeout = 30;
    private int $connectTimeout = 10;

    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
    }

    /**
     * 基本的なプロキシリクエスト
     */
    public function get(string $url, array $headers = []): array
    {
        $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 => $this->connectTimeout,
        ]);

        // プロキシ設定
        curl_setopt($ch, CURLOPT_PROXY, $this->proxyHost);
        curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxyPort);
        curl_setopt($ch, CURLOPT_PROXYUSERPWD, "{$this->username}:{$this->password}");
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);

        // ヘッダー設定
        if (!empty($headers)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        }

        // TLS/SSL設定(後述の詳細セクションを参照)
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        $errno = curl_errno($ch);

        curl_close($ch);

        return [
            'status' => $httpCode,
            'body' => $response,
            'error' => $error,
            'errno' => $errno,
            'success' => $errno === 0 && $httpCode >= 200 && $httpCode < 400,
        ];
    }

    /**
     * 地理的ターゲティング付きリクエスト
     */
    public function getWithGeo(string $url, string $country, ?string $city = null): array
    {
        // ProxyHatの形式: user-country-US-city-newyork
        $geoUser = $this->username;
        $geoUser .= "-country-{$country}";
        if ($city !== null) {
            $geoUser .= "-city-" . strtolower($city);
        }

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$geoUser}:{$this->password}",
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            CURLOPT_TIMEOUT => $this->timeout,
        ]);

        $response = curl_exec($ch);
        $info = curl_getinfo($ch);
        curl_close($ch);

        return [
            'status' => $info['http_code'],
            'body' => $response,
            'total_time' => $info['total_time'],
        ];
    }
}

// 使用例
$client = new ProxyCurlClient('your_username', 'your_password');

// 基本的なGETリクエスト
$result = $client->get('https://httpbin.org/ip');
echo "Status: {$result['status']}\n";
echo "Response: {$result['body']}\n";

// 地理的ターゲティング(日本からのアクセスを模倣)
$geoResult = $client->getWithGeo('https://httpbin.org/ip', 'JP', 'tokyo');
echo "Japan IP Response: {$geoResult['body']}\n";

エラーハンドリングと再試行ロジック

プロキシリクエストは、タイムアウト、接続エラー、ブロックなど多様な障害に直面します。堅牢な再試行ロジックを実装しましょう。

<?php

class RobustProxyCurl
{
    private int $maxRetries = 3;
    private int $retryDelayMs = 1000;
    private array $retryableCodes = [429, 500, 502, 503, 504];

    public function __construct(
        private string $username,
        private string $password,
        private string $proxyHost = 'gate.proxyhat.com',
        private int $proxyPort = 8080
    ) {}

    public function requestWithRetry(
        string $url,
        string $method = 'GET',
        array $data = [],
        array $headers = []
    ): array {
        $attempt = 0;
        $lastError = null;

        while ($attempt < $this->maxRetries) {
            $attempt++;
            $result = $this->executeRequest($url, $method, $data, $headers);

            if ($result['success']) {
                return $result;
            }

            $lastError = $result['error'];

            // 再試行すべきか判断
            if (!$this->shouldRetry($result)) {
                break;
            }

            // 指数バックオフで待機
            $delay = $this->retryDelayMs * pow(2, $attempt - 1);
            usleep($delay * 1000);

            // プロキシIPを変更(スティッキーセッションでない場合)
            // 新しいセッションIDを生成してIPをローテーション
            error_log("Retry attempt {$attempt} after {$delay}ms for URL: {$url}");
        }

        return [
            'success' => false,
            'error' => "Max retries exceeded. Last error: {$lastError}",
            'attempts' => $attempt,
        ];
    }

    private function executeRequest(
        string $url,
        string $method,
        array $data,
        array $headers
    ): array {
        $ch = curl_init();

        $options = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$this->username}:{$this->password}",
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
        ];

        if ($method === 'POST') {
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = http_build_query($data);
        }

        if (!empty($headers)) {
            $options[CURLOPT_HTTPHEADER] = $headers;
        }

        curl_setopt_array($ch, $options);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        $errno = curl_errno($ch);

        curl_close($ch);

        return [
            'success' => $errno === 0 && $httpCode >= 200 && $httpCode < 400,
            'status' => $httpCode,
            'body' => $response,
            'error' => $error,
            'errno' => $errno,
        ];
    }

    private function shouldRetry(array $result): bool
    {
        // cURLエラー(タイムアウト、接続失敗など)
        if ($result['errno'] !== 0) {
            return true;
        }

        // HTTPステータスコードによる判断
        return in_array($result['status'], $this->retryableCodes);
    }
}

Guzzleでプロキシを設定する

Guzzle proxy設定は、LaravelやSymfonyプロジェクトで広く採用されているパターンです。Guzzleのrequest_optionsでプロキシを柔軟に設定できます。

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleProxyClient
{
    private Client $client;
    private string $username;
    private string $password;

    public function __construct(
        string $username,
        string $password,
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080,
        int $timeout = 30
    ) {
        $this->username = $username;
        $this->password = $password;

        // プロキシURLを構築
        $proxyUrl = "http://{$username}:{$password}@{$proxyHost}:{$proxyPort}";

        // ハンドラスタックを作成(ロギングやリトライ用)
        $stack = HandlerStack::create();

        // リトライミドルウェアを追加
        $stack->push(Middleware::retry(
            function ($retries, RequestInterface $request, ResponseInterface $response = null, $exception = null) {
                if ($retries >= 3) {
                    return false;
                }
                if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
                    return true;
                }
                if ($response && $response->getStatusCode() >= 500) {
                    return true;
                }
                return false;
            },
            function ($retries) {
                return 1000 * pow(2, $retries); // 指数バックオフ
            }
        ));

        $this->client = new Client([
            'handler' => $stack,
            'proxy' => $proxyUrl,
            'timeout' => $timeout,
            'connect_timeout' => 10,
            'verify' => true, // SSL証明書検証を有効化
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            ],
        ]);
    }

    /**
     * リクエストごとにプロキシをローテーション
     */
    public function requestWithRotation(
        string $method,
        string $uri,
        array $options = [],
        ?string $country = null,
        ?string $sessionId = null
    ): ResponseInterface {
        // ユーザー名に地理情報やセッションIDを埋め込む
        $dynamicUsername = $this->username;

        if ($country !== null) {
            $dynamicUsername .= "-country-{$country}";
        }

        if ($sessionId !== null) {
            $dynamicUsername .= "-session-{$sessionId}";
        }

        // 動的プロキシURLを構築
        $proxyUrl = "http://{$dynamicUsername}:{$this->password}@gate.proxyhat.com:8080";

        // リクエストオプションにプロキシをマージ
        $options = array_merge($options, [
            'proxy' => $proxyUrl,
        ]);

        return $this->client->request($method, $uri, $options);
    }

    /**
     * 並行リクエスト(複数URLを同時にフェッチ)
     */
    public function fetchConcurrent(array $urls, int $concurrency = 5): array
    {
        $promises = [];
        $results = [];

        foreach ($urls as $key => $url) {
            $promises[$key] = $this->client->getAsync($url);
        }

        // 並行してリクエストを実行
        $responses = \GuzzleHttp\Promise\Utils::settle($promises)->wait();

        foreach ($responses as $key => $result) {
            if ($result['state'] === 'fulfilled') {
                $results[$key] = [
                    'success' => true,
                    'body' => (string) $result['value']->getBody(),
                    'status' => $result['value']->getStatusCode(),
                ];
            } else {
                $results[$key] = [
                    'success' => false,
                    'error' => $result['reason']->getMessage(),
                ];
            }
        }

        return $results;
    }

    /**
     * スティッキーセッション(同一IPを維持)
     */
    public function createSessionClient(string $sessionId): Client
    {
        $sessionUser = $this->username . "-session-{$sessionId}";
        $proxyUrl = "http://{$sessionUser}:{$this->password}@gate.proxyhat.com:8080";

        return new Client([
            'proxy' => $proxyUrl,
            'timeout' => 30,
            'cookies' => true, // クッキーを維持
        ]);
    }
}

// 使用例
$client = new GuzzleProxyClient('your_username', 'your_password');

// 基本的なGETリクエスト
try {
    $response = $client->requestWithRotation('GET', 'https://httpbin.org/ip');
    echo "Status: " . $response->getStatusCode() . "\n";
    echo "Body: " . $response->getBody() . "\n";
} catch (\Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}

// 地理的ターゲティング
$usResponse = $client->requestWithRotation(
    'GET',
    'https://httpbin.org/ip',
    [],
    'US',
    null
);

// 並行フェッチ
$urls = [
    'page1' => 'https://httpbin.org/delay/1',
    'page2' => 'https://httpbin.org/delay/1',
    'page3' => 'https://httpbin.org/delay/1',
];
$results = $client->fetchConcurrent($urls);
print_r($results);

Symfony HTTP Clientでプロキシと非同期処理

Symfony HTTP Clientは、HttpClient::create()でプロキシを設定でき、AsyncResponseによる非同期処理をサポートします。

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class SymfonyProxyClient
{
    private HttpClientInterface $client;
    private string $username;
    private string $password;

    public function __construct(
        string $username,
        string $password,
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080
    ) {
        $this->username = $username;
        $this->password = $password;

        $proxyUrl = "http://{$username}:{$password}@{$proxyHost}:{$proxyPort}";

        $this->client = HttpClient::create([
            'proxy' => $proxyUrl,
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_host' => true,
            'verify_peer' => true,
            'headers' => [
                'User-Agent' => 'Symfony Proxy Client/1.0',
            ],
        ]);
    }

    /**
     * 同期的なリクエスト
     */
    public function fetch(string $url, array $options = []): array
    {
        $response = $this->client->request('GET', $url, $options);

        try {
            $statusCode = $response->getStatusCode();
            $content = $response->getContent();

            return [
                'success' => true,
                'status' => $statusCode,
                'body' => $content,
                'headers' => $response->getHeaders(),
            ];
        } catch (\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        } catch (\Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface $e) {
            return [
                'success' => false,
                'status' => $e->getResponse()->getStatusCode(),
                'error' => $e->getMessage(),
            ];
        }
    }

    /**
     * 非同期リクエスト(ストリーミング)
     */
    public function fetchAsync(string $url, callable $onChunk, array $options = []): void
    {
        $response = $this->client->request('GET', $url, $options);

        // チャンクごとにコールバックを実行
        foreach ($this->client->stream($response) as $chunk) {
            if ($chunk->isTimeout()) {
                continue; // タイムアウトは無視して継続
            }

            if ($chunk->isFirst()) {
                // 最初のチャンク(ヘッダー取得)
                $onChunk('headers', $response->getHeaders(), $response->getStatusCode());
            }

            if ($chunk->isLast()) {
                // 最後のチャンク
                $onChunk('complete', null, null);
            } else {
                // コンテンツチャンク
                $onChunk('data', $chunk->getContent(), null);
            }
        }
    }

    /**
     * 並行リクエスト(複数URLを効率的に処理)
     */
    public function fetchMultiple(array $urls, int $maxConcurrent = 10): array
    {
        $responses = [];
        $results = [];

        // 全URLに対して非同期リクエストを作成
        foreach ($urls as $key => $url) {
            $responses[$key] = $this->client->request('GET', $url);
        }

        // ストリームで並行処理
        foreach ($this->client->stream($responses) as $key => $chunk) {
            if ($chunk->isLast()) {
                // リクエスト完了
                $response = $responses[$key];
                try {
                    $results[$key] = [
                        'success' => true,
                        'status' => $response->getStatusCode(),
                        'body' => $response->getContent(),
                    ];
                } catch (\Exception $e) {
                    $results[$key] = [
                        'success' => false,
                        'error' => $e->getMessage(),
                    ];
                }
            }
        }

        return $results;
    }

    /**
     * 動的プロキシ設定(リクエストごとに国を変更)
     */
    public function fetchWithGeo(string $url, string $countryCode): array
    {
        $geoUser = $this->username . "-country-{$countryCode}";
        $proxyUrl = "http://{$geoUser}:{$this->password}@gate.proxyhat.com:8080";

        $dynamicClient = HttpClient::create([
            'proxy' => $proxyUrl,
            'timeout' => 30,
        ]);

        $response = $dynamicClient->request('GET', $url);

        return [
            'status' => $response->getStatusCode(),
            'body' => $response->getContent(),
        ];
    }
}

// 使用例
$client = new SymfonyProxyClient('your_username', 'your_password');

// 同期リクエスト
$result = $client->fetch('https://httpbin.org/ip');
print_r($result);

// 並行リクエスト
$urls = [
    'api1' => 'https://httpbin.org/get',
    'api2' => 'https://httpbin.org/headers',
    'api3' => 'https://httpbin.org/user-agent',
];

$results = $client->fetchMultiple($urls);
foreach ($results as $key => $result) {
    echo "{$key}: " . ($result['success'] ? $result['status'] : $result['error']) . "\n";
}

// 非同期ストリーミング
$client->fetchAsync(
    'https://httpbin.org/stream/5',
    function ($type, $data, $status) {
        switch ($type) {
            case 'headers':
                echo "Got headers, status: {$status}\n";
                break;
            case 'data':
                echo "Chunk: " . substr($data, 0, 50) . "...\n";
                break;
            case 'complete':
                echo "Stream complete!\n";
                break;
        }
    }
);

Laravelでのプロキシ統合:サービスクラスとジョブ

Laravel proxy scrapingを実装する際、サービスクラスでプロキシプールを管理し、キュージョブから利用するパターンが効果的です。

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Collection;
use RuntimeException;

class ResidentialProxyService
{
    private string $gateway;
    private string $username;
    private string $password;
    private int $httpPort;
    private int $socksPort;

    public function __construct()
    {
        $this->gateway = config('proxy.gateway', 'gate.proxyhat.com');
        $this->username = config('proxy.username');
        $this->password = config('proxy.password');
        $this->httpPort = config('proxy.http_port', 8080);
        $this->socksPort = config('proxy.socks_port', 1080);
    }

    /**
     * プロキシ設定を生成
     */
    private function buildProxyUrl(array $options = []): string
    {
        $user = $this->username;

        // 地理的ターゲティング
        if (isset($options['country'])) {
            $user .= "-country-{$options['country']}";
        }
        if (isset($options['city'])) {
            $user .= "-city-" . strtolower($options['city']);
        }

        // スティッキーセッション
        if (isset($options['session'])) {
            $user .= "-session-{$options['session']}";
        }

        $port = $options['protocol'] === 'socks5' ? $this->socksPort : $this->httpPort;
        $protocol = $options['protocol'] ?? 'http';

        return "{$protocol}://{$user}:{$this->password}@{$this->gateway}:{$port}";
    }

    /**
     * HTTPクライアントを作成(プロキシ付き)
     */
    public function createClient(array $options = []): \Illuminate\Http\Client\PendingRequest
    {
        $proxyUrl = $this->buildProxyUrl($options);

        return Http::withOptions([
            'proxy' => $proxyUrl,
            'timeout' => $options['timeout'] ?? 30,
            'connect_timeout' => $options['connect_timeout'] ?? 10,
            'verify' => $options['verify_ssl'] ?? true,
            'headers' => array_merge([
                'User-Agent' => $this->getUserAgent($options['user_agent'] ?? null),
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            ], $options['headers'] ?? []),
        ]);
    }

    /**
     * シンプルなGETリクエスト
     */
    public function get(string $url, array $options = []): array
    {
        $client = $this->createClient($options);

        try {
            $response = $client->get($url);

            return [
                'success' => $response->successful(),
                'status' => $response->status(),
                'body' => $response->body(),
                'json' => $response->json(),
            ];
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            Log::warning('Proxy connection failed', [
                'url' => $url,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'connection_failed',
                'message' => $e->getMessage(),
            ];
        } catch (\Exception $e) {
            return [
                'success' => false,
                'error' => 'request_failed',
                'message' => $e->getMessage(),
            ];
        }
    }

    /**
     * リトライ付きリクエスト
     */
    public function getWithRetry(
        string $url,
        int $maxRetries = 3,
        array $options = []
    ): array {
        $attempt = 0;
        $lastError = null;

        while ($attempt < $maxRetries) {
            $attempt++;
            $result = $this->get($url, $options);

            if ($result['success']) {
                return $result;
            }

            $lastError = $result['message'] ?? 'Unknown error';

            // 特定のエラーのみ再試行
            if (in_array($result['error'] ?? '', ['connection_failed', 'timeout'])) {
                sleep($attempt); // 指数的待機
                continue;
            }

            // 4xxエラーは再試行しない
            if (isset($result['status']) && $result['status'] >= 400 && $result['status'] < 500) {
                break;
            }
        }

        return [
            'success' => false,
            'error' => 'max_retries_exceeded',
            'message' => "Failed after {$attempt} attempts. Last error: {$lastError}",
            'attempts' => $attempt,
        ];
    }

    /**
     * ランダムUser-Agentを取得
     */
    private function getUserAgent(?string $type = null): string
    {
        $agents = [
            'chrome_windows' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'chrome_mac' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'firefox_windows' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
            'safari_mac' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
        ];

        if ($type && isset($agents[$type])) {
            return $agents[$type];
        }

        return $agents[array_rand($agents)];
    }

    /**
     * プロキシプールのヘルスチェック
     */
    public function healthCheck(): array
    {
        $testUrls = [
            'httpbin' => 'https://httpbin.org/ip',
            'ipify' => 'https://api.ipify.org?format=json',
        ];

        $results = [];
        foreach ($testUrls as $name => $url) {
            $start = microtime(true);
            $result = $this->get($url, ['timeout' => 10]);
            $latency = round((microtime(true) - $start) * 1000);

            $results[$name] = [
                'success' => $result['success'],
                'latency_ms' => $latency,
                'ip' => $result['json']['ip'] ?? $result['json']['origin'] ?? null,
            ];
        }

        return $results;
    }
}

Laravelジョブでの使用例

<?php

namespace App\Jobs;

use App\Services\ResidentialProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class ScrapeWebPageJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60; // 秒

    private string $url;
    private array $proxyOptions;

    public function __construct(string $url, array $proxyOptions = [])
    {
        $this->url = $url;
        $this->proxyOptions = $proxyOptions;
    }

    public function handle(ResidentialProxyService $proxyService): void
    {
        // ジョブごとにユニークなセッションIDを生成(スティッキーセッション)
        $sessionId = 'job-' . $this->job->uuid();
        $this->proxyOptions['session'] = $sessionId;

        $result = $proxyService->getWithRetry(
            $this->url,
            $this->tries,
            $this->proxyOptions
        );

        if (!$result['success']) {
            Log::error('Scraping job failed', [
                'url' => $this->url,
                'session' => $sessionId,
                'error' => $result['message'],
            ]);

            // 失敗を記録して再試行
            $this->release(60);
            return;
        }

        // 成功時の処理
        $this->processContent($result['body']);

        Log::info('Scraping job completed', [
            'url' => $this->url,
            'session' => $sessionId,
            'status' => $result['status'],
        ]);
    }

    private function processContent(string $html): void
    {
        // スクレイピングしたコンテンツの処理
        // 例: データ抽出、DB保存など
    }
}

// ジョブのディスパッチ
use App\Jobs\ScrapeWebPageJob;

// 基本的な使用
ScrapeWebPageJob::dispatch('https://example.com/data');

// 地理的ターゲティング付き
ScrapeWebPageJob::dispatch('https://region-locked-site.com', [
    'country' => 'JP',
    'city' => 'tokyo',
    'timeout' => 60,
]);

// バッチ処理(複数URL)
$urls = ['https://site1.com', 'https://site2.com', 'https://site3.com'];
foreach ($urls as $url) {
    ScrapeWebPageJob::dispatch($url, ['country' => 'US']);
}

multi_curlによる並行フェッチ

大量のURLを効率的に処理するには、curl_multi_*関数を使用します。これはcURLの並行処理機能を活用し、単一スレッドで複数のリクエストを同時実行します。

<?php

class MultiCurlProxyClient
{
    private string $proxyHost;
    private int $proxyPort;
    private string $username;
    private string $password;
    private int $maxConnections;
    private int $timeout;

    public function __construct(
        string $username,
        string $password,
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080,
        int $maxConnections = 10,
        int $timeout = 30
    ) {
        $this->username = $username;
        $this->password = $password;
        $this->proxyHost = $proxyHost;
        $this->proxyPort = $proxyPort;
        $this->maxConnections = $maxConnections;
        $this->timeout = $timeout;
    }

    /**
     * 複数URLを並行してフェッチ
     *
     * @param array $urls URLをキーとする配列
     * @param array $options 追加オプション
     * @return array 各URLの結果
     */
    public function fetchAll(array $urls, array $options = []): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];

        // 各URLに対してcURLハンドルを作成
        foreach ($urls as $key => $url) {
            $ch = $this->createCurlHandle($url, $options[$key] ?? []);
            curl_multi_add_handle($mh, $ch);
            $handles[$key] = $ch;
        }

        // 並行実行
        $active = null;
        do {
            $status = curl_multi_exec($mh, $active);

            if ($status === CURLM_CALL_MULTI_PERFORM) {
                continue;
            }

            if ($status !== CURLM_OK) {
                break;
            }

            // 何かイベントが発生するまで待機
            curl_multi_select($mh, 1.0);
        } while ($active && $status === CURLM_OK);

        // 結果を収集
        foreach ($handles as $key => $ch) {
            $info = curl_getinfo($ch);
            $error = curl_error($ch);
            $httpCode = $info['http_code'];
            $content = curl_multi_getcontent($ch);

            $results[$key] = [
                'success' => $httpCode >= 200 && $httpCode < 400,
                'status' => $httpCode,
                'body' => $content,
                'error' => $error ?: null,
                'total_time' => $info['total_time'],
                'size_download' => $info['size_download'],
            ];

            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }

        curl_multi_close($mh);

        return $results;
    }

    /**
     * チャンクごとの並行処理(大量のURL用)
     */
    public function fetchInChunks(array $urls, int $chunkSize = 10): array
    {
        $allResults = [];
        $chunks = array_chunk($urls, $chunkSize, true);

        foreach ($chunks as $chunk) {
            $results = $this->fetchAll($chunk);
            $allResults = array_merge($allResults, $results);

            // レート制限を避けるため短い待機
            usleep(100000); // 100ms
        }

        return $allResults;
    }

    /**
     * コールバック付きストリーミング処理
     */
    public function fetchWithCallback(
        array $urls,
        callable $onComplete,
        callable $onError = null,
        array $options = []
    ): void {
        $mh = curl_multi_init();
        $handles = [];

        foreach ($urls as $key => $url) {
            $ch = $this->createCurlHandle($url, $options[$key] ?? []);
            curl_multi_add_handle($mh, $ch);
            $handles[$key] = $ch;
        }

        $active = null;
        do {
            curl_multi_exec($mh, $active);
            curl_multi_select($mh, 0.1);

            // 完了したハンドルをチェック
            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);
                    $content = curl_multi_getcontent($ch);
                    $error = curl_error($ch);

                    if ($info['result'] === CURLE_OK && $httpCode >= 200 && $httpCode < 400) {
                        $onComplete($key, $content, $httpCode);
                    } elseif ($onError) {
                        $onError($key, $error ?: "HTTP {$httpCode}", $httpCode);
                    }

                    curl_multi_remove_handle($mh, $ch);
                    curl_close($ch);
                    unset($handles[$key]);
                }
            }
        } while ($active > 0);

        curl_multi_close($mh);
    }

    /**
     * cURLハンドルを作成
     */
    private function createCurlHandle(string $url, array $options = []): \CurlHandle
    {
        $ch = curl_init();

        // 動的ユーザー名(地理ターゲティング、セッション)
        $username = $this->username;
        if (isset($options['country'])) {
            $username .= "-country-{$options['country']}";
        }
        if (isset($options['session'])) {
            $username .= "-session-{$options['session']}";
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$username}:{$this->password}",
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            CURLOPT_TIMEOUT => $options['timeout'] ?? $this->timeout,
            CURLOPT_CONNECTTIMEOUT => $options['connect_timeout'] ?? 10,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_USERAGENT => $options['user_agent'] ?? 'MultiCurlProxyClient/1.0',
        ]);

        if (!empty($options['headers'])) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $options['headers']);
        }

        return $ch;
    }
}

// 使用例
$client = new MultiCurlProxyClient('your_username', 'your_password');

// 複数URLを並行フェッチ
$urls = [
    'page1' => 'https://httpbin.org/delay/1',
    'page2' => 'https://httpbin.org/delay/2',
    'page3' => 'https://httpbin.org/delay/1',
    'page4' => 'https://httpbin.org/get',
    'page5' => 'https://httpbin.org/headers',
];

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

foreach ($results as $key => $result) {
    echo "{$key}: Status {$result['status']}, Time {$result['total_time']}s\n";
}

// コールバック付き処理
$client->fetchWithCallback(
    $urls,
    function ($key, $content, $status) {
        echo "Completed: {$key} ({$status})\n";
        // データを保存
    },
    function ($key, $error, $status) {
        echo "Failed: {$key} - {$error}\n";
    }
);

// 大量のURLをチャンク処理
$largeUrlList = array_fill(0, 100, 'https://httpbin.org/ip');
$chunkedResults = $client->fetchInChunks($largeUrlList, 10);
echo "Processed " . count($chunkedResults) . " URLs\n";

TLS/SSL設定とCAバンドル管理

プロキシを使用する際、TLS/SSL証明書の検証は重要なセキュリティ考慮事項です。特に中間者攻撃を防ぐため、適切なCAバンドルの設定が必須です。

<?php

class SecureProxyClient
{
    private string $proxyHost;
    private int $proxyPort;
    private string $username;
    private string $password;
    private ?string $caBundlePath;

    public function __construct(
        string $username,
        string $password,
        string $proxyHost = 'gate.proxyhat.com',
        int $proxyPort = 8080
    ) {
        $this->username = $username;
        $this->password = $password;
        $this->proxyHost = $proxyHost;
        $this->proxyPort = $proxyPort;
        $this->caBundlePath = $this->findCaBundle();
    }

    /**
     * CAバンドルのパスを特定
     */
    private function findCaBundle(): ?string
    {
        // 一般的なCAバンドルの場所
        $candidates = [
            // システムのCAバンドル
            '/etc/ssl/certs/ca-certificates.crt', // Debian/Ubuntu
            '/etc/pki/tls/certs/ca-bundle.crt', // RHEL/CentOS
            '/etc/ssl/ca-bundle.pem', // OpenSUSE
            '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', // Fedora
            '/etc/ssl/cert.pem', // Alpine/macOS
            
            // ComposerでインストールされたCAバンドル
            dirname(__DIR__) . '/vendor/paragonie/certainty/cacert.pem',
            dirname(__DIR__) . '/cacert.pem',
        ];

        foreach ($candidates as $path) {
            if (file_exists($path) && is_readable($path)) {
                return $path;
            }
        }

        // cURLにバンドルされたCAを使用
        return null;
    }

    /**
     * セキュアなcURLハンドルを作成
     */
    public function createSecureCurl(string $url, array $options = []): \CurlHandle
    {
        $ch = curl_init();

        // 動的ユーザー名構築
        $username = $this->buildUsername($options);

        $curlOptions = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYPORT => $this->proxyPort,
            CURLOPT_PROXYUSERPWD => "{$username}:{$this->password}",
            CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
            
            // TLS/SSL設定
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_3,
            
            // 証明書ピンニング(オプション)
            CURLOPT_PINNEDPUBLICKEY => $options['pinned_public_key'] ?? null,
            
            // タイムアウト
            CURLOPT_TIMEOUT => $options['timeout'] ?? 30,
            CURLOPT_CONNECTTIMEOUT => $options['connect_timeout'] ?? 10,
            
            // リダイレクト
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
        ];

        // CAバンドルが見つかった場合は明示的に設定
        if ($this->caBundlePath) {
            $curlOptions[CURLOPT_CAINFO] = $this->caBundlePath;
        }

        // 証明書検証の詳細設定
        if (isset($options['verify_peer']) && !$options['verify_peer']) {
            // 警告: 本番環境では無効にしないこと
            $curlOptions[CURLOPT_SSL_VERIFYPEER] = false;
            $curlOptions[CURLOPT_SSL_VERIFYHOST] = 0;
            error_log('Warning: SSL verification disabled. Not recommended for production!');
        }

        curl_setopt_array($ch, $curlOptions);

        return $ch;
    }

    /**
     * Guzzle用のセキュア設定
     */
    public function getGuzzleOptions(array $options = []): array
    {
        $username = $this->buildUsername($options);
        $proxyUrl = "http://{$username}:{$this->password}@{$this->proxyHost}:{$this->proxyPort}";

        $guzzleOptions = [
            'proxy' => $proxyUrl,
            'timeout' => $options['timeout'] ?? 30,
            'connect_timeout' => $options['connect_timeout'] ?? 10,
            'verify' => true, // SSL証明書検証を有効
            'http_errors' => false, // HTTPエラーで例外をスローしない
            'headers' => [
                'User-Agent' => $options['user_agent'] ?? 'SecureProxyClient/1.0',
            ],
        ];

        // CAバンドルの指定
        if ($this->caBundlePath) {
            $guzzleOptions['verify'] = $this->caBundlePath;
        }

        return $guzzleOptions;
    }

    /**
     * 証明書の検証テスト
     */
    public function testSslVerification(): array
    {
        $testUrls = [
            'google' => 'https://www.google.com',
            'cloudflare' => 'https://cloudflare.com',
            'letsencrypt' => 'https://letsencrypt.org',
        ];

        $results = [];

        foreach ($testUrls as $name => $url) {
            $ch = $this->createSecureCurl($url);
            curl_exec($ch);

            $info = curl_getinfo($ch);
            $sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);
            $error = curl_error($ch);

            $results[$name] = [
                'url' => $url,
                'http_code' => $info['http_code'],
                'ssl_verify_result' => $sslVerifyResult,
                'certinfo' => $info['ssl_verify_result'] ?? null,
                'error' => $error ?: null,
                'success' => $info['http_code'] >= 200 && $info['http_code'] < 400,
            ];

            curl_close($ch);
        }

        return $results;
    }

    /**
     * TLSバージョン情報を取得
     */
    public function getTlsInfo(string $url): array
    {
        $ch = $this->createSecureCurl($url);
        curl_setopt($ch, CURLOPT_CERTINFO, true);
        curl_setopt($ch, CURLOPT_VERBOSE, true);

        // 標準エラー出力をキャプチャ
        $verbose = fopen('php://temp', 'w+');
        curl_setopt($ch, CURLOPT_STDERR, $verbose);

        curl_exec($ch);

        $info = curl_getinfo($ch);
        $certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);

        rewind($verbose);
        $verboseLog = stream_get_contents($verbose);
        fclose($verbose);

        curl_close($ch);

        return [
            'ssl_verify_result' => $info['ssl_verify_result'],
            'certinfo' => $certInfo,
            'primary_ip' => $info['primary_ip'] ?? null,
            'primary_port' => $info['primary_port'] ?? null,
            'verbose_log' => $verboseLog,
        ];
    }

    private function buildUsername(array $options): string
    {
        $username = $this->username;

        if (isset($options['country'])) {
            $username .= "-country-{$options['country']}";
        }
        if (isset($options['session'])) {
            $username .= "-session-{$options['session']}";
        }

        return $username;
    }
}

// 使用例
$client = new SecureProxyClient('your_username', 'your_password');

// SSL検証テスト
$sslResults = $client->testSslVerification();
print_r($sslResults);

// セキュアなリクエスト
$ch = $client->createSecureCurl('https://example.com', [
    'country' => 'US',
    'timeout' => 30,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP {$httpCode}: " . substr($response, 0, 100) . "...\n";

// Guzzleでの使用
use GuzzleHttp\Client;

$guzzleOptions = $client->getGuzzleOptions(['country' => 'JP']);
$guzzle = new Client($guzzleOptions);

$response = $guzzle->get('https://httpbin.org/ip');
echo $response->getBody();

比較:各アプローチの選択基準

プロジェクトの要件に応じて、適切なクライアントを選択することが重要です。

クライアント利点欠点推奨用途
生cURL最大の制御、依存なし、高速冗長、エラーハンドリングが手動シンプルなスクリプト、CLIツール
multi_curl真の並行処理、高効率複雑なコード、デバッグ困難大量URL処理、バッチスクレイピング
Guzzle直感的API、ミドルウェア、非同期対応追加依存、わずかなオーバーヘッドLaravel/Symfony統合、REST API
Symfony HTTPストリーミング、非同期、軽量Symfony依存、学習曲線大容量データ、リアルタイム処理
Laravel HTTPLaravelネイティブ、キュー統合Laravel限定、外部依存Laravelアプリ、キュージョブ

本番環境でのベストプラクティス

  • 接続プーリング — GuzzleのHandlerStackで接続を再利用し、ハンドシェイクオーバーヘッドを削減
  • サーキットブレーカー — 連続失敗時にプロキシを一時的に無効化し、タイムアウトを防ぐ
  • ロギングとモニタリング — 成功率、レイテンシ、エラータイプを追跡し、プロキシ品質を監視
  • レート制限の尊重 — 対象サイトのrobots.txtとレート制限を遵守し、倫理的なスクレイピングを心がける
  • ユーザーエージェントのローテーション — 検出を回避するため、UAを定期的に変更
  • 地理的分散 — ターゲット地域に応じたプロキシIPを選択し、より自然なトラフィックパターンを作成

重要: スクレイピングを行う際は、対象サイトの利用規約、robots.txt、および適用される法律(GDPR、CCPAなど)を遵守してください。ProxyHatのロケーションページで利用可能な地理的ターゲティングオプションを確認できます。

要点まとめ

  • 生cURLは最大の制御を提供し、依存関係が不要。シンプルなスクリプトに最適。
  • Guzzleは直感的なAPIと豊富なミドルウェアで、Laravel/Symfonyプロジェクトに適している。
  • Symfony HTTP Clientはストリーミングと非同期処理に優れ、大容量データ処理に適している。
  • Laravelサービスクラスでプロキシプールを抽象化し、キュージョブから簡単に利用可能。
  • multi_curlは真の並行処理を実現し、大量URLの効率的な処理に不可欠。
  • TLS/SSL検証を適切に設定し、セキュリティを妥協しない。
  • ProxyHatのgate.proxyhat.com:8080を使用し、ユーザー名で地理的ターゲティングとセッション管理が可能。

詳細な設定オプションと料金については、ProxyHatの料金ページをご覧ください。スクレイピングのユースケースについては、Webスクレイピングのユースケースを参照してください。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る