## Getting Started
### Minimal Setup
1. **Install the package**:
```bash
composer require aubes/http-pool-bundle
config/bundles.php (auto-registered in Symfony 6.4+):
Aubes\HttpPoolBundle\HttpPoolBundle::class => ['all' => true],
config/packages/http_pool.yaml:
http_pool:
default_concurrency: 10
error_strategy: collect
use Aubes\HttpPoolBundle\Pool\PoolFactoryInterface;
public function __construct(
private readonly PoolFactoryInterface $httpPool,
) {}
public function fetchUserData(int $userId): array
{
$pool = $this->httpPool->create(concurrency: 5);
$pool->add('user', 'GET', "https://api.example.com/users/{$userId}");
$pool->add('orders', 'GET', "https://api.example.com/orders?user={$userId}");
$results = $pool->flush();
return [
'user' => $results->get('user')->toArray(),
'orders' => $results->get('orders')->toArray(),
];
}
Key files to reference:
src/Pool/PoolFactoryInterface.php (core interface)src/Pool/PoolInterface.php (builder methods)src/Pool/PoolResults.php (results handling)config/packages/http_pool.yaml (configuration)// Per-request pool (auto-cleared after flush)
$pool = $this->httpPool->create(concurrency: 10);
$pool->add('data', 'GET', 'https://api.example.com/data');
$results = $pool->flush(); // Pool is now empty
Best Practice: Always use create() for new operations. Avoid reusing pools across requests to prevent memory leaks.
then()$pool->add('user', 'GET', 'https://api.example.com/users/1')
->then(function (ResponseInterface $response, PoolInterface $pool) {
$user = $response->toArray();
$pool->add('posts', 'GET', "https://api.example.com/posts?user={$user['id']}");
$pool->add('profile', 'GET', $user['profile_url']);
});
Pattern: Use then() for:
addOnce() for shared data.| Strategy | Use Case | Example |
|---|---|---|
collect |
Log errors silently (default). | $pool->create(errorStrategy: ErrorStrategy::Collect) |
stop_on_first |
Fail fast on critical errors. | $pool->create(errorStrategy: ErrorStrategy::StopOnFirst) |
throw_all |
Aggregate all errors for batch processing. | $pool->create(errorStrategy: ErrorStrategy::ThrowAll) |
Pattern:
$pool->add('fallback', 'GET', 'https://api.example.com/fallback')
->catch(function (\Throwable $e) {
// Custom fallback logic
return new Response(json_encode(['status' => 'fallback']));
});
addOnce()// Shared resource (e.g., brand data)
$pool->addOnce('brand_123', 'GET', 'https://api.example.com/brands/123')
->then(function (ResponseInterface $response) {
$brand = $response->toArray();
// Shared across all consumers
});
// Multiple consumers trigger the same request
$pool->add('product_1', 'GET', 'https://api.example.com/products/1')
->then(fn($res) => $pool->addOnce('brand_' . $res->toArray()['brandId'], ...));
$pool->add('product_2', 'GET', 'https://api.example.com/products/2')
->then(fn($res) => $pool->addOnce('brand_' . $res->toArray()['brandId'], ...));
Pattern:
addOnce() for shared resources (e.g., brands, categories, users).# config/packages/http_pool.yaml
http_pool:
rate_limits:
'api.example.com': 10 # 10 req/s
retry:
503: { max: 3, delay: 1000 } # Retry 503 up to 3x with 1s delay
429: respect_retry_after # Honor Retry-After header
Pattern:
503, 429).$pool->fire('POST', 'https://analytics.example.com/event', [
'json' => ['event' => 'user_viewed', 'user_id' => 123],
]);
// No need to handle the response
Use Case:
use Symfony\Component\Serializer\Normalizer\ContextAwareInterface;
public function normalize($object, string $format = null, array $context = []): array
{
$pool = $context['pool'] ?? $this->httpPool->create();
$data = $object->toArray();
// Fetch related resources
$pool->addOnce('author', 'GET', "https://api.example.com/authors/{$data['author_id']}");
$pool->addOnce('tags', 'GET', "https://api.example.com/tags/{$data['tag_ids']}");
$results = $pool->flush();
return [
...$data,
'author' => $results->get('author')->toArray(),
'tags' => $results->get('tags')->toArray(),
];
}
Pattern:
denormalization_context:
# config/services.yaml
App\Serializer\PoolAwareNormalizer:
arguments:
$httpPool: '@Aubes\HttpPoolBundle\Pool\PoolFactoryInterface'
public function testConcurrentRequests(): void
{
$pool = $this->httpPool->create(concurrency: 2);
$pool->add('user', 'GET', 'https://api.example.com/users/1');
$pool->add('posts', 'GET', 'https://api.example.com/posts/1');
// Mock responses
$this->httpClient->mock([
'GET https://api.example.com/users/1' => new Response(200, [], '{"id":1}'),
'GET https://api.example.com/posts/1' => new Response(200, [], '[{"id":1}]'),
]);
$results = $pool->flush();
$this->assertEquals(1, $results->get('user')->toArray()['id']);
}
Tools:
HttpClientInterface::mock() for isolated tests.PoolResults structure:
$this->assertCount(2, $results->getKeys());
$this->assertEquals('collect', $results->getErrorStrategy());
concurrency: 0 disables parallelism entirely (all requests run sequentially).concurrency: 5).then() callbacks run asynchronously after the HTTP response arrives.
$pool->add('user', 'GET', 'https://api.example.com/users/1')
->then(fn($res) => $this->log("User fetched")); // Runs later!
flush() to wait for all callbacks before proceeding.addOnce() deduplicates by key only. If two requestsHow can I help you explore Laravel packages today?