Search by

yunadmin / http-client

yunadmin

零依赖轻量级 HTTP 客户端类库,支持 cURL/Stream 双引擎、中间件、重定向、重试、JSON/FORM 表单、文件上传。

Package info

github.com/yunadmin-cn/http-client

pkg:composer/yunadmin/http-client

Statistics

Installs: 4

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-25 01:15 UTC

This package is auto-updated.

Last update: 2026-09-25 01:44:14 UTC


README

Packagist Version PHP Version License: MIT Stars Issues

仓库:https://github.com/yunadmin-cn/http-client
安装:composer require yunadmin/http-client

零依赖、轻量级的 PHP HTTP 客户端。仅依赖 PHP ≥ 8.0 的原生扩展(curl / json),无任何 Composer 第三方包。

特性

能力 说明
双引擎 cURL (CurlHandler) / Socket Stream (StreamHandler) 自动选择
异步并发 CurlMultiHandler 基于 cURL Multi;Future、requestAsync()、pool()(并发度控制)
流式上传 按 8KB chunk 读取 body,未知长度自动走 Transfer-Encoding: chunked,大文件友好
中间件系统 HandlerStack 管道;内置重定向、HTTP 错误抛出、指数退避重试、历史记录
Cookie 会话 CookieJar + 标准 Set-Cookie 解析(支持引号内嵌分号)
可测试 MockHandler 响应队列 + HistoryMiddleware 历史记录,不发真实请求即可测业务
常用选项对齐 decode_content、force_ip_resolve、timeout、connect_timeout、verify、cert/ssl_key、proxy、auth、on_stats、sink、json/form_params/multipart/body、query、base_uri、curl
PSR-7 风格对象 Request / Response / Uri / Stream / StreamInterface

要求:php ^8.0、ext-curl、ext-json(已在 composer.json 声明)。

安装

composer require yunadmin/http-client

或手动引入:只需注册 PSR-4 前缀 Yunadmin\HttpClient\ 指向 src/,并 require 'src/functions.php'。

1. 快速开始

use Yunadmin\HttpClient\Client;

$client = new Client([
    'base_uri' => 'https://api.example.com/v1/',
    'timeout'  => 5.0,            // 总超时(秒,支持小数)
    'headers'  => ['Accept' => 'application/json'],
]);

// GET + query
$resp = $client->get('users/123', ['query' => ['include' => 'roles,perms']]);

echo $resp->getStatusCode();          // 200
echo $resp->getHeaderLine('Content-Type'); // application/json; charset=utf-8

// 两种取 body 方式
$raw = (string)$resp->getBody();      // 原始字符串
$data = $resp->getJsonBody();         // stdClass 或 array(JSON)

2. HTTP 方法

便捷方法:get / head / post / put / patch / delete / options,每个都接受 (uri, options)。

$client = new Client();

$client->head('https://example.com');
$client->post('https://api.example.com/items',   ['json' => ['name'=>'x']]);
$client->put('https://api.example.com/items/1',  ['json' => ['name'=>'y']]);
$client->patch('https://api.example.com/items/1',['json' => ['name'=>'z']]);
$client->delete('https://api.example.com/items/1');
$client->options('https://api.example.com/');

对应异步版:getAsync / postAsync / putAsync / patchAsync / deleteAsync / requestAsync,返回 Future(见第 6 节)。

3. 请求体:JSON / 表单 / 文件上传

3.1 JSON(json)

$resp = $client->post('https://api.example.com/articles', [
    'json'    => ['title' => '你好', 'tags' => ['php','http']],
    'headers' => ['X-Trace' => 'abc'],
]);
// 自动:Content-Type: application/json + json_encode

3.2 URL 编码表单(form_params)

$client->post('https://sso.example.com/login', [
    'form_params' => [
        'username' => 'admin',
        'password' => '123456',
        'remember' => 1,
    ],
]);
// Content-Type: application/x-www-form-urlencoded

3.3 multipart 表单 & 文件上传(multipart)

$client->post('https://cdn.example.com/upload', [
    'multipart' => [
        [
            'name'     => 'avatar',
            'contents' => fopen(__DIR__ . '/face.png', 'r'),
            'filename' => 'face.png',
            'headers'  => ['Content-Type' => 'image/png'],
        ],
        [
            'name'     => 'album',
            'contents' => fopen(__DIR__ . '/album.zip', 'r'),
            'filename' => 'album.zip',
        ],
        [
            'name'     => 'desc',
            'contents' => '我的头像 & 相册',
        ],
    ],
]);

