Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

267 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Fetch PHP

Latest Version on PackagistCICodecovCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Fetch PHP is a modern HTTP client library for PHP that brings JavaScript's fetch API experience to PHP. Built on top of Guzzle, Fetch PHP allows you to write HTTP code with a clean, intuitive JavaScript-like syntax while still maintaining PHP's familiar patterns.

With support for both synchronous and asynchronous requests, a fluent chainable API, and powerful retry mechanics, Fetch PHP streamlines HTTP operations in your PHP applications.

Full documentation can be found here


Key Features

  • JavaScript-like Syntax: Write HTTP requests just like you would in JavaScript with the fetch() function and async/await patterns
  • Promise-based API: Use familiar .then(), .catch(), and .finally() methods for async operations
  • Fluent Interface: Build requests with a clean, chainable API
  • Built on Guzzle: Benefit from Guzzle's robust functionality with a more elegant API
  • Streaming & Server-Sent Events: Consume response bodies incrementally (response.body-style) and parse text/event-stream responses β€” ideal for streaming LLM APIs and live feeds
  • Middleware Pipeline: PSR-7-based middleware/interceptors for cross-cutting concerns (auth, logging, versioning) with priority ordering and conditional application
  • Lifecycle Events & Hooks: Observe the full request lifecycle (onRequest/onResponse/onError/onRetry/onTimeout/onRedirect) with correlation IDs and prioritised listeners
  • Retry Mechanics: Configurable retry logic with exponential backoff for transient failures
  • RFC 7234 HTTP Caching: Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
  • Connection Pooling: Reuse TCP connections across requests with global connection pool and DNS caching
  • HTTP/2 Support: Native HTTP/2 protocol support for improved performance
  • Debug & Profiling: Built-in debugging and performance profiling capabilities
  • Type-Safe Enums: Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
  • Testing Utilities: Built-in mock responses and request recording for testing
  • PHP-style Helper Functions: Includes traditional PHP function helpers (get(), post(), etc.) for those who prefer that style
  • PSR Compliant: Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards

Why Choose Fetch PHP?

Beyond Guzzle

While Guzzle is a powerful HTTP client, Fetch PHP enhances the experience by providing:

  • JavaScript-like API: Enjoy the familiar fetch() API and async/await patterns from JavaScript
  • Global client management: Configure once, use everywhere with the global client
  • Simplified requests: Make common HTTP requests with less code
  • Enhanced error handling: Reliable retry mechanics and clear error information
  • Type-safe enums: Use enums for HTTP methods, content types, and status codes
FeatureFetch PHPGuzzle
API StyleJavaScript-like fetch + async/await + PHP-style helpersPHP-style only
Client ManagementGlobal client + instance optionsInstance-based only
Request SyntaxClean, minimalMore verbose
TypesModern PHP 8.3+ enumsString constants
Helper FunctionsMultiple styles availableLimited

Installation

composer require jerome/fetch-php

Requirements: PHP 8.3 or higher

Basic Usage

JavaScript-style API (Promise Chaining)

usefunctionMatrix\Support\async;
// JavaScript-like promise chaining in PHPasync(fn() => fetch('https://api.example.com/users'))
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

Or, using the client handler for more control:

$handler = fetch_client()->getHandler();
$handler->async();
$handler->get('https://api.example.com/users')
->then(fn ($response) => $response->json())
->catch(fn ($error) => echo "Error: " . $error->getMessage())
->finally(fn () => echo "Request completed.");

PHP-style Helpers

// GET request with query parameters$response = get('https://api.example.com/users', ['page' => 1, 'limit' => 10]);
// POST request with JSON data$response = post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com'
]);

Fluent API

// Chain methods to build your request$response = fetch_client()
->baseUri('https://api.example.com')
->withHeaders(['Accept' => 'application/json'])
->withToken('your-auth-token')
->withQueryParameters(['page' => 1, 'limit' => 10])
->get('/users');

Async/Await Pattern

Note: The async functions (async, await, all, race, map, batch, retry) are provided by the jerome/matrix library, which is included as a dependency.

