
# Middleware

El middleware envuelve la ejecución de trabajos en una canalización con estructura de cebolla. Cada middleware puede ejecutar lógica antes y después del trabajo, omitir su ejecución o capturar excepciones. Queuety incluye cinco middleware integrados y admite middleware personalizados mediante la interfaz `Contracts\Middleware`.

El middleware está disponible para [clases de trabajo que se pueden despachar](https://queuety.inline0.dev/es/docs/jobs#dispatchable-job-classes) (clases que implementan `Contracts\Job` con el trait `Dispatchable`).

## La interfaz `Contracts\Middleware`

```php
use Queuety\Contracts\Middleware;

interface Middleware {
    public function handle( object $job, \Closure $next ): void;
}
```

El parámetro `$job` es la instancia del trabajo que se está procesando. Llame a `$next( $job )` para pasar la ejecución al siguiente middleware de la canalización (o al método `handle()` del trabajo si este es el último middleware).

## Añadir middleware a un trabajo

Defina un método `middleware()` en la clase del trabajo que devuelva una matriz de instancias de middleware:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;

class SyncInventoryJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $sku,
    ) {}

    public function handle(): void {
        // Sync inventory for this SKU...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 30 ),
            new RateLimited( max: 100, window: 60 ),
        ];
    }
}
```

## Orden de ejecución de la canalización

El middleware se aplica en el orden de la matriz, y el primero forma la capa más externa. Si su método `middleware()` devuelve `[A, B, C]`, el orden de ejecución es:

```
A::handle() -> B::handle() -> C::handle() -> $job->handle()
```

Cada middleware envuelve al siguiente. `A` se ejecuta primero antes del trabajo y al final después del trabajo, por lo que es ideal para medir el tiempo o envolverlo en un bloque try/catch.

## Escribir middleware personalizado

Implemente la interfaz `Contracts\Middleware`:

```php
use Queuety\Contracts\Middleware;

class LogExecution implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        $class = get_class( $job );
        error_log( "[Queuety] Starting: {$class}" );

        $start = microtime( true );

        try {
            $next( $job );
        } finally {
            $elapsed = round( microtime( true ) - $start, 3 );
            error_log( "[Queuety] Finished: {$class} in {$elapsed}s" );
        }
    }
}
```

Úselo en un trabajo:

```php
public function middleware(): array {
    return [
        new LogExecution(),
    ];
}
```

### Omitir la ejecución

Un middleware puede omitir por completo el trabajo si no llama a `$next()`:

```php
class SkipOnMaintenanceMode implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        if ( wp_is_maintenance_mode() ) {
            return; // Job silently skipped.
        }

        $next( $job );
    }
}
```

## Middleware integrado

### `ThrottlesExceptions`

Limita las excepciones de un controlador de trabajo para evitar avalanchas de trabajos. Si un controlador ha producido demasiadas excepciones durante un intervalo de tiempo, los trabajos posteriores de ese controlador se rechazan con un `RuntimeException` en lugar de procesarse. Así se evita que un servicio externo con fallos agote todos los intentos de reintento de muchos trabajos a la vez.

```php
use Queuety\Middleware\ThrottlesExceptions;

public function middleware(): array {
    return [
        new ThrottlesExceptions( max_attempts: 10, decay_minutes: 10 ),
    ];
}
```

| Parámetro | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
| `$max_attempts` | `int` | `10` | Número máximo de excepciones permitidas durante el intervalo de decaimiento |
| `$decay_minutes` | `int` | `10` | Duración del intervalo en minutos |

Los recuentos de excepciones se registran por clase de controlador en la tabla de bloqueos de Queuety y se comparten entre todos los procesos de trabajo. Al alcanzar el umbral, el middleware lanza un `RuntimeException` sin llamar al trabajo, y el trabajador lo trata como cualquier otro fallo: el intento cuenta para `max_attempts`, el trabajo se reintenta con la espera habitual y queda en la cola de trabajos fallidos cuando se agotan los intentos.

Ejemplo: si una API externa está caída, los primeros 10 fallos se procesan con normalidad (cada uno incrementa el contador de excepciones). Los intentos posteriores fallan rápidamente sin llamar a API hasta que transcurran 10 minutos sin una nueva excepción; el intervalo se reinicia con cada excepción registrada.

### `RateLimited`

Impone un número máximo de ejecuciones durante un intervalo de tiempo. Si se supera el límite dentro del middleware, se lanza un `RateLimitExceededException`; el trabajador lo gestiona como un intento fallido normal, lo reintenta con la espera habitual y lo cuenta para `max_attempts`. Una vez que el middleware registra el límite en el limitador de tasa del trabajador, los trabajos posteriores de ese controlador también se devuelven a `pending` antes de ejecutarse mientras se alcance el límite (consulte [Limitación de tasa](https://queuety.inline0.dev/es/docs/features/rate-limiting)).

```php
use Queuety\Middleware\RateLimited;

