Statamic limita las peticiones a su API. Cuando superas el límite responde 429, normalmente con una cabecera Retry-After. El cliente HTTP de Laravel puede reintentar por ti, y te deja decir exactamente cuándo y durante cuánto tiempo:
// app/Services/License/StatamicDriver.php
private function http(): PendingRequest
{
return Http::statamic($this->token)->retry(
times: 3,
sleepMilliseconds: $this->backoff(...),
when: $this->wasThrottled(...),
throw: false,
);
}
private function wasThrottled(Exception $e): bool
{
return $e instanceof RequestException
&& $e->response->tooManyRequests();
}
private function backoff(int $attempt, Exception $e): int
{
$retryAfter = $e instanceof RequestException
? $e->response->header('Retry-After')
: null;
$delay = is_numeric($retryAfter)
? (int) $retryAfter * 1000
: 500 * 2 ** ($attempt - 1);
return min($delay, 5000);
}
Reintenta solo ante un 429. Un 429 significa que el proveedor rechazó la petición antes de hacer ningún trabajo, así que repetirla es seguro sea cual sea el método. Ese razonamiento no se extiende a un 500 ni a un timeout, donde no tienes idea de hasta dónde llegó el otro lado. Esos no se reintentan.
La espera está acotada. Un proveedor que devuelve Retry-After: 3600 le está pidiendo a tu worker que duerma una hora dentro de una petición que será abortada mucho antes. Respeta la indicación, pero nunca más allá de tu propio timeout.
Y throw: false significa que el cliente devuelve la última respuesta en lugar de lanzar una excepción tras el último intento. El driver decide qué significa un fallo, y así es como delete puede tratar un 404 como un éxito. (Documentación de Laravel: HTTP Client › Retries.)
Un nombre al que la aplicación puede llamar
El driver existe. El controlador necesita una forma de alcanzarlo sin nombrarlo. Dos piezas pequeñas lo consiguen.
Primero, dile al contenedor qué implementación hay detrás del contrato:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(
LicenseContract::class,
fn () => new StatamicDriver(
config('services.statamic.token'),
),
);
}
Después, dale una facade:
// app/Facades/Licenses.php
/**
* @method static Collection<int, License> all()
* @method static License create(string $name, string $domain)
* @method static void delete(string $key)
*/
class Licenses extends Facade
{
protected static function getFacadeAccessor(): string
{
return LicenseContract::class;
}
}
Licenses::all() le pide al contenedor lo que esté enlazado a LicenseContract y llama a all() sobre ello. Un salto, y la única línea del controlador llega a Statamic.
Una facade lleva el nombre de aquello a lo que te da acceso, igual que Cache y Queue. No estás obligado a usar una. Declarar LicenseContract como tipo en un método del controlador funciona igual de bien, y algunos equipos lo prefieren. Este libro usa la facade porque se lee como se lee el resto de Laravel. (Documentación de Laravel: Facades.)
En un test, reemplaza aquello a lo que apunta la facade:
$this->mock(LicenseContract::class)
->shouldReceive('all')
->once()
->andReturn(collect());
$this->mock() pone un doble de prueba en el contenedor bajo el nombre del contrato, y Licenses::all() llega ahora al doble. Las facades del propio Laravel suelen reemplazarse con Cache::shouldReceive(), y Licenses::shouldReceive() funcionaría hoy, siempre que hubiera un token en el entorno de test. Deja de funcionar en el Capítulo 4. Para saber qué clase imitar, shouldReceive() construye primero el driver real, y a partir del Capítulo 4 el driver real no puede construirse sin un consumidor y sus credenciales. (Documentación de Laravel: Mocking › Mocking Objects.)
La configuración es corriente:
// config/services.php
'statamic' => [
'url' => env('STATAMIC_API_URL'),
'token' => env('STATAMIC_API_TOKEN'),
],