
# Middleware

Middleware indpakker jobudførelse i en løgformet pipeline. Hvert middleware-led kan udføre logik før og efter jobbet, afbryde udførelsen eller fange undtagelser. Queuety leveres med fem indbyggede middleware-led og understøtter tilpassede middleware-led via grænsefladen `Contracts\Middleware`.

Middleware er tilgængeligt for [jobklasser, der kan sendes til behandling](https://queuety.inline0.dev/da/docs/jobs#dispatchable-job-classes) (klasser, der implementerer `Contracts\Job` med traiten `Dispatchable`).

## Grænsefladen `Contracts\Middleware`

```php
use Queuety\Contracts\Middleware;

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

Parameteren `$job` er den jobinstans, der behandles. Kald `$next( $job )` for at sende udførelsen videre til det næste middleware-led i pipelinen (eller til jobbets metode `handle()`, hvis dette er det sidste middleware-led).

## Føj middleware til et job

Definér en metode `middleware()` i din jobklasse, som returnerer et array med middlewareinstanser:

```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 ),
        ];
    }
}
```

## Rækkefølge for pipelineudførelse

Middleware anvendes i arrayrækkefølge, hvor det første middleware-led er det yderste lag. Hvis din metode `middleware()` returnerer `[A, B, C]`, er udførelsesrækkefølgen:

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

Hvert middleware-led indpakker det næste. `A` kører først før jobbet og sidst efter jobbet, hvilket gør det velegnet til tidsmåling eller try/catch-indpakning.

## Skriv tilpasset middleware

Implementér grænsefladen `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" );
        }
    }
}
```

Brug det på et job:

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

### Afbryd før jobbet

Et middleware-led kan springe jobbet helt over ved ikke at kalde `$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 );
    }
}
```

## Indbygget middleware

### `ThrottlesExceptions`

Begrænser antallet af undtagelser fra en jobhandler for at forhindre jobstorme. Hvis en handler har udløst for mange undtagelser inden for et tidsinterval, afvises efterfølgende jobs for denne handler med en `RuntimeException` i stedet for at blive behandlet. Det forhindrer, at en ekstern tjeneste, der fejler, opbruger alle gentagelsesforsøg for mange jobs på én gang.

```php
use Queuety\Middleware\ThrottlesExceptions;

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

| Parameter | Type | Standard | Beskrivelse |
|---|---|---|---|
| `$max_attempts` | `int` | `10` | Maksimalt antal undtagelser inden for forfaldsvinduet |
| `$decay_minutes` | `int` | `10` | Vinduesvarighed i minutter |

Antallet af undtagelser registreres pr. handlerklasse i låsetabellen for Queuety og deles mellem alle workerprocesser. Når grænsen nås, udløser middleware-leddet en `RuntimeException` uden at kalde jobbet, og workeren behandler det som enhver anden fejl: forsøget tæller med i `max_attempts`, jobbet gentages med den normale backoff, og det lægges i depot, når alle forsøg er brugt.

Eksempel: Hvis en ekstern API er nede, behandles de første 10 fejl normalt (hver fejl øger undtagelsestælleren). Yderligere forsøg fejler straks uden at kalde API, indtil der er gået 10 minutter uden en ny undtagelse; vinduet starter forfra ved hver registreret undtagelse.

### `RateLimited`

Håndhæver et maksimalt antal kørsler inden for et tidsinterval. Hvis grænsen overskrides inde i middleware-leddet, udløses en `RateLimitExceededException`; workeren håndterer den som et almindeligt mislykket forsøg, gentager det med den normale backoff og tæller det med i `max_attempts`. Når middleware-leddet har registreret grænsen hos workerens hastighedsbegrænser, bliver efterfølgende jobs for handleren også sat tilbage til `pending`, inden de køres, mens grænsen er nået (se [Hastighedsbegrænsning](https://queuety.inline0.dev/da/docs/features/rate-limiting)).

```php
use Queuety\Middleware\RateLimited;

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

| Parameter | Type | Beskrivelse |
|---|---|---|
| `$max` | `int` | Maksimalt antal tilladte kørsler i perioden |
| `$window` | `int` | Periodens varighed i sekunder |

Ratebegrænseren bruger Queuetys interne `RateLimiter`-instans, så begrænsningerne håndhæves på tværs af alle worker-processer.

### `Timeout`

Armerer en timeout for det enkelte jobs kørsel med `pcntl_alarm` og skal udløse en `TimeoutException`, hvis jobbet kører for længe. Timeouten håndhæves ikke i øjeblikket ([#10](https://github.com/inline0/queuety/issues/10)): Queuety aktiverer ikke asynkrone signaler, så alarmhandleren kører ikke, mens jobbet udføres. Middleware-softwaren nulstiller også alarmen, når den returnerer, hvilket annullerer workerens egen alarm for jobtimeout.

```php
use Queuety\Middleware\Timeout;

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

| Parameter | Type | Beskrivelse |
|---|---|---|
| `$seconds` | `int` | Maksimal køretid i sekunder |

Kræver udvidelsen `pcntl` PHP. Hvis `pcntl` ikke er tilgængelig, fortsætter middleware-softwaren lydløst (ingen timeout håndhæves).

### `UniqueJob`

Forhindrer samtidig kørsel af jobs med samme nøgle. Bruger en låsetabel i databasen for at sikre, at kun én instans kører ad gangen. Hvis en lås allerede er taget, springes jobbet lydløst over (det forsøges ikke igen).

```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
    ];
}
```

| Parameter | Type | Standard | Beskrivelse |
|---|---|---|---|
| `$key` | `string\|null` | `null` (bruger klassenavnet) | Låsenøgle til entydighed |

### `WithoutOverlapping`

Forhindrer overlappende kørsel af jobs med samme logiske nøgle. Låsen udløber automatisk, så den ikke bliver hængende, hvis en worker går ned. Ligner `UniqueJob`, men er beregnet til langvarige handlinger, hvor låsen skal frigives automatisk efter en timeout.

```php
use Queuety\Middleware\WithoutOverlapping;

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

| Parameter | Type | Standard | Beskrivelse |
|---|---|---|---|
| `$key` | `string` | (påkrævet) | Nøgle til den logiske handling |
| `$release_after` | `int` | `300` | Maksimal låsevarighed i sekunder før automatisk frigivelse |

Hvis en lås allerede er taget, springes jobbet lydløst over. Udløbne låse ryddes automatisk op før hvert forsøg på at tage en lås.

## Eksempel: kombination af 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 ),
        ];
    }
}
```

Dette job vil:

1. Blive stoppet, hvis det kører i mere end 5 minutter (Timeout)
2. Blive sprunget over, hvis en anden import for samme leverandør allerede kører (WithoutOverlapping)
3. Blive begrænset til 5 importer i minuttet på tværs af alle leverandører (RateLimited)
