Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction

- name: Run PHPStan
run: composer analyse

- name: Execute tests
run: vendor/bin/pest --coverage --coverage-clover=coverage.xml

Expand Down
100 changes: 96 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,28 @@ $transport = TransportBuilder::make()
maxDelayMs: 30000, // Cap at 30 seconds
useJitter: true // Add randomization
),
condition: RetryCondition::default()
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
Expand Down Expand Up @@ -290,8 +303,26 @@ $builder->addField('username', 'john_doe')
$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
Expand Down Expand Up @@ -345,6 +376,67 @@ $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
Expand Down Expand Up @@ -442,15 +534,15 @@ $response = ResponseBuilder::create()

## Documentation

- **[Architecture Guide](docs/architecture.md)** - Deep dive into design patterns and internal architecture
- **[Migration Guide](docs/migration-from-guzzle.md)** - Migrating from Guzzle or v1.x
- **[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

Expand Down Expand Up @@ -522,7 +614,7 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re

## Contributing

Please see [CONTRIBUTING](https://github.com/farzai/.github/blob/main/CONTRIBUTING.md) for details.
Please see [CONTRIBUTING](docs/contributing/CONTRIBUTING.md) for details.

## Security Vulnerabilities

Expand Down
22 changes: 18 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
{
"name": "farzai/transport",
"description": "A HTTP client for Farzai Package",
"description": "A modern, PSR-compliant HTTP client for PHP with middleware architecture, advanced retry strategies, and fluent API for building requests",
"keywords": [
"farzai",
"transport"
"http",
"http-client",
"psr-7",
"psr-18",
"rest-client",
"api-client",
"middleware",
"retry",
"cookie",
"multipart",
"form-data",
"file-upload",
"fluent-api",
"psr"
],
"homepage": "https://github.com/farzai/transport-php",
"license": "MIT",
Expand Down Expand Up @@ -48,7 +60,9 @@
"scripts": {
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
"format": "vendor/bin/pint",
"analyse": "vendor/bin/phpstan analyse --memory-limit=2G",
"analyse:baseline": "vendor/bin/phpstan analyse --memory-limit=2G --generate-baseline"
},
"config": {
"sort-packages": true,
Expand Down
49 changes: 49 additions & 0 deletions examples/event-monitoring.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

require __DIR__.'/../vendor/autoload.php';

use Farzai\Transport\Events\RequestFailedEvent;
use Farzai\Transport\Events\ResponseReceivedEvent;
use Farzai\Transport\Events\RetryAttemptEvent;
use Farzai\Transport\TransportBuilder;

echo "Event Monitoring Example\n";
echo "========================\n\n";

// Create transport with event listeners
$transport = TransportBuilder::make()
->withBaseUri('https://httpbin.org')
->addEventListener(ResponseReceivedEvent::class, function ($event) {
printf(
"[SUCCESS] %s %s → %d (%.2fms)\n",
$event->getMethod(),
$event->getUri(),
$event->getStatusCode(),
$event->getDuration()
);
})
->addEventListener(RequestFailedEvent::class, function ($event) {
printf(
"[ERROR] %s %s failed: %s\n",
$event->getMethod(),
$event->getUri(),
$event->getExceptionMessage()
);
})
->addEventListener(RetryAttemptEvent::class, function ($event) {
printf(
"[RETRY] Attempt %d/%d (delay: %dms)\n",
$event->getAttemptNumber(),
$event->getMaxAttempts(),
$event->getDelay()
);
})
->withRetries(3)
->build();

// Make request
echo "Making GET request...\n";
$response = $transport->get('/get?foo=bar')->send();

echo "\nResponse Status: ".$response->statusCode()."\n";
echo "Response successful!\n";
61 changes: 61 additions & 0 deletions examples/streaming-upload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

require __DIR__.'/../vendor/autoload.php';

use Farzai\Transport\Multipart\MultipartBuilderFactory;
use Farzai\Transport\Multipart\StreamingMultipartBuilder;
use Farzai\Transport\TransportBuilder;

echo "Streaming Upload Example\n";
echo "========================\n\n";

// Create a test file
$testFile = sys_get_temp_dir().'/test-upload.txt';
file_put_contents($testFile, str_repeat('Large file content... ', 1000));
$fileSize = filesize($testFile);

echo "Test file: {$testFile}\n";
echo 'File size: '.number_format($fileSize)." bytes\n\n";

// Method 1: Manual streaming builder
echo "Method 1: Manual Streaming Builder\n";
echo "-----------------------------------\n";

$transport = TransportBuilder::make()
->withBaseUri('https://httpbin.org')
->build();

$builder = new StreamingMultipartBuilder;
$stream = $builder
->addFile('file', $testFile, 'upload.txt', 'text/plain')
->addField('description', 'Streaming upload test')
->addField('timestamp', (string) time())
->build();

echo 'Memory before: '.number_format(memory_get_usage(true) / 1024)." KB\n";

$response = $transport->post('/post')
->withBody($stream)
->withHeader('Content-Type', $builder->getContentType())
->send();

echo 'Memory after: '.number_format(memory_get_usage(true) / 1024)." KB\n";
echo 'Status: '.$response->statusCode()."\n\n";

// Method 2: Auto-selection with factory
echo "Method 2: Auto-Selection Factory\n";
echo "---------------------------------\n";

$parts = [
['name' => 'file', 'contents' => $testFile, 'filename' => 'upload.txt'],
['name' => 'description', 'contents' => 'Auto-selected builder'],
];

$builder = MultipartBuilderFactory::create(null, $parts);
echo 'Selected builder: '.($builder instanceof StreamingMultipartBuilder ? 'Streaming' : 'Standard')."\n";
echo 'Threshold: '.MultipartBuilderFactory::getDefaultThreshold()." bytes\n\n";

// Cleanup
unlink($testFile);

echo "Example complete!\n";
41 changes: 41 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
parameters:
level: 8
paths:
- src
excludePaths:
- tests
- vendor

# Strict rules for better type safety
checkGenericClassInNonGenericObjectType: true
checkMissingVarTagTypehint: true
treatPhpDocTypesAsCertain: false

# Exception rules
exceptions:
check:
missingCheckedExceptionInThrows: true
tooWideThrowType: true
implicitThrows: true
checkedExceptionClasses:
- Farzai\Transport\Exceptions\TransportException

# Additional checks
reportUnmatchedIgnoredErrors: true
reportMaybesInMethodSignatures: true
reportStaticMethodSignatures: true

# Type coverage (optional - uncomment to track type coverage)
# type_coverage:
# param_type: 100
# return_type: 100
# property_type: 100

# Ignore specific errors if needed (add as you encounter false positives)
ignoreErrors:
# Add patterns here if needed
# - '#PHPDoc tag @var for variable#'

# Bootstrap file if needed
# bootstrapFiles:
# - tests/bootstrap.php
4 changes: 3 additions & 1 deletion src/Contracts/ResponseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public function body(): string;

/**
* Return the response headers.
*
* @return array<string, array<string>>
*/
public function headers(): array;

Expand Down Expand Up @@ -57,7 +59,7 @@ public function toArray(): array;
*
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function throw(?callable $callback = null);
public function throw(?callable $callback = null): static;

/**
* Return the psr request.
Expand Down
Loading
Loading