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.

dev-main 2025-09-21 13:04 UTC

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.

PHP License Status Type PSR

CI

Table of Contents

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();

Docs: CACHE.md, VCR.md.

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