说明:

  • contents 支持 string / resource / StreamInterface 三种。
  • filename 省略时,若 contents 是文件资源会自动用 stream_get_meta_data()['uri'],否则不生成 filename=。
  • 会自动生成 boundary,Content-Type: multipart/form-data; boundary=...。
  • 大文件按 8KB chunk 读取,不会一次性进内存。

3.4 原始 body(body)

// 字符串
$client->post('https://api.example.com/raw', [
    'body'    => '任何原始字节...',
    'headers' => ['Content-Type' => 'text/plain'],
]);

// 大文件 — 使用 LazyOpenStream,按需打开、流式发送
use function Yunadmin\HttpClient\lazy_open_stream;

$body = lazy_open_stream('/data/huge.zip', 'r');
$client->post('https://cdn.example.com/upload', [
    'body'    => $body,
    'headers' => ['Content-Type' => 'application/zip'],
]);
  • getSize() != null → 自动设置 Content-Length
  • getSize() == null → 自动使用 Transfer-Encoding: chunked

4. URI 处理

4.1 base_uri + 相对 URI

$client = new Client(['base_uri' => 'https://api.example.com/v1/']);
// 注意 base_uri 以 / 结尾,否则把相对 uri 当作 path 拼接

$client->get('users');                // → /v1/users
$client->get('/users');               // → /users   (以 / 开头会覆盖 path)
$client->get('https://other.com/x');  // → 完整 URI,忽略 base_uri

4.2 query 选项

$client->get('https://api.example.com/search', [
    'query' => ['q' => 'php 8', 'page' => 1, 'tags' => ['a','b']],
]);
// 自动 http_build_query,也支持自己传字符串:'q=php+8&page=1'

4.3 Uri 类(PSR-7 风格)

use Yunadmin\HttpClient\Uri;

$u = new Uri('https://user:pass@api.example.com:8443/v1?q=1#frag');
echo $u->getScheme();        // https
echo $u->getHost();          // api.example.com
echo $u->getPort();          // 8443
echo $u->getPath();          // /v1
echo $u->getQuery();         // q=1
echo $u->getUserInfo();      // user:pass
echo (string)$u->withScheme('http')->withPort(null)->withQuery('x=2');
// → http://user:pass@api.example.com/v1?x=2#frag

5. 认证、代理、超时、TLS

$client = new Client([
    // 1. Basic / Digest / Bearer / 自定义
    'auth' => ['user', 'pass'],                   // Basic
    //'auth' => ['user', 'pass', 'digest'],       // Digest(cURL 支持)
    //'auth' => ['xxxxxxxx-token', '', 'bearer'], // Bearer
    //'auth' => 'Basic dXNlcjpwYXNz',             // 直接写字符串

    // 2. 超时
    'timeout'         => 10.0,    // 总超时秒(支持小数)
    'connect_timeout' => 2.0,     // 连接超时秒

    // 3. 强制 IP 版本
    //'force_ip_resolve' => 'v4', // 'v4' | 'v6' | null

    // 4. TLS
    'verify' => true,                             // 默认 true,校验证书
    //'verify' => '/etc/ssl/certs/ca-bundle.crt', // CA 路径
    //'cert'    => '/path/client.crt',            // 客户端证书
    //'ssl_key' => '/path/client.key',            // 客户端私钥

    // 5. 代理
    //'proxy' => 'http://127.0.0.1:8080',         // 单个
    // 或按 scheme 分别设置
    //'proxy' => [
    //    'http'  => 'http://10.0.0.1:8080',
    //    'https' => 'http://10.0.0.1:8443',
    //    'no'    => ['.intranet.local', 'localhost'],
    //],

    // 6. 解压缩
    'decode_content' => true,    // 默认 true:加 Accept-Encoding,并自动 gzip/deflate 解响应体
]);

6. 异步 & 并发(cURL Multi)

6.1 getAsync / postAsync / requestAsync → 返回 Future

$client = new Client();

$f1 = $client->getAsync('https://example.com/a');
$f2 = $client->getAsync('https://example.com/b');
$f3 = $client->postAsync('https://example.com/c', ['json' => ['k' => 'v']]);

// 阻塞等到各自完成(内部 tick Multi,三条是并发的)
$r1 = $f1->wait();
$r2 = $f2->wait();
$r3 = $f3->wait();

