Skip to content

Commit 1b5760b

Browse files
authored
Merge pull request #26 from farzai/feature/observability-and-performance-improvements
Add event monitoring, streaming uploads, adaptive cookies, and PHPStan analysis
2 parents 19a1014 + 5394c77 commit 1b5760b

56 files changed

Lines changed: 8096 additions & 87 deletions

Some content is hidden

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

.github/workflows/run-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ jobs:
3333
- name: Install dependencies
3434
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
3535

36+
- name: Run PHPStan
37+
run: composer analyse
38+
3639
- name: Execute tests
3740
run: vendor/bin/pest --coverage --coverage-clover=coverage.xml
3841

README.md

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,28 @@ $transport = TransportBuilder::make()
167167
maxDelayMs: 30000, // Cap at 30 seconds
168168
useJitter: true // Add randomization
169169
),
170-
condition: RetryCondition::default()
170+
condition: (new RetryCondition())
171171
->onExceptions([NetworkException::class])
172172
)
173173
->build();
174174

175175
// Retries automatically with exponential backoff + jitter
176176
$response = $transport->get('/unreliable-endpoint')->send();
177+
178+
// Or use default retry condition (retries on any exception)
179+
$transport = TransportBuilder::make()
180+
->withRetries(
181+
maxRetries: 3,
182+
condition: RetryCondition::default() // Retries on ANY exception
183+
)
184+
->build();
177185
```
178186

187+
**Retry Conditions:**
188+
- `RetryCondition::default()` - Retries on any exception
189+
- `(new RetryCondition())->onExceptions([...])` - Retry only specific exceptions
190+
- `(new RetryCondition())->onStatusCodes([500, 502, 503])` - Retry specific HTTP status codes
191+
179192
### Custom Middleware
180193

181194
```php
@@ -290,8 +303,26 @@ $builder->addField('username', 'john_doe')
290303
$response = $transport->post('/api/upload')
291304
->withMultipartBuilder($builder)
292305
->send();
306+
307+
// Memory-efficient streaming for large files
308+
use Farzai\Transport\Multipart\StreamingMultipartBuilder;
309+
310+
$streamBuilder = new StreamingMultipartBuilder();
311+
$stream = $streamBuilder
312+
->addFile('video', '/path/to/large-video.mp4', 'video.mp4')
313+
->addField('title', 'My Video')
314+
->build();
315+
316+
// Streams file without loading entire content into memory
317+
$response = $transport->request()
318+
->withBody($stream)
319+
->withHeader('Content-Type', $streamBuilder->getContentType())
320+
->post('/upload')
321+
->send();
293322
```
294323

324+
**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.
325+
295326
### Cookie Management
296327

297328
```php
@@ -345,6 +376,67 @@ $newJar = new CookieJar();
345376
$newJar->fromArray(json_decode(file_get_contents('cookies.json'), true));
346377
```
347378

379+
**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.
380+
381+
### Event Monitoring
382+
383+
Monitor HTTP requests lifecycle with event listeners:
384+
385+
```php
386+
use Farzai\Transport\Events\RequestSendingEvent;
387+
use Farzai\Transport\Events\ResponseReceivedEvent;
388+
use Farzai\Transport\Events\RequestFailedEvent;
389+
use Farzai\Transport\Events\RetryAttemptEvent;
390+
391+
$transport = TransportBuilder::make()
392+
->withBaseUri('https://api.example.com')
393+
// Track successful responses
394+
->addEventListener(ResponseReceivedEvent::class, function ($event) {
395+
printf(
396+
"[SUCCESS] %s %s → %d (%.2fms)\n",
397+
$event->getMethod(),
398+
$event->getUri(),
399+
$event->getStatusCode(),
400+
$event->getDuration()
401+
);
402+
})
403+
// Track failed requests
404+
->addEventListener(RequestFailedEvent::class, function ($event) {
405+
printf(
406+
"[ERROR] %s %s failed: %s\n",
407+
$event->getMethod(),
408+
$event->getUri(),
409+
$event->getExceptionMessage()
410+
);
411+
})
412+
// Monitor retry attempts
413+
->addEventListener(RetryAttemptEvent::class, function ($event) {
414+
printf(
415+
"[RETRY] Attempt %d/%d (delay: %dms)\n",
416+
$event->getAttemptNumber(),
417+
$event->getMaxAttempts(),
418+
$event->getDelay()
419+
);
420+
})
421+
->withRetries(3)
422+
->build();
423+
424+
// Events are automatically dispatched during request lifecycle
425+
$response = $transport->get('/api/endpoint')->send();
426+
```
427+
428+
**Available Events:**
429+
- `RequestSendingEvent` - Before a request is sent
430+
- `ResponseReceivedEvent` - After successful response (includes duration metrics)
431+
- `RequestFailedEvent` - When a request fails with exception details
432+
- `RetryAttemptEvent` - Before each retry attempt with delay information
433+
434+
**Use Cases:**
435+
- Performance monitoring and metrics collection
436+
- Logging and debugging request/response cycles
437+
- Custom retry notifications
438+
- Request/response instrumentation
439+
348440
### Error Handling
349441

350442
```php
@@ -442,15 +534,15 @@ $response = ResponseBuilder::create()
442534

