Skip to content

Commit 650429d

Browse files
committed
refactor: tests and enhance functionality
1 parent baf90dd commit 650429d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+10456
-1404
lines changed

README.md

Lines changed: 286 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

337344
Configure 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

346354
Notes:
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
428452
if ($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

502531
Logged 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

506779
This project is licensed under the **MIT License** – see the [LICENSE](LICENSE) file for full terms.

docs/.vitepress/config.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,23 @@ export default defineConfig({
199199
text: "Retry Handling",
200200
link: "/guide/retry-handling",
201201
},
202+
{
203+
text: "HTTP Caching",
204+
link: "/guide/http-caching",
205+
},
206+
{
207+
text: "Connection Pooling & HTTP/2",
208+
link: "/guide/connection-pooling",
209+
},
202210
{ text: "File Uploads", link: "/guide/file-uploads" },
203211
{
204212
text: "Custom Clients",
205213
link: "/guide/custom-clients",
206214
},
215+
{
216+
text: "Debugging & Profiling",
217+
link: "/guide/debugging-and-profiling",
218+
},
207219
{ text: "Testing with Mocks", link: "/guide/testing" },
208220
],
209221
},

0 commit comments

Comments
 (0)