6.2 Future 链式回调

$client
    ->getAsync('https://api.example.com/items/1')
    ->then(static fn($resp) => $resp->getJsonBody())   // 成功:转换
    ->then(static fn($data) => process($data))         // 继续转
    ->otherwise(static fn(\Throwable $e) => log($e))   // 失败兜底
    ->wait();

链式回调会在 wait() 时按顺序执行(轻量实现,不依赖 Promise/A+ 库)。

6.3 pool() — 大列表 + 并发度控制

$urls = [
    'https://example.com/p1',
    'https://example.com/p2',
    'https://example.com/p3',
    // ... 可几百上千条
];

$requests = [];
foreach ($urls as $u) {
    $requests[] = ['GET', $u];
}

$client->pool($requests, [
    'concurrency' => 10,          // 同时最多 10 条
    'fulfilled'   => static function ($resp, $index) use ($urls) {
        echo "[OK]  #{$index} {$urls[$index]} => {$resp->getStatusCode()}\n";
    },
    'rejected'    => static function (\Throwable $e, $index) use ($urls) {
        echo "[ERR] #{$index} {$urls[$index]} => {$e->getMessage()}\n";
    },
]);

$requests 每一项支持:

  • ['GET', $uri]
  • ['POST', $uri, ['json' => [...]]]
  • 直接传 RequestInterface 实例

7. 重定向、HTTP 错误、重试

7.1 重定向(allow_redirects)

$client = new Client([
    'allow_redirects' => [
        'max'       => 5,       // 最大重定向次数
        'strict'    => true,    // 严格模式:302/303 POST → 改 GET;false 保持原方法
        'keep_body' => false,   // 重定向后是否保留 body
    ],
]);
// 或简单关闭:
// $client = new Client(['allow_redirects' => false]);

内置 RedirectMiddleware;超过 max 抛 TooManyRedirectsException。

7.2 HTTP 错误(http_errors)

默认 4xx / 5xx 会抛异常:

use Yunadmin\HttpClient\Exception\ClientException;
use Yunadmin\HttpClient\Exception\ServerException;
use Yunadmin\HttpClient\Exception\RequestException;

try {
    $client->get('https://api.example.com/404-thing');
} catch (ClientException $e) {  // 4xx
    echo "4xx: " . $e->getResponse()?->getStatusCode();
} catch (ServerException $e) {  // 5xx
    echo "5xx: " . $e->getResponse()?->getStatusCode();
} catch (RequestException $e) { // 基类(含连接失败等)
    echo "req failed: " . $e->getMessage();
}

关闭:new Client(['http_errors' => false]),自己判断 getStatusCode()。

7.3 重试(指数退避,retry)

$client = new Client([
    'retry' => [
        'retries'    => 3,          // 最多重试 3 次(共 1 + 3 = 4 次请求)
        'delay'      => 300,        // 初始延迟 ms
        'multiplier' => 2.0,        // 2 次幂退避:300 → 600 → 1200 ms
        // 'when' 可选:callable(次数, Request, Response?, e?) : bool
        //   未传时,默认:连接错误重试;5xx 重试;幂等方法的 429/503 重试
    ],
]);

关闭:'retry' => false。

8. Cookie 会话(CookieJar)

use Yunadmin\HttpClient\Cookie\CookieJar;

$jar = new CookieJar($strictMode = true);
$client = new Client(['cookies' => $jar]);

// 1. 登录,Set-Cookie 自动写入 $jar
$client->post('https://sso.example.com/login', [
    'form_params' => ['u' => 'admin', 'p' => '123456'],
]);

// 2. 后续请求自动带上匹配的 Cookie
$me = $client->get('https://sso.example.com/me')->getJsonBody();

CookieJar 常用方法:

$jar->set($name, $value, ['domain'=>'example.com','path'=>'/','secure'=>true,'expires'=>time()+3600]);
$jar->get('session_id', 'example.com', '/');           // Cookie 对象或 null
$jar->toArray();                                        // 导出数组
$jar->toHeaderString('https://api.example.com/x');      // 给某个 URL 拼 Cookie 请求头

Set-Cookie 解析器支持:引号内含分号的 value(例如 tk="abc;foo=bar"; Path=/api),不会被错误切断。

单次请求也可以直接传非 Jar 的 cookies:

