首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP cURL 模擬表單提交封裝類 (支持 GET/POST/文件/Cookie/代理)

     編輯:PHP 相關知識

企業級 cURL 封裝設計原則

  • 資源管理:構造函數初始化,析構函數自動關閉,防止句柄洩漏
  • 錯誤處理:異常模式、詳細錯誤信息、HTTP 狀態碼檢查
  • 功能完整:GET/POST/多部分表單、Cookie 持久化、代理、SSL、超時、重試
  • 鏈式調用:流暢接口,代碼可讀性強
  • PSR-18 兼容:可擴展實現 HttpClientInterface

核心類實現

<?php
/**
 * HttpClient - cURL 封裝
 * 支持:GET/POST/PUT/DELETE、JSON/表單/文件上傳、Cookie、代理、重試、鏈式調用
 */
class HttpClient
{
    private $ch;
    private string $url = '';
    private array $options = [];
    private array $headers = [];
    private array $queryParams = [];
    private array $postFields = [];
    private int $timeout = 30;
    private int $connectTimeout = 10;
    private int $maxRetries = 0;
    private int $retryDelay = 1000; // ms
    private string $cookieFile = '';
    private string $proxy = '';
    private bool $verifySsl = true;
    private bool $followRedirects = true;
    private int $maxRedirects = 5;
    private array $retryStatusCodes = [429, 500, 502, 503, 504];
    
    public function __construct()
    {
        $this->ch = curl_init();
        $this->setDefaultOptions();
    }
    
    public function __destruct()
    {
        if ($this->ch) curl_close($this->ch);
    }
    
    private function setDefaultOptions(): void
    {
        curl_setopt_array($this->ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => true,           // 返回頭部以便解析狀態碼
            CURLOPT_ENCODING => 'gzip,deflate', // 自動解壓
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP HttpClient)',
            CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
            CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
            CURLOPT_FOLLOWLOCATION => $this->followRedirects,
            CURLOPT_MAXREDIRS => $this->maxRedirects,
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
            CURLOPT_TIMEOUT => $this->timeout,
        ]);
    }
    
    // ===== 鏈式配置方法 =====
    public function url(string $url): self
    {
        $this->url = $url;
        return $this;
    }
    
    public function timeout(int $seconds): self
    {
        $this->timeout = $seconds;
        curl_setopt($this->ch, CURLOPT_TIMEOUT, $seconds);
        return $this;
    }
    
    public function connectTimeout(int $seconds): self
    {
        $this->connectTimeout = $seconds;
        curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $seconds);
        return $this;
    }
    
    public function header(string $name, string $value): self
    {
        $this->headers[] = "{$name}: {$value}";
        return $this;
    }
    
    public function headers(array $headers): self
    {
        foreach ($headers as $k => $v) $this->header($k, $v);
        return $this;
    }
    
    public function query(array $params): self
    {
        $this->queryParams = array_merge($this->queryParams, $params);
        return $this;
    }
    
    public function form(array $fields): self
    {
        $this->postFields = $fields;
        curl_setopt($this->ch, CURLOPT_POST, true);
        return $this;
    }
    
    public function json(array $data): self
    {
        $this->header('Content-Type', 'application/json');
        $this->postFields = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        curl_setopt($this->ch, CURLOPT_POST, true);
        return $this;
    }
    
    public function multipart(array $fields): self
    {
        // 文件上傳:['file' => new CURLFile('/path/to/file', 'image/png', 'filename.png')]
        $this->postFields = $fields;
        curl_setopt($this->ch, CURLOPT_POST, true);
        return $this;
    }
    
    public function cookieFile(string $path): self
    {
        $this->cookieFile = $path;
        curl_setopt($this->ch, CURLOPT_COOKIEJAR, $path);
        curl_setopt($this->ch, CURLOPT_COOKIEFILE, $path);
        return $this;
    }
    
    public function proxy(string $proxy): self
    {
        $this->proxy = $proxy;
        curl_setopt($this->ch, CURLOPT_PROXY, $proxy);
        return $this;
    }
    
    public function verifySsl(bool $verify = true): self
    {
        $this->verifySsl = $verify;
        curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verify);
        curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0);
        return $this;
    }
    
    public function retry(int $maxRetries, int $delayMs = 1000, array $statusCodes = []): self
    {
        $this->maxRetries = $maxRetries;
        $this->retryDelay = $delayMs;
        if ($statusCodes) $this->retryStatusCodes = $statusCodes;
        return $this;
    }
    
    // ===== 執行方法 =====
    public function get(): Response
    {
        return $this->execute('GET');
    }
    
    public function post(): Response
    {
        return $this->execute('POST');
    }
    
    public function put(): Response
    {
        return $this->execute('PUT');
    }
    
    public function delete(): Response
    {
        return $this->execute('DELETE');
    }
    
    private function execute(string $method): Response
    {
        $this->prepareRequest($method);
        
        $attempt = 0;
        while (true) {
            $response = $this->doRequest();
            
            if ($this->shouldRetry($response, $attempt)) {
                $attempt++;
                usleep($this->retryDelay * 1000);
                continue;
            }
            
            return $response;
        }
    }
    
    private function prepareRequest(string $method): void
    {
        // 構建最終 URL
        $finalUrl = $this->url;
        if ($this->queryParams) {
            $finalUrl .= (strpos($finalUrl, '?') === false ? '?' : '&') . http_build_query($this->queryParams);
        }
        curl_setopt($this->ch, CURLOPT_URL, $finalUrl);
        curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method);
        
        // 設置 Headers
        if ($this->headers) {
            curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->headers);
        }
        
        // 設置 Body
        if ($this->postFields !== []) {
            if (is_array($this->postFields)) {
                // 檢測是否為多部分表單 (包含 CURLFile)
                $hasFile = false;
                foreach ($this->postFields as $v) {
                    if ($v instanceof CURLFile) { $hasFile = true; break; }
                }
                if (!$hasFile) {
                    $this->postFields = http_build_query($this->postFields);
                }
            }
            curl_setopt($this->ch, CURLOPT_POSTFIELDS, $this->postFields);
        }
    }
    
    private function doRequest(): Response
    {
        $rawResponse = curl_exec($this->ch);
        $errno = curl_errno($this->ch);
        $error = curl_error($this->ch);
        $info = curl_getinfo($this->ch);
        
        if ($errno) {
            throw new HttpException("cURL error [{$errno}]: {$error}", $errno, $info);
        }
        
        // 分離頭部和體
        $headerSize = $info['header_size'];
        $headers = substr($rawResponse, 0, $headerSize);
        $body = substr($rawResponse, $headerSize);
        
        return new Response(
            (int)$info['http_code'],
            $this->parseHeaders($headers),
            $body,
            $info
        );
    }
    
    private function shouldRetry(Response $response, int $attempt): bool
    {
        if ($attempt >= $this->maxRetries) return false;
        return in_array($response->getStatusCode(), $this->retryStatusCodes);
    }
    
    private function parseHeaders(string $rawHeaders): array
    {
        $headers = [];
        foreach (explode("\r\n", $rawHeaders) as $line) {
            if (strpos($line, ':') !== false) {
                [$name, $value] = explode(':', $line, 2);
                $headers[trim($name)] = trim($value);
            }
        }
        return $headers;
    }
}