public function middleware(): array {
    return [
        new RateLimited( max: 60, window: 60 ), // 60 per minute
    ];
}
```

| Parámetro | Tipo | Descripción |
|---|---|---|
| `$max` | `int` | Máximo de ejecuciones permitidas en el intervalo |
| `$window` | `int` | Duración del intervalo en segundos |

El limitador de tasa usa la instancia interna `RateLimiter` de Queuety, por lo que los límites se aplican a todos los procesos de trabajo.

### `Timeout`

Activa un tiempo de espera de ejecución por trabajo con `pcntl_alarm` y está diseñado para lanzar un `TimeoutException` si el trabajo tarda demasiado. Actualmente, el tiempo de espera no se aplica ([#10](https://github.com/inline0/queuety/issues/10)): Queuety no habilita las señales asíncronas, por lo que el controlador de la alarma no se ejecuta mientras el trabajo está en curso. El middleware también restablece la alarma al finalizar, lo que cancela la alarma de tiempo de espera del trabajo del propio trabajador.

```php
use Queuety\Middleware\Timeout;

public function middleware(): array {
    return [
        new Timeout( seconds: 30 ),
    ];
}
```

| Parámetro | Tipo | Descripción |
|---|---|---|
| `$seconds` | `int` | Tiempo máximo de ejecución en segundos |

Requiere la extensión `pcntl` PHP. Si `pcntl` no está disponible, el middleware continúa sin avisar (no se aplica ningún tiempo de espera).

### `UniqueJob`

Evita la ejecución simultánea de trabajos con la misma clave. Usa una tabla de bloqueos en la base de datos para garantizar que solo se ejecute una instancia a la vez. Si ya hay un bloqueo, el trabajo se omite silenciosamente (sin reintento).

```php
use Queuety\Middleware\UniqueJob;

public function middleware(): array {
    return [
        new UniqueJob(),                         // Lock key defaults to the job class name
        new UniqueJob( key: 'sync_inventory' ),   // Custom lock key
    ];
}
```

| Parámetro | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
| `$key` | `string\|null` | `null` (usa el nombre de la clase) | Clave de bloqueo para garantizar la unicidad |

### `WithoutOverlapping`

Evita que se solapen las ejecuciones de trabajos que comparten una clave lógica, con una caducidad automática del bloqueo para evitar bloqueos permanentes si los trabajadores se bloquean. Es similar a `UniqueJob`, pero está diseñado para operaciones de larga duración en las que el bloqueo debe liberarse automáticamente tras un tiempo de espera.

```php
use Queuety\Middleware\WithoutOverlapping;

public function middleware(): array {
    return [
        new WithoutOverlapping( key: 'import_products', release_after: 600 ),
    ];
}
```

| Parámetro | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
| `$key` | `string` | (obligatorio) | Clave de la operación lógica |
| `$release_after` | `int` | `300` | Duración máxima del bloqueo en segundos antes de su liberación automática |

Si ya hay un bloqueo, el trabajo se omite silenciosamente. Los bloqueos caducados se limpian automáticamente antes de cada intento de adquisición.

## Ejemplo: combinar middleware

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
use Queuety\Middleware\WithoutOverlapping;

class ImportProductsJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly int $supplier_id,
    ) {}

    public function handle(): void {
        // Fetch and import products from the supplier...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 300 ),
            new WithoutOverlapping(
                key: "import_supplier_{$this->supplier_id}",
                release_after: 600,
            ),
            new RateLimited( max: 5, window: 60 ),
        ];
    }
}
```

Este trabajo:

1. Se cancelará si tarda más de 5 minutos (Timeout)
2. Se omitirá si ya hay otra importación en curso para el mismo proveedor (WithoutOverlapping)
3. Tendrá un límite de tasa de 5 importaciones por minuto entre todos los proveedores (RateLimited)
