@@ -24,8 +24,15 @@ Full documentation can be found [here](https://fetch-php.thavarshan.com/)
2424- ** Promise-based API** : Use familiar ` .then() ` , ` .catch() ` , and ` .finally() ` methods for async operations
2525- ** Fluent Interface** : Build requests with a clean, chainable API
2626- ** Built on Guzzle** : Benefit from Guzzle's robust functionality with a more elegant API
27- - ** Retry Mechanics** : Automatically retry failed requests with exponential backoff
27+ - ** Retry Mechanics** : Configurable retry logic with exponential backoff for transient failures
28+ - ** RFC 7234 HTTP Caching** : Full caching support with ETag/Last-Modified revalidation, stale-while-revalidate, and stale-if-error
29+ - ** Connection Pooling** : Reuse TCP connections across requests with global connection pool and DNS caching
30+ - ** HTTP/2 Support** : Native HTTP/2 protocol support for improved performance
31+ - ** Debug & Profiling** : Built-in debugging and performance profiling capabilities
32+ - ** Type-Safe Enums** : Modern PHP 8.3+ enums for HTTP methods, content types, and status codes
33+ - ** Testing Utilities** : Built-in mock responses and request recording for testing
2834- ** PHP-style Helper Functions** : Includes traditional PHP function helpers (` get() ` , ` post() ` , etc.) for those who prefer that style
35+ - ** PSR Compliant** : Implements PSR-7 (HTTP Messages), PSR-18 (HTTP Client), and PSR-3 (Logger) standards
2936
3037## Why Choose Fetch PHP?
3138
@@ -326,27 +333,28 @@ $data = await(retry(
326333
327334### Automatic Retries
328335
329- Fetch PHP automatically retries transient failures with exponential backoff and jitter .
336+ Fetch PHP automatically retries transient failures with exponential backoff.
330337
331- - Default attempts: initial try + 1 retry (configurable )
332- - Default delay: 100ms base with exponential backoff and jitter
338+ - Default: No retries enabled (set ` maxRetries ` to null by default )
339+ - Default delay: 100ms base with exponential backoff (when retries configured)
333340- Retry triggers:
334- - Network/connect errors (e.g., timeouts, DNS, connection refused )
335- - HTTP status codes such as 408, 429, 500, 502, 503, 504 (customizable)
341+ - Network/connect errors (e.g., ConnectException )
342+ - HTTP status codes: 408, 429, 500, 502, 503, 504, 507, 509, 520-523, 525, 527, 530 (customizable)
336343
337344Configure per-request:
338345
339346``` php
340347$response = fetch_client()
341348 ->retry(3, 200) // 3 retries, 200ms base delay
342349 ->retryStatusCodes([429, 503]) // optional: customize which statuses retry
350+ ->retryExceptions([ConnectException::class]) // optional: customize exception types
343351 ->get('https://api.example.com/unstable');
344352```
345353
346354Notes:
347355
348356- HTTP error statuses do not throw; you receive the response. Retries happen internally when configured.
349- - Network failures are retried and, if all attempts fail, throw a ` Fetch\\ Exceptions\ \RequestException ` .
357+ - Network failures are retried and, if all attempts fail, throw a ` Fetch\Exceptions\RequestException ` .
350358
351359### Authentication
352360
@@ -408,21 +416,37 @@ if ($response->successful()) {
408416 // HTTP status code
409417 echo $response->getStatusCode(); // 200
410418
411- // Response body as JSON
419+ // Response body as JSON (returns array by default)
412420 $user = $response->json();
413421
422+ // Response body as object
423+ $userObject = $response->object();
424+
425+ // Response body as array
426+ $userArray = $response->array();
427+
414428 // Response body as string
415- $body = $response->getBody()->getContents ();
429+ $body = $response->text ();
416430
417431 // Get a specific header
418432 $contentType = $response->getHeaderLine('Content-Type');
419433
420434 // Check status code categories
421- if ($response->statusEnum()->isSuccess()) {
422- echo "Request succeeded";
435+ if ($response->isSuccess()) {
436+ echo "Request succeeded (2xx)";
437+ }
438+
439+ if ($response->isOk()) {
440+ echo "Request returned 200 OK";
441+ }
442+
443+ if ($response->isNotFound()) {
444+ echo "Resource not found (404)";
423445 }
424446}
425- ```
447+
448+ // ArrayAccess support
449+ $name = $response['name']; // Access JSON response data directly
426450
427451// Inspect retry-related statuses explicitly if needed
428452if ($response->getStatusCode() === 429) {
@@ -441,7 +465,12 @@ $client = fetch_client();
441465$response = $client->request(Method::POST, '/users', $userData);
442466
443467// Check HTTP status with enums
444- if ($response->getStatus() === Status::OK) {
468+ if ($response->statusEnum() === Status::OK) {
469+ // Process successful response
470+ }
471+
472+ // Or use the isStatus helper
473+ if ($response->isStatus(Status::OK)) {
445474 // Process successful response
446475}
447476
@@ -501,6 +530,250 @@ When request/response logging is enabled via a logger, sensitive values are reda
501530
502531Logged context includes method, URI, selected options (sanitized), status code, duration, and content length.
503532
533+ ## Caching (sync-only)
534+
535+ > ** Note:** Caching is available for synchronous requests only. Async requests intentionally bypass the cache.
536+
537+ 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 ` .
538+
539+ ### Cache Behavior
540+
541+ - ** Cacheable methods by default** : ` GET ` , ` HEAD `
542+ - ** Cacheable status codes** : 200, 203, 204, 206, 300, 301, 404, 410 (RFC 7234 defaults)
543+ - ** Cache-Control headers respected** : ` no-store ` , ` no-cache ` , ` max-age ` , ` s-maxage ` , etc.
544+ - ** Revalidation** : Automatically adds ` If-None-Match ` (ETag) and ` If-Modified-Since ` (Last-Modified) headers for stale entries
545+ - ** 304 Not Modified** : Merges headers and returns cached body
546+ - ** Vary headers** : Supports cache variance by headers (default: Accept, Accept-Encoding, Accept-Language)
547+
548+ ### Basic Cache Setup
549+
550+ ``` php
551+ use Fetch\Cache\MemoryCache;
552+ use Fetch\Cache\FileCache;
553+
554+ $handler = fetch_client()->getHandler();
555+
556+ // Enable cache with in-memory backend (default)
557+ $handler->withCache();
558+
559+ // Or use file-based cache
560+ $handler->withCache(new FileCache('/path/to/cache'));
561+
562+ // Disable cache
563+ $handler->withoutCache();
564+
565+ $response = $handler->get('https://api.example.com/users');
566+ ```
567+
568+ ### Advanced Cache Configuration
569+
570+ ``` php
571+ $handler->withCache(null, [
572+ 'ttl' => 3600, // Default TTL in seconds (overridden by Cache-Control)
573+ 'respect_headers' => true, // Respect Cache-Control headers (default: true)
574+ 'is_shared_cache' => false, // Act as shared cache (respects s-maxage)
575+ 'stale_while_revalidate' => 60, // Serve stale for 60s while revalidating
576+ 'stale_if_error' => 300, // Serve stale for 300s if backend fails
577+ 'vary_headers' => ['Accept', 'Accept-Language'], // Headers to vary cache by
578+ 'cache_methods' => ['GET', 'HEAD'], // Cacheable HTTP methods
579+ 'cache_status_codes' => [200, 301], // Cacheable status codes
580+ 'force_refresh' => false, // Bypass cache and force fresh request
581+ ]);
582+ ```
583+
584+ ### Per-Request Cache Control
585+
586+ ``` php
587+ // Force a fresh request (bypass cache)
588+ $response = $handler->withOptions(['cache' => ['force_refresh' => true]])
589+ ->get('https://api.example.com/users');
590+
591+ // Custom TTL for specific request
592+ $response = $handler->withOptions(['cache' => ['ttl' => 600]])
593+ ->get('https://api.example.com/users');
594+
595+ // Custom cache key
596+ $response = $handler->withOptions(['cache' => ['key' => 'custom:users']])
597+ ->get('https://api.example.com/users');
598+ ```
599+
600+ ## Connection Pooling & HTTP/2
601+
602+ 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.
603+
604+ ### Enable Connection Pooling
605+
606+ ``` php
607+ $handler = fetch_client()->getHandler();
608+
609+ // Enable with default settings
610+ $handler->withConnectionPool(true);
611+
612+ // Or configure with custom options
613+ $handler->withConnectionPool([
614+ 'enabled' => true,
615+ 'max_connections' => 50, // Total connections across all hosts
616+ 'max_per_host' => 10, // Max connections per host
617+ 'connection_ttl' => 60, // Connection lifetime in seconds
618+ 'idle_timeout' => 30, // Idle connection timeout in seconds
619+ 'dns_cache_ttl' => 300, // DNS cache TTL in seconds
620+ ]);
621+ ```
622+
623+ ### Enable HTTP/2
624+
625+ ``` php
626+ // Enable HTTP/2 (requires curl with HTTP/2 support)
627+ $handler->withHttp2(true);
628+
629+ // Or configure with options
630+ $handler->withHttp2([
631+ 'enabled' => true,
632+ // Additional HTTP/2 configuration options...
633+ ]);
634+ ```
635+
636+ ### Pool Management
637+
638+ ``` php
639+ // Get pool statistics
640+ $stats = $handler->getPoolStats();
641+ // Returns: connections_created, connections_reused, total_requests, total_latency_ms
642+
643+ // Close all active connections
644+ $handler->closeAllConnections();
645+
646+ // Reset pool and DNS cache (useful for testing)
647+ $handler->resetPool();
648+ ```
649+
650+ > ** Note** : The connection pool is static/global and shared across all handlers. Call ` resetPool() ` in your test teardown to ensure isolation between tests.
651+
652+ ## Debugging & Profiling
653+
654+ Enable debug snapshots and optional profiling:
655+
656+ ``` php
657+ $handler = fetch_client()->getHandler();
658+
659+ // Enable debug with default options (captures everything)
660+ $handler->withDebug();
661+
662+ // Or enable with specific options
663+ $handler->withDebug([
664+ 'request_headers' => true,
665+ 'request_body' => true,
666+ 'response_headers' => true,
667+ 'response_body' => 1024, // Truncate response body at 1024 bytes
668+ 'timing' => true,
669+ 'memory' => true,
670+ 'dns_resolution' => true,
671+ ]);
672+
673+ // Enable profiling
674+ $handler->withProfiler(new \Fetch\Support\FetchProfiler);
675+
676+ // Set log level (requires PSR-3 logger to be configured)
677+ $handler->withLogLevel('info'); // default: debug
678+
679+ $response = $handler->get('https://api.example.com/users');
680+
681+ // Get debug info from last request
682+ $debug = $handler->getLastDebugInfo();
683+ ```
684+
685+ ## Testing Support
686+
687+ Fetch PHP includes built-in testing utilities for mocking HTTP responses:
688+
689+ ``` php
690+ use Fetch\Testing\MockResponse;
691+ use Fetch\Testing\MockResponseSequence;
692+
693+ // Mock a single response
694+ $handler = fetch_client()->getHandler();
695+ $handler->mock(MockResponse::make(['id' => 1, 'name' => 'John'], 200));
696+
697+ $response = $handler->get('https://api.example.com/users/1');
698+ // Returns mocked response without making actual HTTP request
699+
700+ // Mock a sequence of responses
701+ $sequence = new MockResponseSequence([
702+ MockResponse::make(['id' => 1], 200),
703+ MockResponse::make(['id' => 2], 200),
704+ MockResponse::make(null, 404),
705+ ]);
706+
707+ $handler->mock($sequence);
708+ // Each subsequent request returns the next response in sequence
709+ ```
710+
711+ ## Advanced Response Features
712+
713+ ### Response Status Checks
714+
715+ ``` php
716+ $response = fetch('https://api.example.com/data');
717+
718+ // Status category checks
719+ $response->isInformational(); // 1xx
720+ $response->isSuccess(); // 2xx
721+ $response->isRedirection(); // 3xx
722+ $response->isClientError(); // 4xx
723+ $response->isServerError(); // 5xx
724+
725+ // Specific status checks
726+ $response->isOk(); // 200
727+ $response->isCreated(); // 201
728+ $response->isNoContent(); // 204
729+ $response->isNotFound(); // 404
730+ $response->isForbidden(); // 403
731+ $response->isUnauthorized(); // 401
732+
733+ // Generic status check
734+ $response->isStatus(Status::CREATED);
735+ $response->isStatus(201);
736+ ```
737+
738+ ### Response Helpers
739+
740+ ``` php
741+ // Check if response contains JSON
742+ if ($response->isJson()) {
743+ $data = $response->json();
744+ }
745+
746+ // Get response as different types with error handling
747+ $data = $response->json(assoc: true, throwOnError: false);
748+ $object = $response->object(throwOnError: false);
749+ $array = $response->array(throwOnError: false);
750+ ```
751+
752+ ## Connection Pool Management
753+
754+ Clean up connections or reset the pool (useful in tests):
755+
756+ ``` php
757+ $handler = fetch_client()->getHandler();
758+
759+ // Close all active connections
760+ $handler->closeAllConnections();
761+
762+ // Reset the entire pool and DNS cache (useful in tests)
763+ $handler->resetPool();
764+
765+ // Get pool statistics
766+ $stats = $handler->getPoolStats();
767+ // Returns: connections_created, connections_reused, total_requests, total_latency_ms
768+ ```
769+
770+ ## Async Notes
771+
772+ - Async requests use the same pipeline (mocking, profiling, logging) but bypass caching by design.
773+ - Matrix helpers (` async ` , ` await ` , ` all ` , ` race ` , ` map ` , ` batch ` , ` retry ` ) are re-exported in ` Fetch\Support\helpers.php ` .
774+ - Errors are wrapped with method/URL context while preserving the original exception chain.
775+ - Use ` $handler->async() ` to enable async mode, or use the Matrix async utilities directly.
776+
504777## License
505778
506779This project is licensed under the ** MIT License** – see the [ LICENSE] ( LICENSE ) file for full terms.
0 commit comments