/** 響應對象 */
class Response
{
    public function __construct(
        public int $statusCode,
        public array $headers,
        public string $body,
        public array $info = []
    ) {}
    
    public function json(bool $assoc = true): mixed
    {
        return json_decode($this->body, $assoc);
    }
    
    public function xml(): ?SimpleXMLElement
    {
        return simplexml_load_string($this->body);
    }
    
    public function isSuccess(): bool
    {
        return $this->statusCode >= 200 && $this->statusCode < 300;
    }
    
    public function getHeader(string $name): ?string
    {
        return $this->headers[$name] ?? null;
    }
}

/** 異常類 */
class HttpException extends Exception
{
    public function __construct(
        string $message,
        public int $curlErrno,
        public array $curlInfo
    ) {
        parent::__construct($message, $curlErrno);
    }
}

使用示例

// 1. 簡單 GET
$response = (new HttpClient())
    ->url('https://api.example.com/users')
    ->query(['page' => 1, 'limit' => 20])
    ->header('Authorization', 'Bearer ' . $token)
    ->timeout(10)
    ->get();

if ($response->isSuccess()) {
    $users = $response->json();
}

// 2. JSON POST
$response = (new HttpClient())
    ->url('https://api.example.com/orders')
    ->json([
        'user_id' => 123,
        'items' => [['sku_id' => 1, 'qty' => 2]],
        'address_id' => 456
    ])
    ->retry(3, 500)
    ->post();

// 3. 文件上傳 (multipart/form-data)
$response = (new HttpClient())
    ->url('https://api.example.com/upload')
    ->multipart([
        'file' => new CURLFile('/path/to/avatar.png', 'image/png', 'avatar.png'),
        'user_id' => '123',
        'type' => 'avatar'
    ])
    ->timeout(60)
    ->post();

// 4. 持久化 Cookie (模擬登錄會話)
$client = (new HttpClient())
    ->cookieFile('/tmp/cookies.txt')
    ->url('https://site.com/login')
    ->form(['username' => 'user', 'password' => 'pass'])
    ->post();

$client->url('https://site.com/profile')->get(); // 自動攜帶 Cookie

// 5. 代理請求
$response = (new HttpClient())
    ->url('https://httpbin.org/ip')
    ->proxy('https://proxy.example.com:8080')
    ->verifySsl(false) // 代理通常需關閉驗證
    ->get();

進階擴展:PSR-18 適配器

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;

class Psr18HttpClient implements ClientInterface
{
    public function __construct(
        private HttpClient $client,
        private RequestFactoryInterface $requestFactory,
        private ResponseFactoryInterface $responseFactory
    ) {}
    
    public function sendRequest(Psr\Http\Message\RequestInterface $request): Psr\Http\Message\ResponseInterface
    {
        $method = $request->getMethod();
        $uri = (string)$request->getUri();
        
        $httpClient = (new HttpClient())
            ->url($uri)
            ->headers($this->headersToArray($request->getHeaders()))
            ->timeout(30);
        
        if ($method !== 'GET' && $method !== 'HEAD') {
            $body = (string)$request->getBody();
            $contentType = $request->getHeaderLine('Content-Type');
            if (str_contains($contentType, 'application/json')) {
                $httpClient->json(json_decode($body, true));
            } else {
                $httpClient->form($this->parseFormBody($body));
            }
        }
        
        $response = $httpClient->$method();
        
        return $this->responseFactory->createResponse($response->getStatusCode())
            ->withHeaders($response->headers)
            ->withBody($this->streamFactory->createStream($response->body));
    }
}

此封裝類已在多個高併發項目中穩定運行,支持同步阻塞模式;若需異步非阻塞,建議升級至 Swoole/OpenSwoole 或使用 amphp/http-client。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved