simsoft/http-client
最新稳定版本:2.2.1
Composer 安装命令:
composer require simsoft/http-client
包简介
A fluent, zero-dependency PHP HTTP client with built-in OAuth2, retry, middleware, and streaming — powered by cURL.
关键字:
README 文档
README
A fluent, production-grade PHP HTTP client built directly on the curl_*
extension.
It combines a zero-dependency core with full PSR compliance — giving you precise
control over cURL behavior while remaining interoperable with any PSR-7/PSR-18
compatible framework or library.
Prerequisites
- PHP 8.1 or higher
- The
ext-curlextension (enabled by default in most PHP distributions) - Composer
Strengths
- Zero runtime overhead — no deep object nesting, no hidden abstraction layers. The execution path from your call to the cURL handle is direct and auditable.
- Full PSR compliance — implements PSR-7 (HTTP messages), PSR-18 (HTTP client), and supports PSR-17 factories and PSR-3 logging out of the box.
- Fluent API — chainable methods read like natural language and require no configuration objects or builder classes.
- Production-ready resilience — built-in retry with exponential backoff, customizable retry conditions, connection reuse, and HTTP/2 support.
- Precise cURL control — every cURL option is accessible via
withOptions(), buffer size is tunable, DNS cache timeout is configurable, and download resumption is handled automatically. - Memory-efficient streaming — large uploads and downloads use PSR-7
StreamInterfaceobjects and cURL's nativeCURLOPT_READFUNCTION/CURLOPT_FILErather than buffering the entire body in memory. - Extensible middleware pipeline — named, ordered middleware closures intercept both the request and response, enabling auth injection, caching, circuit breaking, and error normalization without touching core logic.
- Concurrent request execution —
HttpPoolsends batches of requests viacurl_multi_*with a configurable sliding window, automatic HTTP/2 multiplexing, and per-response callbacks. - Built-in test double —
FakeHttpClientprovides request mocking, wildcard pattern matching, response sequencing, and PHPUnit assertion methods — no external mocking libraries needed.
Comparison
| Feature | Simsoft HttpClient | Guzzle | Symfony HttpClient | Laravel HTTP Client |
|---|---|---|---|---|
| PHP requirement | 8.1+ | 7.2.5+ | 8.2+ | 8.2+ (framework) |
| Dependencies | ext-curl only |
psr/http-*, psr/log, optional adapters |
None (native PHP streams or curl) | Wraps Guzzle |
| Architecture | Single class + traits, direct cURL | Handler stack, middleware, promises | Contracts + multiple transports | Facade over Guzzle |
| PSR-18 | ✅ | ✅ | ✅ (adapter) | ❌ (Guzzle underneath) |
| PSR-7 | ✅ (response) | ✅ (full) | ❌ (own contracts) | ❌ (own contracts) |
| Transport | cURL directly | cURL or stream | cURL, stream, amphp | Guzzle (cURL) |
| HTTP/2 | ✅ native + multiplexing (HttpPool) | ✅ via cURL | ✅ native + multiplexing | ✅ via Guzzle |
| Fluent API | ✅ | ❌ (options array) | ✅ | ✅ |
| Middleware pipeline | ✅ named closures | ✅ HandlerStack | ✅ event listeners | ✅ (limited) |
| Retry built-in | ✅ + custom callback | Via middleware | ✅ RetryableHttpClient | ✅ |
| Async / concurrent | ✅ HttpPool (curl_multi) | ✅ promises | ✅ native | ✅ via Guzzle |
| Streaming upload | ✅ StreamInterface | ✅ | ✅ | ✅ |
| Streaming download | ✅ sink / sinkStream | ✅ | ✅ | ✅ |
| File attachments | ✅ CURLFile, path, resource, string | ✅ | ✅ | ✅ |
| Response dot-notation | ✅ + wildcards | ❌ | ❌ | ❌ |
| Request mocking / testing | ✅ FakeHttpClient | ✅ MockHandler | ✅ MockHttpClient | ✅ Http::fake() |
| Connection pooling | ✅ automatic handle reuse | ✅ | ✅ | ✅ via Guzzle |
| Standalone | ✅ | ✅ | ✅ | ❌ requires Laravel |
| Install size | ⭐ Tiny | Medium | Medium | Large (framework) |
| Memory footprint | ⭐ Minimal | Moderate | Low | Moderate + framework |
| Learning curve | Low | Medium | Medium | Low (Laravel only) |
Key differentiators
- Simpler mental model — One class, trait composition, no handler stacks or
DI containers. You chain methods and call
get()/post(). No factory setup is needed. - Zero-dependency core — Only requires ext-curl. Guzzle pulls in 5+ packages; Symfony needs its contracts package; Laravel needs the full framework.
- Dot-notation response access —
$response->data('data.users.*.name')with wildcard support. Other clients require manual array traversal or separate packages. - Direct cURL control — Every cURL option is accessible without abstraction layers. Buffer sizes, DNS cache, download resumption, and HTTP/2 are all first-class.
- Concurrent requests without promises —
HttpPool::create()gives you concurrent execution viacurl_multi_*with a sliding window, HTTP/2 multiplexing, and per-response callbacks — no promise chains or event loops. - Built-in test double —
FakeHttpClientprovides request mocking, pattern matching, response sequencing, and PHPUnit assertions without extra packages.
Trade-offs
- No pluggable transports — locked to cURL. This is intentional: cURL is available everywhere PHP runs, and single transport means zero adapter complexity, predictable behavior, and direct access to every cURL option. Adding stream or amphp backends would introduce abstraction layers that contradict the library's "direct and auditable" philosophy.
- No promise-based async — concurrent requests use
curl_multipolling, not promises. In PHP's short-lived request lifecycle, promises are syntactic sugar over the samecurl_multi_execloop. They add object allocations, callback chains, and event loop concepts without providing true non-blocking I/O.HttpPool::create()->send($requests)is more explicit about what actually happens.
When to choose each
| Choose | When |
|---|---|
| Simsoft HttpClient | Standalone microservices, CLI tools, or libraries where you want minimal dependencies, full cURL control, concurrent requests, built-in testing, and a fluent API without framework overhead. |
| Guzzle | You need promise-based async, broad ecosystem support, or are already in a Guzzle-dependent stack. |
| Symfony HttpClient | You need multiple transport backends (amphp, native streams), or are in a Symfony project. |
| Laravel HTTP | You're in Laravel and want the framework's testing fakes and collection integration. |
Usage Guide
- Installation
- Basic Usage
- Sending Request
- Post Request
- Set Headers
- Set CURL options
- Useful Methods
- Upload File
- Download File
- Retry Failed Request
- Logging
- Middleware Usage
- Response Handling With Dot-notation
- Response Body
- Create Custom SDK
- OAuth2 Authentication
- PSR-18 Usage
- Macro
- Concurrent Requests (HttpPool)
- Testing with FakeHttpClient
Install
composer require simsoft/http-client
Basic Usage
require "vendor/autoload.php"; use Simsoft\HttpClient\HttpClient; $response = HttpClient::make() ->withBaseUrl('https://api.domain.com/api') ->withBearerToken('YOUR_TOKEN') ->get('/users', [ 'page' => 1, 'limit' => 10, ]); echo $response->getStatusCode() . PHP_EOL; if ($response->ok()) { //{"status": 200, "data": [{"name": "John Doe","gender": "m"},{"name": "Jane Doe","gender": "f"}]} echo $response->data('status') . PHP_EOL; echo $response->data('data.0.name') . PHP_EOL; echo $response->data('data.1.name') . PHP_EOL; } else { // {"errors": {"status": 404, "title": "The resource was not found"}} echo $response->data('errors.status') . PHP_EOL; echo $response->data('errors.title') . PHP_EOL; } // Output: 200 John Doe Jane Doe
Sending Requests
use Simsoft\HttpClient\HttpClient; $client = new HttpClient(); $client->withBaseUrl('https://api.domain.com/api'); $response = $client->get('/resource'); // Perform GET request. $response = $client->get('/resource', ['foo' => 'bar', 'foo1' => 'bar2']); // GET with query params:foo=bar&foo1=bar2 $response = $client->put('/resource', ['id' => 1, 'name' => 'updated']); // Perform PUT request. $response = $client->patch('/resource', ['id' => 1]); // Perform PATCH request. $response = $client->delete('/resource', ['id' => 2]); // Perform DELETE request.
Post Requests
use Simsoft\HttpClient\HttpClient; $client = new HttpClient(); $client->withBaseUrl('https://api.domain.com'); $response = $client->withMultipart(['foo' => 'bar', 'baz' => 'qux'])->post('/user'); // Perform form-data post $response = $client->asMultipart()->post('/user', ['foo' => 'bar', 'baz' => 'qux']); // Perform form-data post $response = $client->post('/user', ['foo' => 'bar', 'baz' => 'qux']); // Perform form-data post $response = $client->withForm(['foo' => 'bar', 'baz' => 'qux'])->post('/user'); // Perform x-www-form-urlencoded post $response = $client->asForm()->post('/user', ['foo' => 'bar', 'baz' => 'qux']); // Perform x-www-form-urlencoded post $response = $client->withJson(['foo' => 'bar', 'baz' => 'qux'])->post('/user'); // JSON content request $response = $client->asJson()->post('/user', ['foo' => 'bar', 'baz' => 'qux']); // JSON content request $response = $client->withRaw('hello world')->post('/user'); // Raw text/plain content request $response = $client->withRaw('<xml>data</xml>', 'application/xml')->post('/user'); // Raw XML content request $response = $client->asRaw()->post('/user', 'hello world'); // Raw text/plain content request // Note: The client takes ownership of the stream and will close it after the request is complete. // Do not reuse the stream after this call. // For streams, you want to manage yourself, use withBody() instead. $response = $client->withBodyStream(new MyStream())->post('/user'); // Post a stream. $response = $client->withBodyStream(new MyPdfStream(), 'application/pdf')->post('/user'); // Post a PDF stream $response = $client->withBodyStream(new MyVideoStream(), 'video/mp4')->post('/user'); // Post a video stream. // GraphQL request. $response = $client->withGraphQL(' query GetUser($id: ID!) { user(id: $id) { id name } }', ['id' => 123]) ->post('/resource');
Set Headers
use Simsoft\HttpClient\HttpClient; $response = HttpClient::make() ->withBaseUrl('https://domain.com/api') ->withHeader('x-Author', 'John Doe') ->withHeaders([ 'Accept' => 'application/json', 'X-App-Version' => '1.0.0', ]) ->post('/resource', ['foo' => 'bar']);
Set CURL options
use Simsoft\HttpClient\HttpClient; $client = new HttpClient(); $response = $client ->withBaseUrl('https://domain.com/api') ->withOptions([ CURLOPT_CONNECTTIMEOUT_MS => 2000, // 2 seconds CURLOPT_TIMEOUT_MS => 5000, // 5 seconds ]) ->post('/resource', ['foo' => 'bar']);
Useful methods
use Simsoft\HttpClient\HttpClient; $response = HttpClient::make() ->withBaseUrl('https://domain.com/api') ->withBearerToken('YOUR_TOKEN') // set header Bearer YOUR_TOKEN ->timeout(30) // Request timeout in seconds (CURLOPT_TIMEOUT). ->connectionTimeout(5) // Connection timeout in seconds (CURLOPT_CONNECTTIMEOUT). ->withoutVerifying() // Disable TLS certificates verify. ->withoutReturnTransfer() // Disable return transfer. ->verbose() // Enable verbose mode ->post('/resource', ['foo' => 'bar']);
dump() vs dd() for Debugging
// dd() — dumps current state and immediately exits. Use during development. HttpClient::make() ->withBaseUrl('https://domain.com/api') ->withJson(['foo' => 'bar']) ->dd() ->post('/resource', ['foo' => 'bar']); // triggers execution — dumps full state then exits before request send. // dump() — dumps state inside the request pipeline after prepareHandle(), // then continues and completes the request. Use to inspect the fully built state. $response = HttpClient::make() ->withBaseUrl('https://domain.com/api') ->dump() ->post('/resource', ['foo' => 'bar']); // request still fires
Upload File
Upload a single file
use Simsoft\HttpClient\HttpClient; $client = HttpClient::make()->withBaseUrl('https://domain.com/api/upload'); // Attach CURLFile object. (Recommended) $response = $client->attach('file', new CURLFile('path/to/file.pdf'))->post(); // Note: Upload with a custom filename & MIME type. // attach (field name, file path|CURLFile, file name, mime type) // In practice, use only one per request unless your API accepts multiple fields. // Upload a file from a stream resource $response = $client->attach('attachment', fopen('path/to/file.pdf', 'r'), 'file.pdf', 'application/pdf')->post(); // Upload a file via a path $response = $client->attach('document', 'path/to/file.pdf', 'file.pdf', 'application/pdf')->post(); // Upload from string. $response = $client->attach('file', 'Hello world, file content here', 'note.txt')->post();
Upload multiple files.
use Simsoft\HttpClient\HttpClient; $client = HttpClient::make()->withBaseUrl('https://domain.com/api/upload') // Upload CURLFile objects. (Recommended) $response = $client->attach('files', [ new CURLFile('path/to/file1.pdf'), new CURLFile('path/to/file2.pdf'), ])->post(); // or upload files from resources $response = $client->attach('documents', [ fopen('path/to/file1.pdf', 'r'), fopen('path/to/file2.pdf', 'r'), ], 'file.pdf')->post(); // or upload files from paths $response = $client->attach('attachments', [ 'path/to/file1.pdf', 'path/to/file2.pdf', ], 'file.pdf')->post();
Download File
Download a file to disk (uses CURLOPT_FILE internally).
use Simsoft\HttpClient\HttpClient; HttpClient::make() ->sink('path/to/file.zip') ->get('https://example.com/file.zip');
Stream download to a file handle (uses CURLOPT_WRITEFUNCTION internally).
use Simsoft\HttpClient\HttpClient; $fp = fopen('php://output', 'wb'); // or $fp = fopen('path/to/file.zip', 'wb'); HttpClient::make() ->sinkStream($fp) ->get('https://example.com/file.zip'); fclose($fp);
Both methods accept a file path (string) or an open resource handle.
Retry Failed Request
Imports (
use Simsoft\HttpClient\HttpClient) are omitted in the examples below for brevity.
use Simsoft\HttpClient\HttpClient; $client = new HttpClient(); $client->withBaseUrl('https://domain.com/api/endpoint'); $response = $client->retry(3)->get(); // Retry 3 times. No wait in between attempts. $response = $client->retry(3, after: 500)->get(); // Retry 3 times, wait 500ms between attempts. $response = $client->retry(3, after: 2000)->get(); // Retry 3 times, wait 2 seconds between attempts. // Note: the second argument is in milliseconds.
Using retryWhen() to customize retry logic.
Example: Retry only network-level errors, never double-submit on 5xx.
use Simsoft\HttpClient\Response; HttpClient::make() ->retry(3, after: 500) ->retryWhen(function(Response $response, string $method, int $attempt): bool { // Only retry network-level failures, never server errors on POST return $response->isRetryableNetworkError(); }) ->withJson(['order_id' => 123]) ->post('https://api.example.com/orders');
Example: Exponential backoff with jitter
use Simsoft\HttpClient\Response; HttpClient::make() ->retry(5) ->retryWhen(function(Response $response, string $method, int $attempt): bool { if (!$response->isServerError() && !$response->isRetryableNetworkError()) { return false; } // Exponential backoff: 100ms, 200ms, 400ms, 800ms... $delay = (int) (100 * (2 ** ($attempt - 1))); // Add jitter ±20% to avoid thundering herd $jitter = (int) ($delay * 0.2); $sleep = $delay + random_int(-$jitter, $jitter); usleep($sleep * 1000); return true; }) ->get('https://api.example.com/reports/summary');
Example: Retry on specific HTTP status codes (e.g., 429 Too Many Requests)
use Simsoft\HttpClient\Response; HttpClient::make() ->withBaseUrl('https://api.example.com') ->retry(4) ->retryWhen(function(Response $response, string $method, int $attempt): bool { // Respect Retry-After header on 429 if ($response->getStatusCode() === 429) { $retryAfter = (int) $response->getHeaderLine('retry-after'); sleep(max(1, $retryAfter)); return true; } return $response->isRetryableNetworkError(); }) ->get('/search');
Logging
Set logger Psr\Log\LoggerInterface;
use Simsoft\HttpClient\HttpClient; use Monolog\Logger; $logger = new Logger('app'); $response = HttpClient::make() ->withLogger($logger) // Log with LoggerInterface. ->post('https://domain.com/api/endpoint', ['foo' => 'bar']);
Middleware Usage
Add middleware to the request pipeline. The middleware must be a callable that accepts a request instance and a closure and returns a response instance. More examples can be found in Middleware Examples
use Closure; use Simsoft\HttpClient\HttpClient; use Simsoft\HttpClient\Response; $client = HttpClient::make() ->withBaseUrl('https://api.example.com') // withMiddleware(Closure, middleware_name) middleware_name is optional. // The closure receives 2 arguments: the request object and the next middleware. // It must return a Response instance. ->withMiddleware(function (HttpClient $request, Closure $next): Response { // Modify the request before it is sent $request->withHeader('X-Custom-Header', 'Custom Value'); $response = $next(); // Inspect or modify the response after it is received return $response; }, 'my-middleware') ->get('/users');
Response Handling With Dot-notation
use Simsoft\HttpClient\HttpClient; $response = HttpClient::make() ->withBaseUrl('https://domain.com/api') ->post('/users', ['foo' => 'bar']); print_r($response->getHeaders()); // Get all headers. // output [ 'content-type' => 'application/json', 'cache-control' => 'no-cache', ] echo $response->getHeaderLine('content-type'); // output: application/json echo $response->getStatusCode(); // output: 200. echo $response->getTotalTime(); // output: 0.0112 (seconds, e.g. 11.2ms). if ($response->ok()) { // Or $response->successful() for 2xx status codes. // Output: {"status": 200, "total_records": 2034 "data": [{"name": "John Doe","gender": "m"},{"name": "Jane Doe","gender": "f"}]} echo (string) $response->getBody(); // Get raw body // Convert to object $users = $response->object(); echo $users->status . PHP_EOL; echo $users->total_records . PHP_EOL; foreach($users->data as $user) { echo $user->name . PHP_EOL; echo $user->gender . PHP_EOL; } // {"status": 200, "data": [{"name": "John Doe","gender": "m"},{"name": "Jane Doe","gender": "f"}]} $data = $response->data(); // Get full decoded array. Equivalent to $response->toArray() echo $data['status'] . PHP_EOL; echo $data['data'][0]['name'] . PHP_EOL; echo $data['data'][1]['name'] . PHP_EOL; // Support Dot-notation echo $response->data('status') . PHP_EOL; // 200 echo $response->data('data.0.name') . PHP_EOL; // 'John Doe' echo $response->data('data.1.name') . PHP_EOL; // 'Jane Doe' // output all names using wildcard. foreach($response->data('data.*.name') as $name) { echo $name . PHP_EOL; } } elseif ($response->failed()) { // for 4xx or 5xx or network error. echo $response->isNetworkError() ? 'Network Error' : 'Not Network Error'; echo $response->isServerError() ? 'Server Error' : 'Not Server Error'; echo $response->isClientError() ? 'Client Error' : 'Not Client Error'; echo $response->getMessage() . PHP_EOL; // {"errors": {"status": 404, "title": "The resource was not found"}} echo $response->data('errors.status') . PHP_EOL; echo $response->data('errors.title') . PHP_EOL; }
Response Body
The response body is an instance of Psr\Http\Message\StreamInterface.
// 3 ways to get a raw body. $raw = (string) $response->getBody(); $raw = $response->body(); $raw = $response->getRaw(); $body = $response->getBody(); echo $body->getSize(); // Get the size before reading. echo $body->getContents(); // Read body full contents. // Rewind the body to the beginning before read again. $body->rewind(); echo $body->getContents(); // Incrementally read the body. while (!$body->eof()) { echo $body->read(1024); }
License
The Simsoft HttpClient is licensed under the MIT License. See the LICENSE file for details
统计信息
- 总下载量: 130
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-06-23