Using Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
$response = await(async(fn() => fetch('https://api.example.com/users')));
$users = $response->json();
echo"Fetched " . count($users) . " users";

Multiple Concurrent Requests with Async/Await

// These async functions are provided by the Matrix library dependencyusefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
usefunctionMatrix\Support\all;
// Execute an async functionawait(async(function() {
// Create multiple requests$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts')),
'comments' => async(fn() => fetch('https://api.example.com/comments'))
]));
// Process the results$users = $results['users']->json();
$posts = $results['posts']->json();
$comments = $results['comments']->json();
echo"Fetched " . count($users) . " users, " .
count($posts) . " posts, and " .
count($comments) . " comments";
}));

Sequential Requests with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
await(async(function() {
// First request: get auth token$authResponse = await(async(fn() =>
fetch('https://api.example.com/auth/login', [
'method' => 'POST',
'json' => [
'username' => 'user',
'password' => 'pass'
]
])
));
$token = $authResponse->json()['token'];
// Second request: use token to get user data$userResponse = await(async(fn() =>
fetch('https://api.example.com/me', [
'token' => $token
])
));
return$userResponse->json();
}));

Error Handling with Async/Await

usefunctionMatrix\Support\async;
usefunctionMatrix\Support\await;
try {
$data = await(async(function() {
$response = await(async(fn() =>
fetch('https://api.example.com/users/999')
));
if ($response->isNotFound()) {
thrownew \Exception("User not found");
}
return$response->json();
}));
// Process the data
} catch (\Exception$e) {
echo"Error: " . $e->getMessage();
}

Traditional Promise-based Pattern

// Set up an async request// Get the handler for async operations$handler = fetch_client()->getHandler();
$handler->async();
// Make the async request$promise = $handler->get('https://api.example.com/users');
// Handle the result with callbacks$promise->then(
function ($response) {
// Process successful response$users = $response->json();
foreach ($usersas$user) {
echo$user['name'] . PHP_EOL;
}
},
function ($exception) {
// Handle errorsecho"Error: " . $exception->getMessage();
}
);

Advanced Async Usage

Concurrent Requests with Promise Utilities

usefunctionMatrix\Support\race;
// Create promises for redundant endpoints$promises = [
async(fn() => fetch('https://api1.example.com/data')),
async(fn() => fetch('https://api2.example.com/data')),
async(fn() => fetch('https://api3.example.com/data'))
];
// Get the result from whichever completes first$response = await(race($promises));
$data = $response->json();
echo"Got data from the fastest source";

Controlled Concurrency with Map

usefunctionMatrix\Support\map;
// List of user IDs to fetch$userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process at most 3 requests at a time$responses = await(map($userIds, function($id) {
returnasync(function() use ($id) {
returnfetch("https://api.example.com/users/{$id}");
});
}, 3));
// Process the responsesforeach ($responsesas$index => $response) {
$user = $response->json();
echo"Processed user {$user['name']}\n";
}

Batch Processing

usefunctionMatrix\Support\batch;
// Array of items to process$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Process in batches of 3 with max 2 concurrent batches$results = await(batch(
$items,
function($batch) {
// Process a batchreturnasync(function() use ($batch) {
$batchResults = [];
foreach ($batchas$id) {
$response = await(async(fn() =>
fetch("https://api.example.com/users/{$id}")
));
$batchResults[] = $response->json();
}
return$batchResults;
});
},
3, // batch size2// concurrency
));

With Retries

usefunctionMatrix\Support\retry;
// Retry a flaky request up to 3 times with exponential backoff$data = await(retry(
function() {
returnasync(function() {
returnfetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attemptsfunction($attempt) {
// Exponential backoff strategyreturnmin(pow(2, $attempt) * 100, 1000);
}
));

Advanced Configuration

Automatic Retries

Fetch PHP automatically retries transient failures with exponential backoff.

  • Default: 1 retry attempt (ClientHandler::DEFAULT_RETRIES) with a 100 ms base delay
  • Default delay: 100 ms base with exponential backoff (when retries configured)
  • Retry triggers:
    • Network/connect errors (e.g., ConnectException)
    • HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)

