將零散的 curl_setopt 封裝為可復用、可測試、型別安全的 HttpClient 類,是現代 PHP 專案的標配。本文提供兩個版本:輕量級靜態工具類(適合簡單專案)與 完整版物件導向客戶端(支援中介軟體、異常處理、PSR-18 兼容)。
<?php
class Curl {
/** 執行 GET 請求 */
public static function get(string $url, array $headers = [], int $timeout = 10): ?string {
return self::request('GET', $url, null, $headers, $timeout);
}
/** 執行 POST 請求 */
public static function post(string $url, $data = null, array $headers = [], int $timeout = 10): ?string {
return self::request('POST', $url, $data, $headers, $timeout);
}
/** 核心請求邏輯 */
private static function request(string $method, string $url, $data, array $headers, int $timeout): ?string {
$ch = curl_init();
if (!$ch) return null;
$options = [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'PHP-Curl/1.0',
CURLOPT_HTTPHEADER => array_map(fn($k, $v) => "$k: $v", array_keys($headers), $headers),
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = is_array($data) ? http_build_query($data) : $data;
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errno !== CURLE_OK) {
error_log("cURL [$errno] $error for $method $url");
return null;
}
return $response;
}
}
// 使用示例
$html = Curl::get('https://api.example.com/users');
$json = Curl::post('https://api.example.com/login', ['user' => 'a', 'pass' => 'b']);
?>
<?php
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class HttpClient implements ClientInterface {
private $ch;
private $cookieJar;
private array $defaultHeaders = ['User-Agent' => 'PHP-HttpClient/2.0'];
private array $middleware = [];
public function __construct(?string $cookieFile = null) {
$this->ch = curl_init();
$this->cookieJar = $cookieFile ?? sys_get_temp_dir() . '/curl_cookie_' . spl_object_id($this) . '.txt';
curl_setopt_array($this->ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_COOKIEJAR => $this->cookieJar,
CURLOPT_COOKIEFILE => $this->cookieJar,
CURLOPT_ENCODING => 'gzip, deflate', // 自動解壓
]);
}
/** 加入中介軟體:function (RequestInterface $req, callable $next): ResponseInterface */
public function addMiddleware(callable $mw): self {
$this->middleware[] = $mw;
return $this;
}
public function sendRequest(RequestInterface $request): ResponseInterface {
// 構建 cURL 選項
$opts = [
CURLOPT_URL => (string)$request->getUri(),
CURLOPT_CUSTOMREQUEST => $request->getMethod(),
CURLOPT_HTTPHEADER => $this->buildHeaders($request->getHeaders()),
];
$body = (string)$request->getBody();
if ($body !== '') {
$opts[CURLOPT_POSTFIELDS] = $body;
}
curl_setopt_array($this->ch, $opts);
// 執行請求
$responseBody = curl_exec($this->ch);
if ($responseBody === false) {
throw new \RuntimeException(curl_error($this->ch), curl_errno($this->ch));
}
// 構建 PSR-7 回應(需 nyholm/psr7 或 guzzlehttp/psr7)
$response = $this->createResponse($responseBody);
// 執行中介軟體鏈(日誌、重試、快取等)
return $this->runMiddleware($request, $response);
}
private function buildHeaders(array $headers): array {
$merged = array_merge($this->defaultHeaders, $headers);
return array_map(fn($k, $v) => "$k: $v", array_keys($merged), $merged);
}
private function createResponse(string $body): ResponseInterface {
$code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
$headers = $this->parseResponseHeaders();
// 這裡需使用 PSR-7 工廠創建 Response 物件,略過具體實現
return new \GuzzleHttp\Psr7\Response($code, $headers, $body);
}
private function parseResponseHeaders(): array {
// 需啟用 CURLOPT_HEADER => true 並分離頭部,此處簡化
return [];
}
private function runMiddleware(RequestInterface $req, ResponseInterface $res): ResponseInterface {
$handler = function ($r) use ($res) { return $res; };
foreach (array_reverse($this->middleware) as $mw) {
$handler = fn($r) => $mw($r, $handler);
}
return $handler($req);
}
public function __destruct() {
if (is_resource($this->ch)) curl_close($this->ch);
if (file_exists($this->cookieJar)) @unlink($this->cookieJar);
}
}
// 使用示例(需 PSR-7 實現)
$client = (new HttpClient())
->addMiddleware(function ($req, $next) {
$start = microtime(true);
$res = $next($req);
error_log(sprintf('[%s] %s %s - %dms', date('H:i:s'), $req->getMethod(), $req->getUri(), (microtime(true)-$start)*1000));
return $res;
});
$request = (new \GuzzleHttp\Psr7\ServerRequestFactory())->createRequest('GET', 'https://api.example.com/data');
$response = $client->sendRequest($request);
echo $response->getBody();
?>
| 功能 | 輕量版 | 完整版 |
|---|---|---|
| PSR-18 兼容 | ❌ | ✅ |
| Cookie 持久化 | ❌ | ✅ |
| 中介軟體/攔截器 | ❌ | ✅ |
| 自動 GZIP 解壓 | ❌ | ✅ |
| 依賴 | 零 | psr/http-client, psr/http-factory, nyholm/psr7 |
| 適用場景 | 舊專案、簡單腳本、無 Composer | 現代框架、微服務、需可觀測性 |
HttpClient 實例,避免重複 TCP/SSL 握手。CURLOPT_DNS_CACHE_TIMEOUT = 300(5 分鐘)。CURLOPT_FORBID_REUSE。curl_multi、Swoole、amphp 或 GuzzleHttp\Promise。