https://github.com/farzai/transport-php
Transport classes and utilities shared among PHP Farzai libraries
https://github.com/farzai/transport-php
Last synced: 7 months ago
JSON representation
Transport classes and utilities shared among PHP Farzai libraries
- Host: GitHub
- URL: https://github.com/farzai/transport-php
- Owner: farzai
- License: mit
- Created: 2023-05-29T03:30:50.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2025-10-24T00:35:48.000Z (8 months ago)
- Last Synced: 2025-10-24T02:47:18.180Z (8 months ago)
- Language: PHP
- Size: 79.1 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE.md
Awesome Lists containing this project
README
# Transport PHP
[](https://packagist.org/packages/farzai/transport)
[](https://github.com/farzai/transport-php/actions/workflows/run-tests.yml)
[](https://codecov.io/gh/farzai/transport-php)
[](https://packagist.org/packages/farzai/transport)
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:
```bash
composer require farzai/transport
```
The library will auto-detect any available PSR-18 HTTP client. If you don't have one installed, we recommend:
```bash
# 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!
```php
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)
```php
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
```php
// 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
```php
// 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
```php
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: (new RetryCondition())
->onExceptions([NetworkException::class])
)
->build();
// Retries automatically with exponential backoff + jitter
$response = $transport->get('/unreliable-endpoint')->send();
// Or use default retry condition (retries on any exception)
$transport = TransportBuilder::make()
->withRetries(
maxRetries: 3,
condition: RetryCondition::default() // Retries on ANY exception
)
->build();
```
**Retry Conditions:**
- `RetryCondition::default()` - Retries on any exception
- `(new RetryCondition())->onExceptions([...])` - Retry only specific exceptions
- `(new RetryCondition())->onStatusCodes([500, 502, 503])` - Retry specific HTTP status codes
### Custom Middleware
```php
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
```php
$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
```php
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
```php
// 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();
// Memory-efficient streaming for large files
use Farzai\Transport\Multipart\StreamingMultipartBuilder;
$streamBuilder = new StreamingMultipartBuilder();
$stream = $streamBuilder
->addFile('video', '/path/to/large-video.mp4', 'video.mp4')
->addField('title', 'My Video')
->build();
// Streams file without loading entire content into memory
$response = $transport->request()
->withBody($stream)
->withHeader('Content-Type', $streamBuilder->getContentType())
->post('/upload')
->send();
```
**Note:** The library automatically selects `StreamingMultipartBuilder` for large files (>1MB by default) to optimize memory usage. You can also manually use it for memory-efficient uploads of any size.
### Cookie Management
```php
// 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));
```
**Performance Note:** The cookie management system automatically optimizes for different workloads. For applications with many cookies (50+), it uses indexed collections with O(1) domain lookups. For smaller cookie counts, it uses simpler collections to minimize overhead.
### Event Monitoring
Monitor HTTP requests lifecycle with event listeners:
```php
use Farzai\Transport\Events\RequestSendingEvent;
use Farzai\Transport\Events\ResponseReceivedEvent;
use Farzai\Transport\Events\RequestFailedEvent;
use Farzai\Transport\Events\RetryAttemptEvent;
$transport = TransportBuilder::make()
->withBaseUri('https://api.example.com')
// Track successful responses
->addEventListener(ResponseReceivedEvent::class, function ($event) {
printf(
"[SUCCESS] %s %s → %d (%.2fms)\n",
$event->getMethod(),
$event->getUri(),
$event->getStatusCode(),
$event->getDuration()
);
})
// Track failed requests
->addEventListener(RequestFailedEvent::class, function ($event) {
printf(
"[ERROR] %s %s failed: %s\n",
$event->getMethod(),
$event->getUri(),
$event->getExceptionMessage()
);
})
// Monitor retry attempts
->addEventListener(RetryAttemptEvent::class, function ($event) {
printf(
"[RETRY] Attempt %d/%d (delay: %dms)\n",
$event->getAttemptNumber(),
$event->getMaxAttempts(),
$event->getDelay()
);
})
->withRetries(3)
->build();
// Events are automatically dispatched during request lifecycle
$response = $transport->get('/api/endpoint')->send();
```
**Available Events:**
- `RequestSendingEvent` - Before a request is sent
- `ResponseReceivedEvent` - After successful response (includes duration metrics)
- `RequestFailedEvent` - When a request fails with exception details
- `RetryAttemptEvent` - Before each retry attempt with delay information
**Use Cases:**
- Performance monitoring and metrics collection
- Logging and debugging request/response cycles
- Custom retry notifications
- Request/response instrumentation
### Error Handling
```php
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
```php
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
```php
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
- **[Examples](examples/)** - Practical usage examples:
- [Basic Usage](examples/basic-usage.php)
- [Custom HTTP Clients](examples/custom-client.php)
- [Advanced Retry Logic](examples/advanced-retry.php)
- [Custom Middleware](examples/middleware-example.php)
- [File Upload](examples/file-upload.php)
- [Streaming Upload](examples/streaming-upload.php)
- [Cookie Session Management](examples/cookie-session.php)
- [Event Monitoring](examples/event-monitoring.php)
## 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:
```php
// ✅ 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
```bash
composer test
```
## Code Quality
```bash
# Run tests with coverage
composer test-coverage
# Fix code style
composer format
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](docs/contributing/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [parsilver](https://github.com/parsilver)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.