443535
## Documentation
444536

445-
- **[Architecture Guide](docs/architecture.md)** - Deep dive into design patterns and internal architecture
446-
- **[Migration Guide](docs/migration-from-guzzle.md)** - Migrating from Guzzle or v1.x
447537
- **[Examples](examples/)** - Practical usage examples:
448538
- [Basic Usage](examples/basic-usage.php)
449539
- [Custom HTTP Clients](examples/custom-client.php)
450540
- [Advanced Retry Logic](examples/advanced-retry.php)
451541
- [Custom Middleware](examples/middleware-example.php)
452542
- [File Upload](examples/file-upload.php)
543+
- [Streaming Upload](examples/streaming-upload.php)
453544
- [Cookie Session Management](examples/cookie-session.php)
545+
- [Event Monitoring](examples/event-monitoring.php)
454546

455547
## Architecture
456548

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

523615
## Contributing
524616

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

527619
## Security Vulnerabilities
528620

composer.json

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
{
22
"name": "farzai/transport",
3-
"description": "A HTTP client for Farzai Package",
3+
"description": "A modern, PSR-compliant HTTP client for PHP with middleware architecture, advanced retry strategies, and fluent API for building requests",
44
"keywords": [
5-
"farzai",
6-
"transport"
5+
"http",
6+
"http-client",
7+
"psr-7",
8+
"psr-18",
9+
"rest-client",
10+
"api-client",
11+
"middleware",
12+
"retry",
13+
"cookie",
14+
"multipart",
15+
"form-data",
16+
"file-upload",
17+
"fluent-api",
18+
"psr"
719
],
820
"homepage": "https://github.com/farzai/transport-php",
921
"license": "MIT",
@@ -48,7 +60,9 @@
4860
"scripts": {
4961
"test": "vendor/bin/pest",
5062
"test-coverage": "vendor/bin/pest --coverage",
51-
"format": "vendor/bin/pint"
63+
"format": "vendor/bin/pint",
64+
"analyse": "vendor/bin/phpstan analyse --memory-limit=2G",
65+
"analyse:baseline": "vendor/bin/phpstan analyse --memory-limit=2G --generate-baseline"
5266
},
5367
"config": {
5468
"sort-packages": true,

examples/event-monitoring.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
require __DIR__.'/../vendor/autoload.php';
4+
5+
use Farzai\Transport\Events\RequestFailedEvent;
6+
use Farzai\Transport\Events\ResponseReceivedEvent;
7+
use Farzai\Transport\Events\RetryAttemptEvent;
8+
use Farzai\Transport\TransportBuilder;
9+
10+
echo "Event Monitoring Example\n";
11+
echo "========================\n\n";
12+
13+
// Create transport with event listeners
14+
$transport = TransportBuilder::make()
15+
->withBaseUri('https://httpbin.org')
16+
->addEventListener(ResponseReceivedEvent::class, function ($event) {
17+
printf(
18+
"[SUCCESS] %s %s → %d (%.2fms)\n",
19+
$event->getMethod(),
20+
$event->getUri(),
21+
$event->getStatusCode(),
22+
$event->getDuration()
23+
);
24+
})
25+
->addEventListener(RequestFailedEvent::class, function ($event) {
26+
printf(
27+
"[ERROR] %s %s failed: %s\n",
28+
$event->getMethod(),
29+
$event->getUri(),
30+
$event->getExceptionMessage()
31+
);
32+
})
33+
->addEventListener(RetryAttemptEvent::class, function ($event) {
34+
printf(
35+
"[RETRY] Attempt %d/%d (delay: %dms)\n",
36+
$event->getAttemptNumber(),
37+
$event->getMaxAttempts(),
38+
$event->getDelay()
39+
);
40+
})
41+
->withRetries(3)
42+
->build();
43+
44+
// Make request
45+
echo "Making GET request...\n";
46+
$response = $transport->get('/get?foo=bar')->send();
47+
48+
echo "\nResponse Status: ".$response->statusCode()."\n";
49+
echo "Response successful!\n";

examples/streaming-upload.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
require __DIR__.'/../vendor/autoload.php';
4+
5+
use Farzai\Transport\Multipart\MultipartBuilderFactory;
6+
use Farzai\Transport\Multipart\StreamingMultipartBuilder;
7+
use Farzai\Transport\TransportBuilder;
8+
9+
echo "Streaming Upload Example\n";
10+
echo "========================\n\n";
11+
12+
// Create a test file
13+
$testFile = sys_get_temp_dir().'/test-upload.txt';
14+
file_put_contents($testFile, str_repeat('Large file content... ', 1000));
15+
$fileSize = filesize($testFile);
16+
17+
echo "Test file: {$testFile}\n";
18+
echo 'File size: '.number_format($fileSize)." bytes\n\n";
19+
20+
// Method 1: Manual streaming builder
21+
echo "Method 1: Manual Streaming Builder\n";
22+
echo "-----------------------------------\n";
23+
24+
$transport = TransportBuilder::make()
25+
->withBaseUri('https://httpbin.org')
26+
->build();
27+
28+
$builder = new StreamingMultipartBuilder;
29+
$stream = $builder
30+
->addFile('file', $testFile, 'upload.txt', 'text/plain')
31+
->addField('description', 'Streaming upload test')
32+
->addField('timestamp', (string) time())
33+
->build();
34+
35+
echo 'Memory before: '.number_format(memory_get_usage(true) / 1024)." KB\n";
36+
37+
$response = $transport->post('/post')
38+
->withBody($stream)
39+
->withHeader('Content-Type', $builder->getContentType())
40+
->send();
41+
42+
echo 'Memory after: '.number_format(memory_get_usage(true) / 1024)." KB\n";
43+
echo 'Status: '.$response->statusCode()."\n\n";
44+
45+
// Method 2: Auto-selection with factory
46+
echo "Method 2: Auto-Selection Factory\n";
47+
echo "---------------------------------\n";
48+
49+
$parts = [
50+
['name' => 'file', 'contents' => $testFile, 'filename' => 'upload.txt'],
51+
['name' => 'description', 'contents' => 'Auto-selected builder'],
52+
];
53+
54+
$builder = MultipartBuilderFactory::create(null, $parts);
55+
echo 'Selected builder: '.($builder instanceof StreamingMultipartBuilder ? 'Streaming' : 'Standard')."\n";
56+
echo 'Threshold: '.MultipartBuilderFactory::getDefaultThreshold()." bytes\n\n";
57+
58+
// Cleanup
59+
unlink($testFile);
60+
61+
echo "Example complete!\n";

phpstan.neon

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
parameters:
2+
level: 8
3+
paths:
4+
- src
5+
excludePaths:
6+
- tests
7+
- vendor
8+
9+
# Strict rules for better type safety
10+
checkGenericClassInNonGenericObjectType: true
11+
checkMissingVarTagTypehint: true
12+
treatPhpDocTypesAsCertain: false
13+
14+
# Exception rules
15+
exceptions:
16+
check:
17+
missingCheckedExceptionInThrows: true
18+
tooWideThrowType: true
19+
implicitThrows: true
20+
checkedExceptionClasses:
21+
- Farzai\Transport\Exceptions\TransportException
22+
23+
# Additional checks
24+
reportUnmatchedIgnoredErrors: true
25+
reportMaybesInMethodSignatures: true
26+
reportStaticMethodSignatures: true
27+
28+
# Type coverage (optional - uncomment to track type coverage)
29+
# type_coverage:
30+
# param_type: 100
31+
# return_type: 100
32+
# property_type: 100
33+
34+
# Ignore specific errors if needed (add as you encounter false positives)
35+
ignoreErrors:
36+
# Add patterns here if needed
37+
# - '#PHPDoc tag @var for variable#'
38+
39+
# Bootstrap file if needed
40+
# bootstrapFiles:
41+
# - tests/bootstrap.php

src/Contracts/ResponseInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public function body(): string;
2121

2222
/**
2323
* Return the response headers.
24+
*
25+
* @return array<string, array<string>>
2426
*/
2527
public function headers(): array;
2628

@@ -57,7 +59,7 @@ public function toArray(): array;
5759
*
5860
* @throws \Psr\Http\Client\ClientExceptionInterface
5961
*/
60-
public function throw(?callable $callback = null);
62+
public function throw(?callable $callback = null): static;
6163

6264
/**
6365
* Return the psr request.

0 commit comments

Comments
 (0)