Configure per-request:

$response = fetch_client()
->retry(3, 200) // 3 retries, 200ms base delay
->retryStatusCodes([429, 503]) // optional: customize which statuses retry
->retryExceptions([ConnectException::class]) // optional: customize exception types
->get('https://api.example.com/unstable');

Notes:

  • HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
  • Network failures are retried and, if all attempts fail, throw a Fetch\Exceptions\RequestException.

Authentication

// Basic auth$response = fetch('https://api.example.com/secure', [
'auth' => ['username', 'password']
]);
// Bearer token$response = fetch_client()
->withToken('your-oauth-token')
->get('https://api.example.com/secure');

Proxies

$response = fetch('https://api.example.com', [
'proxy' => 'http://proxy.example.com:8080'
]);
// Or with fluent API$response = fetch_client()
->withProxy('http://proxy.example.com:8080')
->get('https://api.example.com');

Global Client Configuration

// Configure once at application bootstrapfetch_client([
'base_uri' => 'https://api.example.com',
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json',
],
'timeout' => 10,
]);
// Use the configured client throughout your applicationfunctiongetUserData($userId) {
returnfetch_client()->get("/users/{$userId}")->json();
}
functioncreateUser($userData) {
returnfetch_client()->post('/users', $userData)->json();
}

Working with Responses