$client->get('https://example.com/x', [
    'cookies' => ['sid' => 'abc'],         // array → 转 k=v
    //'cookies' => 'sid=abc; other=xyz',   // string → 直接 Cookie 头
]);

9. sink 下载、on_stats 统计、调试

9.1 文件下载(sink)

// 直接写文件路径
$client->get('https://example.com/big.zip', [
    'sink' => __DIR__ . '/tmp/big.zip',
]);

// 或写已打开的资源流
$fp = fopen('php://temp', 'w+b');
$client->get('https://example.com/x', ['sink' => $fp]);
rewind($fp);
echo stream_get_contents($fp);
fclose($fp);

使用 sink 后,$resp->getBody() 会被清空(返回空字符串),避免内存保留一份。

9.2 on_stats 统计回调

$client->get('https://example.com', [
    'on_stats' => static function (object $s) {
        echo "url: "     . $s->url . "\n";
        echo "status: "  . ($s->response?->getStatusCode() ?? '-') . "\n";
        echo "time(s): " . $s->time . "\n";
        if (isset($s->handler_data)) {
            // cURL: curl_getinfo 结果数组
            // Stream: 包含 dns/connect/starttransfer/ttfb_ms 等分段耗时(毫秒)
        }
    },
]);

9.3 debug:输出 HTTP 原始报文

$client->get('https://example.com', [
    'debug' => true,             // 输出到 stdout
    //'debug' => fopen('trace.log', 'ab'),  // 写文件
]);

9.4 全局 http_* 快捷函数

use function Yunadmin\HttpClient\http_get;
use function Yunadmin\HttpClient\http_post;
use function Yunadmin\HttpClient\http_json;

$html  = http_get('https://example.com');                             // 直接 string body
$html2 = http_post('https://example.com/form', ['a' => 1]);           // application/x-www-form-urlencoded
$data  = http_json('https://api.example.com/items');                  // 自动 json_decode → array/stdClass
$resp  = http_json('https://api.example.com/items', true);            // true = 返回 Response 对象

这些函数会复用同一个静态 Client 实例。

10. 可测试:Mock + 历史记录

use Yunadmin\HttpClient\Client;
use Yunadmin\HttpClient\Handler\HandlerStack;
use Yunadmin\HttpClient\Handler\MockHandler;
use Yunadmin\HttpClient\Middleware\HistoryMiddleware;
use Yunadmin\HttpClient\Response;

// 1. 准备响应队列
$mock = new MockHandler();
$mock->append(new Response(200, ['Content-Type' => 'application/json'], '{"id":1,"name":"a"}'));
$mock->append(new Response(201, ['X-Id' => '2'], ''));
$mock->append(new \RuntimeException('Network down'));  // 也可以排队 Exception

// 2. 建 HandlerStack,推送 HistoryMiddleware
$history = [];
$stack = HandlerStack::createWithMock($mock);
$stack->push(new HistoryMiddleware($history), 'history');

// 3. 业务代码里注入 client,不发真实请求
$client = new Client(['handler' => $stack, 'base_uri' => 'https://api.example.com/']);
$a = $client->get('items/1')->getJsonBody();
$b = $client->post('items', ['json' => ['name' => 'x']]);
try {
    $client->get('items/3');
} catch (\RuntimeException $e) {
    // Net down
}

// 4. 断言
echo $history[0]['request']->getMethod();     // GET
echo $history[0]['request']->getUri();        // https://api.example.com/items/1
echo $history[0]['response']->getStatusCode();// 200
echo $history[1]['request']->getMethod();     // POST
echo $history[1]['options']['json']['name'];  // x
echo count($history);                         // 3

11. 自定义中间件 & HandlerStack

每个中间件签名:callable(RequestInterface, array): (ResponseInterface|FutureInterface)。

use Yunadmin\HttpClient\Handler\HandlerStack;
use Yunadmin\HttpClient\Contract\RequestInterface;
use Yunadmin\HttpClient\Response;

$stack = HandlerStack::create();    // 自动挑 CurlHandler / StreamHandler

