Skip to content

Commit 5545ed6

Browse files
committed
Refactor to v2: Middleware architecture and fluent API
Major architectural improvements: - Introduce middleware pipeline architecture (LoggingMiddleware, TimeoutMiddleware, RetryMiddleware) - Implement fluent RequestBuilder API for chainable request construction - Add advanced retry strategies with exponential backoff and jitter - Create comprehensive exception hierarchy (NetworkException, TimeoutException, JsonParseException, etc.) - Add immutable TransportConfig for thread-safe configuration - Implement RetryContext and RetryCondition for intelligent retry logic - Enhance ResponseInterface with json(), jsonOrNull(), and toArray() methods - Remove deprecated ClientInterface, RequestInterface, and related traits - Update to Pest v2 for testing - Comprehensive README with usage examples and architecture documentation Breaking changes: - Configuration is now immutable and set during build phase - Removed mutable setters in favor of builder pattern - Changed ClientInterface to use PSR-18 directly
1 parent 48a120f commit 5545ed6

41 files changed

Lines changed: 2805 additions & 934 deletions

Some content is hidden

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

README.md

Lines changed: 316 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,24 @@
55
[![codecov](https://codecov.io/gh/farzai/transport-php/branch/main/graph/badge.svg)](https://codecov.io/gh/farzai/transport-php)
66
[![Total Downloads](https://img.shields.io/packagist/dt/farzai/transport.svg?style=flat-square)](https://packagist.org/packages/farzai/transport)
77

8-
A HTTP client for Farzai Package.
8+
A modern, PSR-compliant HTTP client for PHP with middleware architecture, advanced retry strategies, and fluent API for building requests.
99

10+
## Features
11+
12+
-**PSR-7** HTTP Message Interface
13+
-**PSR-18** HTTP Client Interface
14+
-**PSR-3** Logger Interface support
15+
-**Middleware Architecture** - Extensible plugin system for custom behavior
16+
-**Advanced Retry Strategies** - Exponential backoff with jitter, custom retry conditions
17+
-**Fluent Request Builder** - Chainable API for building requests
18+
-**Immutable Configuration** - Thread-safe, predictable behavior
19+
-**Type-Safe** - Full PHP 8.1+ type hints and strict types
20+
-**JSON Helpers** - Parse JSON with proper error handling
21+
-**Custom Exceptions** - Detailed error context and retry information
22+
23+
## Requirements
24+
25+
- PHP 8.1 or higher
1026

1127
## Installation
1228

@@ -16,12 +32,311 @@ You can install the package via composer:
1632
composer require farzai/transport
1733
```
1834

35+
## Usage
36+
37+
### Basic Usage (Fluent API)
38+
39+
```php
40+
use Farzai\Transport\TransportBuilder;
41+
42+
// Create a transport client with configuration
43+
$transport = TransportBuilder::make()
44+
->withBaseUri('https://api.example.com')
45+
->withHeaders([
46+
'Authorization' => 'Bearer token123',
47+
'Accept' => 'application/json',
48+
])
49+
->withTimeout(30)
50+
->build();
51+
52+
// Make requests using fluent API
53+
$response = $transport->get('/users/123')->send();
54+
55+
// Access response data
56+
echo $response->statusCode(); // 200
57+
echo $response->body(); // Raw response body
58+
$data = $response->json(); // Parsed JSON as array
59+
```
60+
61+
### Fluent Request Building
62+
63+
```php
64+
// GET request with query parameters
65+
$response = $transport
66+
->get('/users')
67+
->withQuery(['page' => 1, 'limit' => 10])
68+
->withHeader('X-Custom-Header', 'value')
69+
->send();
70+
71+
// POST with JSON body
72+
$response = $transport
73+
->post('/users')
74+
->withJson([
75+
'name' => 'John Doe',
76+
'email' => 'john@example.com'
77+
])
78+
->send();
79+
80+
// POST with form data
81+
$response = $transport
82+
->post('/login')
83+
->withForm([
84+
'username' => 'john',
85+
'password' => 'secret'
86+
])
87+
->send();
88+
89+
// With authentication
90+
$response = $transport
91+
->get('/protected')
92+
->withBearerToken('your-token')
93+
->send();
94+
95+
// Or basic auth
96+
$response = $transport
97+
->get('/protected')
98+
->withBasicAuth('username', 'password')
99+
->send();
100+
```
101+
102+
### Working with JSON Responses
103+
104+
```php
105+
// Parse JSON with automatic error handling
106+
$data = $response->json(); // ['id' => 123, 'name' => 'John Doe']
107+
108+
// Get specific field using dot notation
109+
$name = $response->json('name'); // 'John Doe'
110+
$city = $response->json('user.address.city'); // 'New York'
111+
112+
// Get as array
113+
$array = $response->toArray();
114+
115+
// Safe JSON parsing (returns null on error instead of throwing)
116+
$data = $response->jsonOrNull();
117+
118+
// Check if response is successful
119+
if ($response->isSuccessful()) {
120+
// Handle success (2xx status codes)
121+
}
122+
```
123+
124+
### Advanced Retry Logic
125+
126+
```php
127+
use Farzai\Transport\Retry\ExponentialBackoffStrategy;
128+
use Farzai\Transport\Retry\RetryCondition;
129+
use Farzai\Transport\Exceptions\NetworkException;
130+
131+
// Configure retry with exponential backoff
132+
$transport = TransportBuilder::make()
133+
->withRetries(
134+
maxRetries: 3,
135+
strategy: new ExponentialBackoffStrategy(
136+
baseDelayMs: 1000, // Start with 1 second
137+
multiplier: 2.0, // Double each retry
138+
maxDelayMs: 30000, // Cap at 30 seconds
139+
useJitter: true // Add randomization
140+
),
141+
condition: RetryCondition::default()
142+
->onExceptions([NetworkException::class])
143+
)
144+
->build();
145+
146+
// Retries automatically with exponential backoff + jitter
147+
$response = $transport->get('/unreliable-endpoint')->send();
148+
```
149+
150+
### Custom Middleware
151+
152+
```php
153+
use Farzai\Transport\Middleware\MiddlewareInterface;
154+
use Psr\Http\Message\RequestInterface;
155+
use Psr\Http\Message\ResponseInterface;
156+
157+
// Create custom middleware
158+
class AuthMiddleware implements MiddlewareInterface
159+
{
160+
public function handle(RequestInterface $request, callable $next): ResponseInterface
161+
{
162+
// Add auth token to all requests
163+
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getToken());
164+
165+
return $next($request);
166+
}
167+
168+
private function getToken(): string
169+
{
170+
// Your token logic
171+
return 'your-token';
172+
}
173+
}
174+
175+
// Add to transport
176+
$transport = TransportBuilder::make()
177+
->withMiddleware(new AuthMiddleware())
178+
->build();
179+
```
180+
181+
### Configuration Options
182+
183+
```php
184+
$transport = TransportBuilder::make()
185+
->withBaseUri('https://api.example.com')
186+
->withHeaders(['Accept' => 'application/json'])
187+
->withTimeout(30) // Seconds
188+
->withRetries(3) // Max retry attempts
189+
->withMiddleware($customMiddleware)
190+
->withoutDefaultMiddlewares() // Disable logging, timeout, retry middlewares
191+
->setClient($customPsrClient) // Use custom PSR-18 client
192+
->setLogger($customLogger) // Use custom PSR-3 logger
193+
->build();
194+
```
195+
196+
### Error Handling
197+
198+
```php
199+
use Farzai\Transport\Exceptions\RetryExhaustedException;
200+
use Farzai\Transport\Exceptions\JsonParseException;
201+
use Farzai\Transport\Exceptions\NetworkException;
202+
203+
// Throw exception on non-2xx responses
204+
try {
205+
$response->throw();
206+
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
207+
echo $e->getMessage();
208+
}
209+
210+
// Custom error handling callback
211+
$response->throw(function ($response, $exception) {
212+
if ($response->statusCode() === 404) {
213+
throw new \Exception('Resource not found!');
214+
}
215+
throw $exception;
216+
});
217+
218+
// Handle retry exhaustion
219+
try {
220+
$response = $transport->get('/flaky-endpoint')->send();
221+
} catch (RetryExhaustedException $e) {
222+
echo "Failed after {$e->getAttempts()} attempts\n";
223+
echo "Delays used: " . implode(', ', $e->getDelaysUsed()) . "ms\n";
224+
225+
foreach ($e->getRetryExceptions() as $attempt => $exception) {
226+
echo "Attempt $attempt: {$exception->getMessage()}\n";
227+
}
228+
}
229+
230+
// Handle JSON parse errors
231+
try {
232+
$data = $response->json();
233+
} catch (JsonParseException $e) {
234+
echo "Invalid JSON: {$e->getMessage()}\n";
235+
echo "JSON string: {$e->jsonString}\n";
236+
echo "Error code: {$e->jsonErrorCode}\n";
237+
}
238+
```
239+
240+
### Using Custom PSR-18 Client and Logger
241+
242+
```php
243+
use Farzai\Transport\TransportBuilder;
244+
use Monolog\Logger;
245+
use Monolog\Handler\StreamHandler;
246+
247+
// Create custom logger
248+
$logger = new Logger('http-client');
249+
$logger->pushHandler(new StreamHandler('path/to/your.log'));
250+
251+
// Use any PSR-18 compliant client
252+
$client = new \Your\Custom\Psr18Client();
253+
254+
$transport = TransportBuilder::make()
255+
->setClient($client)
256+
->setLogger($logger)
257+
->build();
258+
```
259+
260+
### Testing with Response Builder
261+
262+
```php
263+
use Farzai\Transport\ResponseBuilder;
264+
265+
$response = ResponseBuilder::create()
266+
->statusCode(200)
267+
->withHeader('Content-Type', 'application/json')
268+
->withBody('{"success": true}')
269+
->build();
270+
271+
// Or use the fluent builder methods
272+
$response = ResponseBuilder::create()
273+
->statusCode(404)
274+
->withHeaders([
275+
'Content-Type' => 'application/json',
276+
'X-Request-ID' => '12345'
277+
])
278+
->withBody('{"error": "Not found"}')
279+
->withVersion('1.1')
280+
->withReason('Not Found')
281+
->build();
282+
```
283+
284+
## Architecture
285+
286+
### Middleware System
287+
288+
The library uses a middleware pipeline architecture that allows you to easily extend functionality:
289+
290+
```
291+
Request → Middleware Stack → HTTP Client → Response
292+
293+
[LoggingMiddleware]
294+
[TimeoutMiddleware]
295+
[RetryMiddleware]
296+
[CustomMiddleware...]
297+
```
298+
299+
Default middlewares (can be disabled with `withoutDefaultMiddlewares()`):
300+
- **LoggingMiddleware**: Logs requests and responses
301+
- **TimeoutMiddleware**: Enforces request timeouts
302+
- **RetryMiddleware**: Handles retry logic with configurable strategies
303+
304+
### Immutable Configuration
305+
306+
All configuration is immutable and set during the build phase:
307+
308+
```php
309+
// ✅ Correct - configuration during build
310+
$transport = TransportBuilder::make()
311+
->withTimeout(30)
312+
->withRetries(3)
313+
->build();
314+
315+
// ❌ No longer available - no mutable setters
316+
// $transport->setTimeout(30); // Method doesn't exist
317+
```
318+
319+
This makes the Transport instance:
320+
- **Thread-safe** - Can be safely shared across threads
321+
- **Predictable** - Configuration can't change unexpectedly
322+
- **Easier to test** - No hidden state changes
323+
19324
## Testing
20325

21326
```bash
22327
composer test
23328
```
24329

330+
## Code Quality
331+
332+
```bash
333+
# Run tests with coverage
334+
composer test-coverage
335+
336+
# Fix code style
337+
composer format
338+
```
339+
25340
## Changelog
26341

27342
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
},
2424
"require-dev": {
2525
"laravel/pint": "^1.2",
26-
"pestphp/pest": "^1.20",
26+
"mockery/mockery": "^1.6",
27+
"pestphp/pest": "^2.0",
2728
"spatie/ray": "^1.28"
2829
},
2930
"autoload": {

src/Contracts/ClientInterface.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Contracts/RequestInterface.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)