$response = fetch('https://api.example.com/users/1');
// Check if request was successfulif ($response->successful()) {
// HTTP status codeecho$response->getStatusCode(); // 200// Response body as JSON (returns array by default)$user = $response->json();
// Response body as object$userObject = $response->object();
// Response body as array$userArray = $response->array();
// Response body as string$body = $response->text();
// Get a specific header$contentType = $response->getHeaderLine('Content-Type');
// Check status code categoriesif ($response->isSuccess()) {
echo"Request succeeded (2xx)";
}
if ($response->isOk()) {
echo"Request returned 200 OK";
}
if ($response->isNotFound()) {
echo"Resource not found (404)";
}
}
// ArrayAccess support$name = $response['name']; // Access JSON response data directly// Inspect retry-related statuses explicitly if neededif ($response->getStatusCode() === 429) {
// Handle rate limit response
}
## Working with Type-Safe Enums
```phpuse Fetch\Enum\Method;use Fetch\Enum\ContentType;use Fetch\Enum\Status;// Use enums for HTTP methods$client = fetch_client();$response = $client->request(Method::POST, '/users', $userData);// Check HTTP status with enumsif ($response->statusEnum() === Status::OK) { // Process successful response}// Or use the isStatus helperif ($response->isStatus(Status::OK)) { // Process successful response}// Content type handling$response = $client->withBody($data, ContentType::JSON)->post('/users');

Error Handling

// Synchronous error handlingtry {
$response = fetch('https://api.example.com/nonexistent');
if (!$response->successful()) {
echo"Request failed with status: " . $response->getStatusCode();
}
} catch (\Throwable$e) {
echo"Exception: " . $e->getMessage();
}
// Asynchronous error handling$handler = fetch_client()->getHandler();
$handler->async();
$promise = $handler->get('https://api.example.com/nonexistent')
->then(function ($response) {
if ($response->successful()) {
return$response->json();
}
thrownew \Exception("Request failed with status: " . $response->getStatusCode());
})
->catch(function (\Throwable$e) {
echo"Error: " . $e->getMessage();
});

Timeouts

Control both total request timeout and connection timeout:

$response = fetch('https://api.example.com/data', [
'timeout' => 15, // total request timeout (seconds)'connect_timeout' => 5, // connection timeout (seconds)
]);

If connect_timeout is not provided, it defaults to the timeout value.

Logging and Redaction

When request/response logging is enabled via a logger, sensitive values are redacted:

  • Headers: Authorization, X-API-Key, API-Key, X-Auth-Token, Cookie, Set-Cookie
  • Options: auth credentials

Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.

Caching (sync-only)

Note: Caching is available for synchronous requests only. Async requests intentionally bypass the cache.

Fetch PHP implements RFC 7234-aware HTTP caching with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error support. The default backend is an in-memory cache (MemoryCache), but you can use FileCache or implement your own backend via CacheInterface.

Cache Behavior

  • Cacheable methods by default: GET, HEAD
  • Cacheable status codes: 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
  • Cache-Control headers respected: no-store, no-cache, max-age, s-maxage, etc.
  • Revalidation: Automatically adds If-None-Match (ETag) and If-Modified-Since (Last-Modified) headers for stale entries
  • 304 Not Modified: Merges headers and returns cached body
  • Vary headers: Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)

Basic Cache Setup

useFetch\Cache\MemoryCache;
useFetch\Cache\FileCache;
$handler = fetch_client()->getHandler();
// Enable cache with in-memory backend (default)$handler->withCache();
// Or use file-based cache$handler->withCache(newFileCache('/path/to/cache'));
// Disable cache$handler->withoutCache();
$response = $handler->get('https://api.example.com/users');

Advanced Cache Configuration

$handler->withCache(null, [
'default_ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)'respect_cache_headers' => true, // Honor Cache-Control headers (default: true)'is_shared_cache' => false, // Act as shared cache (respects s-maxage)'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating'stale_if_error' => 300, // Serve stale for 300s if backend fails'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods'cache_status_codes' => [200, 301], // Cacheable status codes
]);

Per-Request Cache Control

// Force a fresh request (bypass cache)$response = $handler->withOptions(['cache' => ['force_refresh' => true]])
->get('https://api.example.com/users');
// Custom TTL for specific request$response = $handler->withOptions(['cache' => ['ttl' => 600]])
->get('https://api.example.com/users');
// Custom cache key$response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
->get('https://api.example.com/users');
// Cache POST/PUT payloads (requires allowing the method globally)$handler->withCache(null, [
'cache_methods' => ['GET', 'HEAD', 'POST'],
]);
$report = $handler->withOptions([
'cache' => [
'ttl' => 120,
'cache_body' => true, // include the JSON body in the cache key
],
])->post('https://api.example.com/reports', ['range' => 'weekly']);
Useful patterns:
- **Force refresh**: set `force_refresh => true` on the request to ignore stored entries.
- **Cache POST/PUT**: allow the verb in `cache_methods` via `withCache()` and set `cache_body => true` so the request body participates in the cache key.- **Static assets**: pin a custom `key` for predictable lookups regardless of URL params.

Connection Pooling & HTTP/2

Connection pooling enables reuse of TCP connections across multiple requests, reducing latency and improving performance. The pool is shared globally across all handler instances, and includes DNS caching for faster lookups.

Enable Connection Pooling

$handler = fetch_client()->getHandler();
// Enable with default settings$handler->withConnectionPool(true);
// Or configure with custom options$handler->withConnectionPool([
'enabled' => true,
'max_connections' => 50, // Total connections across all hosts'max_per_host' => 10, // Max connections per host'max_idle_per_host' => 5, // Idle sockets kept per host'keep_alive_timeout' => 60, // Connection lifetime in seconds'connection_timeout' => 5, // Dial timeout in seconds'dns_cache_ttl' => 300, // DNS cache TTL in seconds'connection_warmup' => false,
'warmup_connections' => 0,
]);

Enable HTTP/2

// Enable HTTP/2 (requires curl with HTTP/2 support)$handler->withHttp2(true);
// Or configure with options$handler->withHttp2([
'enabled' => true,
// Additional HTTP/2 configuration options...
]);

Pool Management

// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate// Close all active connections$handler->closeAllConnections();
// Reset pool and DNS cache (useful for testing)$handler->resetPool();

Note: The connection pool is static/global and shared across all handlers. Call resetPool() in your test teardown to ensure isolation between tests.

Debugging & Profiling

Enable debug snapshots and optional profiling:

$handler = fetch_client()->getHandler();
// Enable debug with default options (captures everything)$handler->withDebug();
// Or enable with specific options$handler->withDebug([
'request_headers' => true,
'request_body' => true,
'response_headers' => true,
'response_body' => 1024, // Truncate response body at 1024 bytes'timing' => true,
'memory' => true,
'dns_resolution' => true,
]);
// Enable profiling$handler->withProfiler(new \Fetch\Support\FetchProfiler);
// Set log level (requires PSR-3 logger to be configured)$handler->withLogLevel('info'); // default: debug$response = $handler->get('https://api.example.com/users');
// Preferred: read per-response debug snapshot$responseDebug = $response->getDebugInfo();
// Legacy fallback for BC: handler-level snapshot (may lag in concurrent flows)$lastDebug = $handler->getLastDebugInfo();

Testing Support

Fetch PHP includes built-in testing utilities for mocking HTTP responses:

useFetch\Testing\MockServer;
useFetch\Testing\MockResponse;
// Mock a single response
MockServer::fake([
'GET https://api.example.com/users/1' => MockResponse::json([
'id' => 1,
'name' => 'Ada Lovelace',
]),
]);
$response = fetch('https://api.example.com/users/1');
// Returns mocked response without making an actual HTTP request
MockServer::assertSent('GET https://api.example.com/users/1');
// Mock a sequence of responses
MockServer::fake([
'https://api.example.com/users/*' => MockResponse::sequence([
MockResponse::json(['id' => 1]),
MockResponse::json(['id' => 2]),
MockResponse::notFound(),
]),
]);
fetch('https://api.example.com/users/alpha'); // gets id 1fetch('https://api.example.com/users/beta'); // gets id 2fetch('https://api.example.com/users/omega'); // 404 from sequence

Advanced Response Features

Response Status Checks

$response = fetch('https://api.example.com/data');
// Status category checks$response->isInformational(); // 1xx$response->isSuccess(); // 2xx$response->isRedirection(); // 3xx$response->isClientError(); // 4xx$response->isServerError(); // 5xx// Specific status checks$response->isOk(); // 200$response->isCreated(); // 201$response->isNoContent(); // 204$response->isNotFound(); // 404$response->isForbidden(); // 403$response->isUnauthorized(); // 401// Generic status check$response->isStatus(Status::CREATED);
$response->isStatus(201);

Response Helpers

// Check if response contains JSONif ($response->isJson()) {
$data = $response->json();
}
// Get response as different types with error handling$data = $response->json(assoc: true, throwOnError: false);
$object = $response->object(throwOnError: false);
$array = $response->array(throwOnError: false);

Connection Pool Management

Clean up connections or reset the pool (useful in tests):

$handler = fetch_client()->getHandler();
// Close all active connections$handler->closeAllConnections();
// Reset the entire pool and DNS cache (useful in tests)$handler->resetPool();
// Get pool statistics$stats = $handler->getPoolStats();
// Returns: connections_created, connections_reused, total_requests, average_latency, reuse_rate

Async Notes

  • Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
  • Matrix helpers (async, await, all, race, map, batch, retry) are re-exported in Fetch\Support\helpers.php.
  • Errors are wrapped with method/URL context while preserving the original exception chain.
  • Use $handler->async() to enable async mode, or use the Matrix async utilities directly.

License

This project is licensed under the MIT License – see the LICENSE file for full terms.

The MIT License allows you to:

  • Use the software for any purpose, including commercial applications
  • Modify and distribute the software
  • Include it in proprietary software
  • Use it without warranty or liability concerns

This permissive license encourages adoption while maintaining attribution requirements.

Contributing

Contributions are welcome! We're currently looking for help with:

  • Expanding test coverage
  • Improving documentation
  • Adding support for additional HTTP features

To contribute:

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/amazing-feature)
  3. Commit your Changes (git commit -m 'Add some amazing-feature')
  4. Push to the Branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

  • Thanks to Guzzle HTTP for providing the underlying HTTP client
  • Thanks to all contributors who have helped improve this package
  • Special thanks to the PHP community for their support and feedback

About

πŸš€ Modern PHP HTTP client inspired by JavaScript's fetch API. Async/await, streaming, SSE, middleware, RFC 7234 caching, and full PSR-7/18 compliance.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

449 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages