ndtan / php-curl-framework
NDT PHP cURL Framework — chainable HTTP client & PSR-18 adapter built on ext-curl: retries with decorrelated jitter, hedged requests, circuit breaker, async pool, TLS pinning, caching, VCR, logging, and Laravel/Symfony integrations.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
- psr/http-client: ^1.0
- psr/http-message: ^1.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5
- psr/simple-cache: ^3.0
Suggests
- guzzlehttp/psr7: Alternative PSR-7 implementation
- nyholm/psr7: PSR-7 implementation for PSR-18 adapter
- open-telemetry/api: Enable OpenTelemetry spans
- opis/json-schema: Optional JSON Schema validation (v0.3+)
- psr/simple-cache: Enable PSR-16 cache for HTTP caching & rate limiter
This package is auto-updated.
Last update: 2025-09-21 13:08:24 UTC
README
NDT PHP cURL Framework
Chainable HTTP client & PSR‑18 adapter on top of ext‑curl — production‑ready with decorrelated jitter retries, (API) hedged requests, circuit breaker, HTTP/2, TLS pinning, large‑file streaming, HTTP cache, VCR, and Laravel/Symfony integrations.
Table of Contents
- Why NDT?
- Features
- Installation
- Quick Start (Plain PHP)
- Cheat Sheet
- JSON & Body Modes
- Cookies (Netscape / JSON)
- Custom cURL Options
- Reliability: Retry · Backoff · Circuit · Hedging
- Large Files (Download/Upload > 1GB)
- HTTP Cache & VCR
- Observability (Timings, Hooks, Otel)
- Security & TLS
- Framework Integrations
- PSR‑18 Adapter
- Config Reference
- Examples
- Testing
- License
Why NDT?
- DX first: fluent builder like
Http::to(...)->asJson()->post(...)
. - Reliability: retries (decorrelated jitter) · hedging (API) · circuit breaker · deadlines.
- Performance: HTTP/2, keep‑alive, happy‑eyeballs, streaming, resume, progress.
- Security: TLS policy, pinned public key, proxy/DoH, DNS overrides.
- Ops friendly: cache, VCR, hooks, timings, PSR‑3 logs, Otel‑ready.
- Portable: PSR‑18 client & Laravel/Symfony bridges.
Features
- ✅ Retry with decorrelated jitter — honors
Retry-After
- ✅ Circuit breaker (half‑open probe) per host/service
- ✅ Hedged requests (API ready) — duplicate after X ms if no TTFB (curl_multi execution lands in v0.2)
- ✅ HTTP/2, keep‑alive, happy‑eyeballs (if supported by libcurl)
- ✅ Auto‑decompression (gzip/br/deflate), Content‑Length sanity
- ✅ Large files: streaming download/upload, resume, progress
- ✅ Cookies: Netscape or JSON jar (autodetect)
- ✅ HTTP cache (PSR‑16/PSR‑6), VCR record/replay
- ✅ Security: TLS policy, pinned public key, proxy & DoH, DNS overrides
- ✅ Observability: timings (dns/connect/tls/ttfb/transfer/total), hooks, PSR‑3 logging, OpenTelemetry (optional)
- ✅ PSR‑18 client + Laravel/Symfony bridges
New preview features & examples are linked in docs at the end of each section.
Installation
composer require ndtan/php-curl-framework
Requires PHP 8.1+,
ext-curl
,ext-json
.
Quick Start (Plain PHP)
<?php require __DIR__ . '/vendor/autoload.php'; use ndtan\Curl\Http\Http; $res = Http::to('https://httpbin.org/get') ->asJson()->expectJson() // request+response JSON ->retry(3)->backoff('decorrelated', 100, 2000, true) ->get(); if ($res->ok()) { $data = $res->json(); print_r($data); }
Tip: Use
->deadline(microtime(true)+12.0)
for a hard cap across redirects/retries.
Cheat Sheet
Http::to($url) ->headers(['Authorization' => 'Bearer XXX']) ->query(['page'=>1]) ->asJson()->data(['a'=>1])->post(); // JSON post Http::to($fileUrl)->resumeFromBytes(0)->saveTo('/tmp/file')->get(); // stream download Http::to($api)->multipart(['file'=>Http::file('/path/img.png')])->post(); // upload Http::to($url)->retry(5)->backoff('decorrelated',200,5000,true)->get(); // retry+jitter Http::to($url)->hedge(150,1)->get(); // hedge API (v0.2 adds curl_multi execution) Http::to($url)->cookieJar(__DIR__.'/cookies.txt','auto',true)->get(); // Netscape/JSON Http::to($url)->opt(CURLOPT_DOH_URL,'https://dns.google/dns-query')->get(); // DoH
See docs: BODY_MODES, COOKIES, CURL_OPTIONS, RETRY & HEDGING.
JSON & Body Modes
Choose one of these patterns:
// 1) Both request+response are JSON Http::to('/v1/users')->asJson()->post(['name' => 'Tony'])->json(); // 2) Request is JSON only Http::to('/v1/webhook')->sendJson(['event' => 'ping'])->post(); // 3) Response is JSON only $data = Http::to('/v1/metrics')->expectJson()->get()->json(); // 4) Form / Multipart / Raw stream Http::to('/submit')->asForm()->data(['a'=>1,'b'=>2])->post(); Http::to('/upload')->multipart(['file' => Http::file('/path/img.png')])->post(); Http::to('/raw')->data(fopen('/path/1GB.bin','rb'))->put(); // JSON options Http::to('/v1')->jsonFlags(JSON_THROW_ON_ERROR)->jsonAssoc(true);
See docs/BODY_MODES.md.
Cookies (Netscape / JSON)
Http::to('https://example.com') ->cookieJar(__DIR__.'/storage/cookies.txt', format: 'auto', persist: true) ->get();
auto
infers by extension (.txt
→ Netscape,.json
→ JSON) and falls back by signature.- Programmatic:
->cookie('name','value')
,->cookies([...])
,->clearCookies()
. Read docs/COOKIES.md.
Custom cURL Options
Http::to('https://example.com') ->opt(CURLOPT_SSL_VERIFYSTATUS, true) // OCSP stapling ->opt(CURLOPT_DOH_URL, 'https://dns.google/dns-query') // DoH ->opts([CURLOPT_TCP_FASTOPEN => true]) ->get();
See docs/CURL_OPTIONS.md.
Reliability: Retry · Backoff · Circuit · Hedging
use ndtan\Curl\Security\CircuitBreaker; // Retry with decorrelated jitter $res = Http::to('https://api.example.com/pay') ->asJson()->data(['amount'=>1000]) ->retry(5)->backoff('decorrelated', baseMs: 200, maxMs: 5000, jitter: true) ->post(); // Circuit breaker (half-open probe) $cb = new CircuitBreaker(failureThreshold: 5, coolDown: 30, halfOpen: 2); $res = Http::to('https://api.users.com') ->circuit($cb) ->get(); // Hedged requests (API) – duplicate after 150ms if no TTFB Http::to('https://slow.example.com') ->hedge(afterMs: 150, max: 1) ->get();
Docs: RETRY_HEDGING.md, CIRCUIT_BREAKER.md.
Large Files (Download/Upload > 1GB)
// Resume + streaming download Http::to('https://cdn.example.com/big.iso') ->resumeFromBytes(filesize('/tmp/big.iso') ?: 0) ->saveTo('/tmp/big.iso') ->get(); // Streaming upload (PUT) + multipart Http::to('https://api.example.com/put') ->data(fopen('/path/1GB.bin','rb')) ->put(); Http::to('https://api.example.com/upload') ->multipart(['file' => Http::file('/path/big.iso')]) ->post();
See docs/LARGE_FILES.md.
HTTP Cache & VCR
$cache = /* Psr\SimpleCache\CacheInterface */; $vcr = new ndtan\Curl\Vcr\Vcr(__DIR__.'/storage/cassettes','record'); $res = Http::to('https://api.example.com/users') ->cache($cache, defaultTtl: 120) // RFC semantics (ETag, Last-Modified) ->vcr($vcr) // record/replay HTTP for tests ->get();
Observability (Timings, Hooks, Otel)
- Timings:
dns
,connect
,tls
,ttfb
,transfer
,total
via$res->timings()
- Hooks:
onRequest
,onResponse
,onRetry
,onRedirect
- OpenTelemetry: instrument via hooks (optional package)
Docs: OBSERVABILITY.md.
Security & TLS
Http::to('https://secure.example.com') ->tls([ 'min' => 'TLSv1.2', 'pinned_pubkey' => 'sha256//...', 'verify_peer' => true, 'verify_host' => 2, ]) ->proxy('http://proxy.local:8080', 'localhost,127.0.0.1') ->get();
Docs: SECURITY.md.
Framework Integrations
Laravel
- Auto‑discovered ServiceProvider:
ndtan\Curl\Integrations\Laravel\NdtCurlServiceProvider
- Publish config:
php artisan vendor:publish --tag=config --provider="ndtan\Curl\Integrations\Laravel\NdtCurlServiceProvider"
Docs: LARAVEL.md
Symfony
Minimal bundle placeholder with service wiring guide.
Docs: SYMFONY.md
PSR18 Adapter
Bring your own PSR‑7 (nyholm/psr7
or guzzlehttp/psr7
), map to builder, and convert back:
$client = new ndtan\Curl\Integrations\PSR18\Client( mapper: function(Psr\Http\Message\RequestInterface $req) { $b = \ndtan\Curl\Http\Http::to((string)$req->getUri())->method($req->getMethod()); foreach ($req->getHeaders() as $k=>$vals) $b->header($k,$vals); $body = (string)$req->getBody(); if ($body !== '') $b->data($body); return $b; }, responseFactory: function(\ndtan\Curl\Http\Response $res) use ($psr7Factory) { $psr = $psr7Factory->createResponse($res->status()); foreach ($res->headers() as $k=>$v) $psr = $psr->withHeader($k, $v); $psr->getBody()->write($res->body()); return $psr; } );
Docs: PSR18.md.
Config Reference
Config file with header comments is provided at config/ndt_curl.php
.
Docs: CONFIG.md.
Examples
You can run the examples with plain PHP after composer install
:
php examples/quick_start.php php examples/json_post.php php examples/file_upload.php php examples/large_download.php php examples/retry_hedge.php php examples/cache_vcr.php php examples/aws_sigv4.php php examples/psr18_client.php
More in examples/
folder.
Testing
composer install vendor/bin/phpunit --testdox
Add coverage:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
License
MIT © Tony Nguyen