cURL 取得回應內容是 HTTP 客戶端核心操作。本文詳細解析 CURLOPT_RETURNTRANSFER、curl_multi_getcontent()、同步/非同步模式、串流處理大檔案、編碼轉換、錯誤處理,提供生產級封裝類別。
<?php
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 關鍵:回傳字串而非直接輸出
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch); // 現在 $response 是字串
if ($response === false) {
throw new RuntimeException("cURL 執行失敗: " . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 處理回應
$data = json_decode($response, true);
預設值為 false:直接輸出到 STDOUT,無法變數接收。**必須設為 true**。
<?php
function fetchMulti(array $urls, int $concurrency = 10): array {
$mh = curl_multi_init();
$handles = [];
$results = [];
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_PRIVATE => $url,
]);
curl_multi_add_handle($mh, $ch);
$handles[] = $ch;
}
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh, 0.1);
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$url = curl_getinfo($ch, CURLINFO_PRIVATE);
$content = curl_multi_getcontent($ch); // 關鍵:取得內容
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$results[$url] = [
'content' => $errno ? null : $content,
'http_code' => $httpCode,
'error' => $errno ? curl_error($ch) : null,
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
} while ($running);
curl_multi_close($mh);
return $results;
}
<?php
function downloadToFile(string $url, string $savePath, int $timeout = 300): bool {
$fp = fopen($savePath, 'wb');
if (!$fp) throw new RuntimeException("無法開啟寫入檔案: $savePath");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_FILE => $fp, // 直接寫入檔案指標
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_NOPROGRESS => false, // 可啟用進度回調
CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
if ($dlTotal > 0) {
$pct = round($dlNow / $dlTotal * 100, 1);
echo "\r下載進度: {$pct}% ({$dlNow}/{$dlTotal} bytes)";
}
},
]);
$result = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
fclose($fp);
curl_close($ch);
if ($errno || $httpCode < 200 || $httpCode >= 300) {
@unlink($savePath); // 失敗刪除不完整檔案
throw new RuntimeException("下載失敗: HTTP $httpCode, cURL $errno: " . curl_error($ch));
}
return true;
}
<?php
namespace App\Http;
class HttpClient {
private array $defaultOptions;
private ?resource $multiHandle = null;
public function __construct(array $defaultOptions = []) {
$this->defaultOptions = array_merge([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'App/HttpClient/1.0',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_ENCODING => 'gzip, deflate', // 自動解壓
], $defaultOptions);
}
public function get(string $url, array $options = []): Response {
return $this->request('GET', $url, null, $options);
}
public function post(string $url, mixed $data = null, array $options = []): Response {
return $this->request('POST', $url, $data, $options);
}
public function request(string $method, string $url, mixed $data = null, array $options = []): Response {
$ch = curl_init($url);
$opts = array_merge($this->defaultOptions, $options);
$opts[CURLOPT_CUSTOMREQUEST] = $method;
if ($data !== null) {
if (is_array($data) && !$this->isAssoc($data)) {
$opts[CURLOPT_POSTFIELDS] = http_build_query($data);
} elseif (is_array($data) || is_object($data)) {
$opts[CURLOPT_POSTFIELDS] = json_encode($data);
$opts[CURLOPT_HTTPHEADER] = array_merge(
$opts[CURLOPT_HTTPHEADER] ?? [],
['Content-Type: application/json']
);
} else {
$opts[CURLOPT_POSTFIELDS] = $data;
}
}
curl_setopt_array($ch, $opts);
$content = curl_exec($ch);
$errno = curl_errno($ch);
$error = $errno ? curl_error($ch) : null;
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
throw new HttpException("cURL 錯誤 ($errno): $error", 0, null, $info);
}
$httpCode = $info['http_code'] ?? 0;
$headers = $this->parseHeaders($info['header_size'] ? substr($content, 0, $info['header_size']) : '');
$body = $info['header_size'] ? substr($content, $info['header_size']) : $content;
return new Response($httpCode, $headers, $body, $info);
}
public function multiGet(array $urls, int $concurrency = 10): Generator {
// ... 同前方 fetchMulti 實作,yield Response 物件
}
private function parseHeaders(string $raw): array {
$headers = [];
foreach (explode("\r\n", trim($raw)) as $line) {
if (strpos($line, ':') !== false) {
[$k, $v] = explode(':', $line, 2);
$headers[strtolower(trim($k))] = trim($v);
}
}
return $headers;
}
private function isAssoc(array $arr): bool {
return array_keys($arr) !== range(0, count($arr) - 1);
}
}
class Response {
public function __construct(
public int $httpCode,
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->httpCode >= 200 && $this->httpCode < 300;
}
public function __toString(): string {
return $this->body;
}
}
class HttpException extends RuntimeException {
public function __construct(string $message, int $code = 0, ?Throwable $previous = null, public array $info = []) {
parent::__construct($message, $code, $previous);
}
}
| 情況 | 處理 |
|---|---|
| 回應為 JSON | json_decode($body, true),檢查 json_last_error() |
| 回應為 XML | simplexml_load_string($body) 或 DOMDocument |
| 編碼非 UTF-8 | 從 Content-Type: charset=xxx 解析,mb_convert_encoding($body, 'UTF-8', $from) |
| gzip/deflate 壓縮 | 設定 CURLOPT_ENCODING => 'gzip, deflate',cURL 自動解壓 |
| 二進位檔案 | 直接使用 $body 或 CURLOPT_FILE 寫檔 |
// 除錯模式:輸出完整請求/回應
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_VERBOSE, true);
$verbose = fopen('php://temp', 'rw+');
curl_setopt($ch, CURLOPT_STDERR, $verbose);
curl_exec($ch);
rewind($verbose);
echo "=== cURL Verbose ===\n" . stream_get_contents($verbose);
fclose($verbose);
// 檢查 SSL 憑證鏈
curl_setopt($ch, CURLOPT_CERTINFO, true);
$info = curl_getinfo($ch);
print_r($info['certinfo'] ?? []);
openssl.cafile