# Doc: Symfony HttpClient Best Practices (Symfony 7.x)

> Relevancia Wari: BggApiService usa HttpClient para comunicarse con la API de BoardGameGeek. DT-003, DT-030, DT-034.

## Scoped client para APIs externas

En lugar de un cliente genérico, configurar un cliente específico para BGG con opciones comunes:

```yaml
# config/packages/framework.yaml
framework:
    http_client:
        scoped_clients:
            bgg.client:
                base_uri: 'https://boardgamegeek.com/xmlapi2/'
                timeout: 30
                max_duration: 60
                headers:
                    'User-Agent': 'Wari/1.0 (contact@wari.app)'
```

```php
class BggApiService
{
    public function __construct(
        private HttpClientInterface $bggClient  // autowired por nombre
    ) {}
}
```

## Reintentos automáticos (sin usleep manual)

```yaml
bgg.client:
    base_uri: 'https://boardgamegeek.com/xmlapi2/'
    retry_failed:
        max_retries: 3
        http_codes: [429, 500, 502, 503, 504]
        delay: 1000       # ms
        multiplier: 2     # 1s, 2s, 4s
        jitter: 0.1
```

Esto elimina el `usleep()` manual del código (DT-003, DT-034). El componente gestiona los reintentos solo.

## Requests en paralelo (para batch de juegos)

```php
// ❌ MAL — secuencial, bloquea en cada request
foreach ($bggIds as $id) {
    $response = $this->client->request('GET', "thing?id={$id}");
    $data = $response->toArray(); // bloquea aquí
}

// ✅ BIEN — paralelo, lanza todos y procesa cuando lleguen
$responses = [];
foreach (array_chunk($bggIds, 20) as $chunk) {
    $ids = implode(',', $chunk);
    $responses[] = $this->client->request('GET', "thing?id={$ids}&stats=1");
}

foreach ($responses as $response) {
    $xml = $response->getContent(); // procesa según llegan
}
```

## Manejo de errores

```php
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;

try {
    $response = $this->bggClient->request('GET', "thing?id={$id}");
    $content = $response->getContent();
} catch (TransportExceptionInterface $e) {
    // Error de red, timeout
    throw new BggApiException("BGG no disponible: {$e->getMessage()}");
} catch (HttpExceptionInterface $e) {
    // 4xx / 5xx
    throw new BggApiException("BGG devolvió error {$e->getResponse()->getStatusCode()}");
}
```

## Timeouts

```php
$response = $this->bggClient->request('GET', 'thing', [
    'query'        => ['id' => $id],
    'timeout'      => 10,    // idle timeout
    'max_duration' => 30,    // duración total máxima
]);
```

## Credenciales y configuración — en .env, nunca en código

```env
# .env
BGG_API_EMAIL=wari@app.local
```

```php
public function __construct(
    private HttpClientInterface $bggClient,
    #[Autowire('%env(BGG_API_EMAIL)%')] private string $bggEmail,
) {}
```

Elimina el email hardcodeado de DT-030.

## Testing con MockHttpClient

```php
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

public function testSincronizarJuego(): void
{
    $mockXml = file_get_contents(__DIR__ . '/fixtures/bgg_thing.xml');
    $client  = new MockHttpClient([
        new MockResponse($mockXml, ['http_code' => 200]),
    ]);

    $service = new BggApiService($client, 'test@example.com');
    $juego   = $service->fetchJuego(12345);

    $this->assertSame('Catan', $juego->getNombre());
}
```

## Errores comunes

| Error | Corrección |
|---|---|
| `usleep()` / `sleep()` en el hilo principal | Usar `retry_failed` en la configuración del cliente |
| Requests secuenciales en bucle | Disparar en paralelo, recoger después |
| Sin timeout configurado | Definir `timeout` y `max_duration` |
| Credenciales hardcodeadas | Mover a `.env` y autowirear |
| Sin manejo de excepciones | Capturar `TransportExceptionInterface` y `HttpExceptionInterface` |
| Tests con requests reales a BGG | Usar `MockHttpClient` + fixtures XML |
