farzai / transport
A HTTP client for Farzai Package
Fund package maintenance!
parsilver
Installs: 6 369
Dependents: 3
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 1
pkg:composer/farzai/transport
Requires
- php: ^8.1
- farzai/support: ^1.4
- nyholm/psr7: ^1.8
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- laravel/pint: ^1.2
- mockery/mockery: ^1.6
- pestphp/pest: ^2.30
- phpstan/phpstan: ^1.10
- spatie/ray: ^1.28
- symfony/http-client: ^6.0|^7.0
Suggests
- guzzlehttp/guzzle: Popular HTTP client implementation with extensive features
- symfony/http-client: Modern HTTP client with async support and HTTP/2
README
A modern, PSR-compliant HTTP client for PHP with middleware architecture, advanced retry strategies, and fluent API for building requests.
Features
- ✅ PSR Standards - Built on PSR-7 (HTTP Messages), PSR-17 (HTTP Factories), PSR-18 (HTTP Client)
- ✅ No Hard Dependencies - Use any PSR-18 HTTP client (Guzzle, Symfony, or custom)
- ✅ Auto-Detection - Automatically discovers and uses available HTTP clients
- ✅ Middleware Architecture - Extensible plugin system for custom behavior
- ✅ Advanced Retry Strategies - Exponential backoff with jitter, custom retry conditions
- ✅ Fluent Request Builder - Chainable API for building requests
- ✅ Immutable Configuration - Thread-safe, predictable behavior
- ✅ Type-Safe - Full PHP 8.1+ type hints and strict types
- ✅ JSON Helpers - Parse JSON with proper error handling and dot-notation access
- ✅ Custom Exceptions - Detailed error context implementing PSR standards
- ✅ Easy to Swap HTTP Clients - Switch between Guzzle, Symfony, or any PSR-18 client
- ✅ File Upload & Multipart - RFC 7578 compliant multipart/form-data with file upload support
- ✅ Cookie Management - RFC 6265 compliant automatic cookie handling with session support
Requirements
- PHP 8.1 or higher
Installation
You can install the package via composer:
composer require farzai/transport
The library will auto-detect any available PSR-18 HTTP client. If you don't have one installed, we recommend:
# Recommended: Modern HTTP client with async support composer require symfony/http-client # Alternative: Popular and widely-used composer require guzzlehttp/guzzle
Quick Start
Transport PHP automatically detects available HTTP clients (Symfony, Guzzle, etc.) - no configuration needed!
use Farzai\Transport\TransportBuilder; // Just works! Auto-detects your HTTP client $transport = TransportBuilder::make() ->withBaseUri('https://api.example.com') ->build(); $response = $transport->get('/users')->send(); echo $response->json('data.0.name'); // Dot notation support!
Usage
Basic Usage (Fluent API)
use Farzai\Transport\TransportBuilder; // Create a transport client with configuration $transport = TransportBuilder::make() ->withBaseUri('https://api.example.com') ->withHeaders([ 'Authorization' => 'Bearer token123', 'Accept' => 'application/json', ]) ->withTimeout(30) ->build(); // Make requests using fluent API $response = $transport->get('/users/123')->send(); // Access response data echo $response->statusCode(); // 200 echo $response->body(); // Raw response body $data = $response->json(); // Parsed JSON as array
Fluent Request Building
// GET request with query parameters $response = $transport ->get('/users') ->withQuery(['page' => 1, 'limit' => 10]) ->withHeader('X-Custom-Header', 'value') ->send(); // POST with JSON body $response = $transport ->post('/users') ->withJson([ 'name' => 'John Doe', 'email' => 'john@example.com' ]) ->send(); // POST with form data $response = $transport ->post('/login') ->withForm([ 'username' => 'john', 'password' => 'secret' ]) ->send(); // With authentication $response = $transport ->get('/protected') ->withBearerToken('your-token') ->send(); // Or basic auth $response = $transport ->get('/protected') ->withBasicAuth('username', 'password') ->send();
Working with JSON Responses
// Parse JSON with automatic error handling $data = $response->json(); // ['id' => 123, 'name' => 'John Doe'] // Get specific field using dot notation $name = $response->json('name'); // 'John Doe' $city = $response->json('user.address.city'); // 'New York' // Get as array $array = $response->toArray(); // Safe JSON parsing (returns null on error instead of throwing) $data = $response->jsonOrNull(); // Check if response is successful if ($response->isSuccessful()) { // Handle success (2xx status codes) }
Advanced Retry Logic
use Farzai\Transport\Retry\ExponentialBackoffStrategy; use Farzai\Transport\Retry\RetryCondition; use Farzai\Transport\Exceptions\NetworkException; // Configure retry with exponential backoff $transport = TransportBuilder::make() ->withRetries( maxRetries: 3, strategy: new ExponentialBackoffStrategy( baseDelayMs: 1000, // Start with 1 second multiplier: 2.0, // Double each retry maxDelayMs: 30000, // Cap at 30 seconds useJitter: true // Add randomization ), condition: RetryCondition::default() ->onExceptions([NetworkException::class]) ) ->build(); // Retries automatically with exponential backoff + jitter $response = $transport->get('/unreliable-endpoint')->send();
Custom Middleware
use Farzai\Transport\Middleware\MiddlewareInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; // Create custom middleware class AuthMiddleware implements MiddlewareInterface { public function handle(RequestInterface $request, callable $next): ResponseInterface { // Add auth token to all requests $request = $request->withHeader('Authorization', 'Bearer ' . $this->getToken()); return $next($request); } private function getToken(): string { // Your token logic return 'your-token'; } } // Add to transport $transport = TransportBuilder::make() ->withMiddleware(new AuthMiddleware()) ->build();
Configuration Options
$transport = TransportBuilder::make() ->withBaseUri('https://api.example.com') ->withHeaders(['Accept' => 'application/json']) ->withTimeout(30) // Seconds ->withRetries(3) // Max retry attempts ->withMiddleware($customMiddleware) ->withoutDefaultMiddlewares() // Disable logging, timeout, retry middlewares ->setClient($customPsrClient) // Use custom PSR-18 client ->setLogger($customLogger) // Use custom PSR-3 logger ->build();
HTTP Client Selection
use Farzai\Transport\Factory\ClientFactory; // Auto-detect (recommended - uses Symfony > Guzzle > Others) $transport = TransportBuilder::make()->build(); // Explicitly use Guzzle $transport = TransportBuilder::make() ->setClient(ClientFactory::createGuzzle(['timeout' => 30])) ->build(); // Explicitly use Symfony HTTP Client $transport = TransportBuilder::make() ->setClient(ClientFactory::createSymfony(['max_redirects' => 5])) ->build(); // Check which client is being used echo ClientFactory::getDetectedClientName(); // e.g., "Symfony\Component\HttpClient\Psr18Client"
File Upload & Multipart Requests
// Simple file upload $response = $transport->post('/upload') ->withFile( name: 'document', path: '/path/to/file.pdf', filename: 'report.pdf', additionalFields: ['title' => 'Monthly Report'] ) ->send(); // Multiple files with form data $response = $transport->post('/upload') ->withMultipart([ // Text fields ['name' => 'title', 'contents' => 'My Upload'], ['name' => 'description', 'contents' => 'File description'], // File uploads [ 'name' => 'avatar', 'contents' => file_get_contents('photo.jpg'), 'filename' => 'avatar.jpg', 'content-type' => 'image/jpeg' ], [ 'name' => 'document', 'contents' => fopen('/path/to/file.pdf', 'r'), 'filename' => 'document.pdf' ] ]) ->send(); // Advanced: Using MultipartStreamBuilder use Farzai\Transport\Multipart\MultipartStreamBuilder; $builder = new MultipartStreamBuilder(); $builder->addField('username', 'john_doe') ->addFile('avatar', '/path/to/avatar.jpg', 'profile.jpg') ->addFileContents('data', $jsonData, 'data.json', 'application/json'); $response = $transport->post('/api/upload') ->withMultipartBuilder($builder) ->send();
Cookie Management
// Automatic cookie handling $transport = TransportBuilder::make() ->withBaseUri('https://api.example.com') ->withCookies() // Enable automatic cookie management ->build(); // Login - cookies are automatically stored $transport->post('/login') ->withJson(['username' => 'user', 'password' => 'pass']) ->send(); // Subsequent requests automatically include cookies $response = $transport->get('/profile')->send(); // Advanced: Manual cookie management use Farzai\Transport\Cookie\CookieJar; use Farzai\Transport\Cookie\Cookie; $cookieJar = new CookieJar(); // Add cookies manually $cookieJar->setCookie(new Cookie( name: 'session_id', value: 'abc123', expiresAt: time() + 3600, domain: 'example.com', path: '/', secure: true, httpOnly: true )); $transport = TransportBuilder::make() ->withCookieJar($cookieJar) ->build(); // Inspect cookies echo "Cookies: {$cookieJar->count()}\n"; foreach ($cookieJar->getAllCookies() as $cookie) { echo "{$cookie->getName()}: {$cookie->getValue()}\n"; } // Export/Import cookies for persistence $data = $cookieJar->toArray(); file_put_contents('cookies.json', json_encode($data)); // Later... $newJar = new CookieJar(); $newJar->fromArray(json_decode(file_get_contents('cookies.json'), true));
Error Handling
use Farzai\Transport\Exceptions\ClientException; use Farzai\Transport\Exceptions\ServerException; use Farzai\Transport\Exceptions\RetryExhaustedException; use Farzai\Transport\Exceptions\JsonParseException; // Throw exception on non-2xx responses try { $response->throw(); } catch (ClientException $e) { // 4xx errors echo "Client error: {$e->getStatusCode()}\n"; echo "Request: {$e->getRequest()->getUri()}\n"; var_dump($e->getContext()); // Rich debugging context } catch (ServerException $e) { // 5xx errors echo "Server error: {$e->getStatusCode()}\n"; } // Custom error handling callback $response->throw(function ($response, $exception) { if ($response->statusCode() === 404) { throw new \Exception('Resource not found!'); } throw $exception; }); // Handle retry exhaustion try { $response = $transport->get('/flaky-endpoint')->send(); } catch (RetryExhaustedException $e) { echo "Failed after {$e->getAttempts()} attempts\n"; echo "Delays used: " . implode(', ', $e->getDelaysUsed()) . "ms\n"; foreach ($e->getRetryExceptions() as $attempt => $exception) { echo "Attempt $attempt: {$exception->getMessage()}\n"; } } // Handle JSON parse errors try { $data = $response->json(); } catch (JsonParseException $e) { echo "Invalid JSON: {$e->getMessage()}\n"; echo "JSON string: {$e->jsonString}\n"; echo "Error code: {$e->jsonErrorCode}\n"; }
Using Custom PSR-18 Client and Logger
use Farzai\Transport\TransportBuilder; use Monolog\Logger; use Monolog\Handler\StreamHandler; // Create custom logger $logger = new Logger('http-client'); $logger->pushHandler(new StreamHandler('path/to/your.log')); // Use any PSR-18 compliant client $client = new \Your\Custom\Psr18Client(); $transport = TransportBuilder::make() ->setClient($client) ->setLogger($logger) ->build();
Testing with Response Builder
use Farzai\Transport\ResponseBuilder; $response = ResponseBuilder::create() ->statusCode(200) ->withHeader('Content-Type', 'application/json') ->withBody('{"success": true}') ->build(); // Or use the fluent builder methods $response = ResponseBuilder::create() ->statusCode(404) ->withHeaders([ 'Content-Type' => 'application/json', 'X-Request-ID' => '12345' ]) ->withBody('{"error": "Not found"}') ->withVersion('1.1') ->withReason('Not Found') ->build();
Documentation
- Architecture Guide - Deep dive into design patterns and internal architecture
- Migration Guide - Migrating from Guzzle or v1.x
- Examples - Practical usage examples:
Architecture
No Hard Dependencies on Guzzle
Transport PHP v2.x uses PSR standards and auto-detection:
- PSR-7 - HTTP Message Interface
- PSR-17 - HTTP Factories for creating requests/responses
- PSR-18 - HTTP Client Interface
This means you can use any PSR-18 compliant HTTP client:
- ✅ Symfony HTTP Client (modern, async, HTTP/2)
- ✅ Guzzle (popular, stable)
- ✅ Any custom PSR-18 implementation
Middleware System
Request → Middleware Stack → HTTP Client → Response
↓
[LoggingMiddleware]
[TimeoutMiddleware]
[RetryMiddleware]
[CustomMiddleware...]
Default middlewares (can be disabled with withoutDefaultMiddlewares()):
- LoggingMiddleware: Logs requests and responses
- TimeoutMiddleware: Enforces request timeouts
- RetryMiddleware: Handles retry logic with configurable strategies
Immutable Configuration
All configuration is immutable and set during the build phase:
// ✅ Correct - configuration during build $transport = TransportBuilder::make() ->withTimeout(30) ->withRetries(3) ->build();
This makes the Transport instance:
- Thread-safe - Can be safely shared across threads
- Predictable - Configuration can't change unexpectedly
- Easier to test - No hidden state changes
Testing
composer test
Code Quality
# Run tests with coverage composer test-coverage # Fix code style composer format
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.