$stack->push(static function (callable $next) {
    return static function (RequestInterface $req, array $opts) use ($next) {
        // 进入:加 trace header
        $req = $req->withHeader('X-Trace-Id', bin2hex(random_bytes(8)));
        $t1 = microtime(true);

        // 交给下一层
        $result = $next($req, $opts);

        // 出来:打点(支持同步 Response 或异步 Future)
        $log = static function ($r) use ($t1, $req) {
            $ms = round((microtime(true) - $t1) * 1000, 1);
            $code = $r instanceof Response ? $r->getStatusCode() : 'ERR';
            file_put_contents('request.log', "{$req->getMethod()} {$req->getUri()} => {$code} ({$ms}ms)\n", FILE_APPEND);
            return $r;
        };

        if ($result instanceof \Yunadmin\HttpClient\Future) {
            return $result->then($log, $log);
        }
        return $log($result);
    };
}, 'trace');

$client = new Client(['handler' => $stack]);
$client->get('https://example.com');

12. 选项速查表

选项 类型 说明
base_uri string / UriInterface 相对 URI 基准
handler HandlerStack / HandlerInterface 自定义底层或中间件栈
timeout / connect_timeout float 总 / 连接超时秒(<1 可用)
verify bool / string false 关校验,或 CA 文件/目录
cert / ssl_key string / [path, pass] 客户端证书/私钥
proxy string / [http,https,no] 代理;no 为不走代理的域名数组
force_ip_resolve 'v4' / 'v6' / null 强制 IPv4/6
decode_content bool 默认 true:自动压缩&解压
auth [u,p] / [t,'','bearer'] / string Authorization 头
headers array 合并进请求头
query array / string 追加到 URI query
json mixed JSON 请求体
form_params array URL 编码表单
multipart array [{name, contents, filename?, headers?}, ...]
body string / resource / StreamInterface 原始请求体
cookies CookieJar / array / string Jar=会话保持;其他=单次
allow_redirects bool / [max,strict,keep_body] 重定向控制
http_errors bool 默认 true,4xx/5xx 抛异常
retry false / [retries,delay,multiplier,when?] 指数退避重试
on_stats callable(object) 请求完成统计回调
sink string / resource 响应体直接写文件/流
debug bool / resource 原始 HTTP 报文输出
curl [CURLOPT_* => v] CurlHandler 直接追加 CURLOPT

13. 目录结构

src/
├── Client.php               主入口:便捷方法、选项归一化、sync/async/pool
├── Future.php               异步结果:pending/fulfilled/rejected + then/otherwise/wait
├── Message.php              Request/Response 公共头操作
├── Request.php              请求对象
├── Response.php             响应对象(带 getJsonBody())
├── Uri.php                  URI 解析与拼接
├── Stream.php               数据流(资源包装 + 静态工厂 fromString/fopen)
├── Contract/                HandlerInterface、MessageInterface、RequestInterface、ResponseInterface、StreamInterface、UriInterface
├── Exception/               RequestException(基类)、ConnectException、ClientException、ServerException、TooManyRedirectsException
├── Handler/
│   ├── CurlHandler.php          同步 cURL:流式上传、on_stats、debug、curl 扩展
│   ├── CurlMultiHandler.php     并发 cURL Multi:enqueue/tick/execute + selectTimeout
│   ├── StreamHandler.php        Socket Stream:流式上传、chunked、分段耗时、force_ip_resolve
│   ├── MockHandler.php          单元测试:响应/异常队列
│   └── HandlerStack.php         中间件管道:create / createWithMock / push / resolve / __invoke / getMultiHandler / enqueue
├── Middleware/
│   ├── RedirectMiddleware.php   3xx 处理(match 表达式分派方法/body/状态)
│   ├── HttpErrorsMiddleware.php 4xx/5xx 抛 ClientException/ServerException
│   ├── RetryMiddleware.php      指数退避重试(默认策略 + 自定义 when)
│   └── HistoryMiddleware.php    记录 {request,response,options,error} 历史
├── Cookie/
│   ├── CookieJar.php            会话存取 + 匹配 domain/path + 请求前/响应后挂钩
│   └── SetCookie.php            Set-Cookie 解析:引号内嵌分号状态机 splitParts
├── Stream/
│   └── LazyOpenStream.php       懒加载文件流(首次读写才 fopen)
└── functions.php                全局辅助:stream_for() / uri_for() / build_query() / lazy_open_stream()
                                 + 便捷:http_get() / http_post() / http_json()
                                 + 调试:dump_request() / dump_response()

版本要求

  • PHP ^8.0(用到构造函数属性提升、match、str_contains/str_starts_with/str_ends_with、nullsafe ?->、??=、mixed 类型等)
  • ext-curl、ext-json(composer 自动校验)