
# Tâches

Les tâches sont l'unité de travail fondamentale dans Queuety. Déclenchez une tâche avec le nom d'un gestionnaire et des données utiles ; un worker la récupère et l'exécute en arrière-plan.

## Déclencher des tâches

```php
use Queuety\Queuety;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] );
```

`dispatch()` renvoie un générateur `PendingJob`. Vous pouvez enchaîner des options avant son déclenchement automatique :

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High )
    ->delay( 300 )
    ->max_attempts( 5 );
```

Pour obtenir immédiatement l'ID de la tâche, appelez `->id()` :

```php
$job_id = Queuety::dispatch( 'send_email', $payload )->id();
```

## Écrire des gestionnaires

Un gestionnaire peut être n'importe quelle classe qui implémente l'interface `Queuety\Handler` :

```php
use Queuety\Handler;

class SendEmailHandler implements Handler {
    public function handle( array $payload ): void {
        wp_mail( $payload['to'], $payload['subject'], $payload['body'] );
    }

    public function config(): array {
        return [
            'queue'           => 'emails',
            'max_attempts'    => 5,
            'backoff'         => 'exponential',
        ];
    }
}
```

La méthode `handle()` reçoit les données utiles de la tâche et effectue le traitement. La méthode `config()` renvoie des valeurs par défaut facultatives :

| Clé | Type | Description |
|---|---|---|
| `queue` | `string` | Nom de la file par défaut |
| `max_attempts` | `int` | Nombre maximal d'exécutions, première exécution comprise, avant la mise à l'écart de la tâche |
| `backoff` | `string` | Stratégie de délai progressif des nouvelles tentatives : `exponential`, `linear` ou `fixed` |
| `rate_limit` | `array` | Limite de débit sous la forme `[max_executions, window_seconds]` |
| `concurrency_group` | `string` | Groupe d'admission partagé des workers pour les gestionnaires coûteux |
| `concurrency_limit` | `int` | Nombre maximal de tâches traitées simultanément dans ce groupe |
| `cost_units` | `int` | Coût d'exécution relatif utilisé par les budgets pondérés de file ou de fournisseur, les budgets de workflow et les heuristiques d'admission |

Ces clés de ressources font partie du modèle plus général de [gestion des ressources](https://queuety.inline0.dev/fr/docs/features/resource-management) de Queuety, où les métadonnées des tâches, les budgets de workflow et l'admission des workers fonctionnent de concert.

Enregistrez le gestionnaire :

```php
Queuety::register( 'send_email', SendEmailHandler::class );
```

Ou utilisez les attributs [PHP](https://queuety.inline0.dev/fr/docs/features/attributes) pour l’enregistrement automatique.

## Priorité

Les tâches ont quatre niveaux de priorité. Les tâches les plus prioritaires sont prises en charge en premier par les workers.

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'handler', $payload )->with_priority( Priority::Urgent );
```

| Niveau | Valeur | Description |
|---|---|---|
| `Priority::Low` | `0` | Par défaut. Travail en arrière-plan qui peut attendre. |
| `Priority::Normal` | `1` | Priorité standard. |
| `Priority::High` | `2` | Traité avant les tâches de priorité basse et normale. |
| `Priority::Urgent` | `3` | Traité en premier. À utiliser avec parcimonie. |

## Délai

Retardez une tâche pour qu’elle ne soit disponible qu’après le nombre de secondes indiqué :

```php
Queuety::dispatch( 'send_reminder', $payload )->delay( 3600 ); // 1 hour
```

La tâche est insérée immédiatement, mais les workers ne la prendront en charge qu’une fois le délai écoulé.

## Tâches uniques

Empêchez les envois en double pour un même gestionnaire et une même charge utile. Si une tâche en attente ou en cours de traitement existe déjà avec le même gestionnaire et la même charge utile, son ID est renvoyé au lieu d’en créer une nouvelle.

```php
Queuety::dispatch( 'sync_inventory', [ 'sku' => 'ABC-123' ] )->unique();
```

## Dépendances entre tâches

Une tâche peut dépendre d’une autre. Un worker ne la prendra en charge qu’une fois la dépendance terminée.

```php
$first_id = Queuety::dispatch( 'import_data', $payload )->id();

Queuety::dispatch( 'send_notification', [ 'import_id' => $first_id ] )
    ->after( $first_id );
```

## Envoi par lot

Envoyez plusieurs tâches en une seule instruction INSERT à plusieurs lignes pour un débit maximal :

```php
$job_ids = Queuety::batch( [
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'a@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'b@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'c@example.com' ] ],
] );
```

Chaque élément peut inclure les clés facultatives suivantes : `queue`, `priority`, `delay`, `max_attempts`.

## Nouvelle tentative et délai exponentiel

Lorsqu’une tâche lève une exception, Queuety la remet dans `pending` avec un `available_at` différé jusqu’à ce qu’elle ait été exécutée `max_attempts` fois au total (la première exécution compte, donc `max_attempts( 3 )` signifie jusqu’à deux nouvelles tentatives). Le délai avant chaque nouvelle tentative est déterminé par la stratégie de temporisation, où `attempt` désigne le numéro de l’exécution qui vient d’échouer :

| Stratégie | Formule du délai | Exemple (après les exécutions 1, 2 et 3) |
|---|---|---|
| `exponential` | `2^attempt * 30` secondes, plafonné à 3600 | 60s, 120s, 240s |
| `linear` | `attempt * 60` secondes | 60s, 120s, 180s |
| `fixed` | `60` secondes | 60s, 60s, 60s |

Définissez la valeur par défaut globale dans `wp-config.php` :

```php
define( 'QUEUETY_RETRY_BACKOFF', 'linear' );
```

Ou définissez-la pour chaque gestionnaire dans `config()` :

```php
public function config(): array {
    return [ 'backoff' => 'exponential', 'max_attempts' => 5 ];
}
```

Ou pour chaque envoi :

```php
Queuety::dispatch( 'handler', $payload )->max_attempts( 10 );
```

## Tâches en attente définitive

Après avoir épuisé toutes les tentatives, une tâche est marquée comme **enterrée**. Les tâches enterrées restent dans la base de données pour inspection, mais ne sont pas traitées. Vous pouvez les relancer manuellement. Une relance manuelle remet la tâche dans `pending` sans réinitialiser son nombre de tentatives. Une tâche enterrée relancée bénéficie donc d’une exécution supplémentaire et sera de nouveau enterrée si cette exécution échoue :

```php
// Retry a specific buried job
Queuety::retry( $job_id );

// Retry all buried jobs
$count = Queuety::retry_buried();
```

Ou via CLI :

```bash
wp queuety retry 42
wp queuety retry-buried
```

## Délai d'attente

Queuety déclenche une `pcntl_alarm()` pour la durée maximale d’exécution et est censé faire échouer la tentative avec une `TimeoutException` si cette durée est dépassée. Ce délai n’est actuellement pas appliqué ([#10](https://github.com/inline0/queuety/issues/10)) : le worker n’active pas les signaux asynchrones. Un gestionnaire de longue durée s’exécute donc jusqu’au bout, et un gestionnaire bloqué dans `sleep()` est interrompu au déclenchement de l’alarme, puis marqué comme terminé. Configurez le délai d’expiration globalement :

```php
define( 'QUEUETY_MAX_EXECUTION_TIME', 300 ); // 5 minutes
```

Les tâches dont le statut `processing` persiste plus de `QUEUETY_STALE_TIMEOUT` secondes sont automatiquement récupérées :

```php
define( 'QUEUETY_STALE_TIMEOUT', 600 ); // 10 minutes
```

Récupérez manuellement les tâches obsolètes :

```bash
wp queuety recover
```

## Files d’attente

Les tâches sont envoyées vers des files d’attente nommées. Chaque worker traite une file ou une liste de files dans un ordre défini (voir [Workers](https://queuety.inline0.dev/fr/docs/features/workers#multi-queue-priorities)) :

```php
Queuety::dispatch( 'handler', $payload )->on_queue( 'emails' );
```

```bash
wp queuety work --queue=emails
```

La file d’attente par défaut est `default`. Vous pouvez exécuter des processus worker distincts pour chaque file afin d’isoler les charges de travail.

## Limitation du débit

Limitez la fréquence à laquelle un gestionnaire peut s’exécuter sur une période donnée :

```php
Queuety::dispatch( 'call_api', $payload )
    ->rate_limit( 10, 60 ); // max 10 executions per 60 seconds
```

Consultez [Limitation du débit](https://queuety.inline0.dev/fr/docs/features/rate-limiting) pour en savoir plus.

## Classes de tâches envoyables

Queuety v0.6.0 introduit un API de tâches fondé sur des classes. Au lieu de séparer le nom du gestionnaire, la charge utile et la classe du gestionnaire, vous définissez une seule classe qui regroupe tout : la charge utile (sous forme de propriétés publiques typées), la logique d’exécution et les middlewares facultatifs.

### L’interface `Contracts\Job`

```php
use Queuety\Contracts\Job;

interface Job {
    public function handle(): void;
}
```

Une classe `Job` stocke sa charge utile dans des propriétés publiques et exécute sa propre logique dans `handle()`. Aucune méthode `config()` distincte ni inscription d’un gestionnaire n’est nécessaire.

### Le trait `Dispatchable`

Ajoutez le trait `Dispatchable` pour obtenir une méthode statique `dispatch()` dans votre classe de tâche :

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class SendEmailJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $to,
        public readonly string $subject,
        public readonly string $body,
    ) {}

    public function handle(): void {
        wp_mail( $this->to, $this->subject, $this->body );
    }
}
```

Envoyez-la en passant les arguments du constructeur :

```php
SendEmailJob::dispatch( 'user@example.com', 'Welcome!', 'Hello there.' )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High );
```

Le trait `Dispatchable` appelle `Queuety::dispatch()` en interne. Toutes les options `PendingJob` (queue, priority, delay, max_attempts, unique, after) fonctionnent donc de la même manière.

### Fonctionnement de la sérialisation

Lorsque vous envoyez une instance de `Job`, le `JobSerializer` de Queuety extrait le nom de classe qualifié complet pour en faire le gestionnaire, ainsi que toutes les propriétés publiques pour constituer la charge utile. Côté worker, le sérialiseur reconstruit l’instance en faisant correspondre les clés de la charge utile aux noms des paramètres du constructeur. Les énumérations adossées sont sérialisées sous leur valeur sous-jacente et restaurées automatiquement.

### Propriétés des tâches

Les classes de tâches peuvent déclarer des propriétés publiques pour contrôler le comportement des nouvelles tentatives et des délais d’expiration. Le worker les lit par réflexion et les applique automatiquement.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ImportProductsJob implements Job {
    use Dispatchable;

    public int $tries = 5;
    public int $timeout = 120;
    public array $backoff = [ 10, 30, 60, 120 ];
    public string $concurrency_group = 'imports';
    public int $concurrency_limit = 2;
    public int $cost_units = 4;

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

    public function handle(): void {
        // Import products...
    }
}
```

| Propriété | Type | Description |
|---|---|---|
| `$tries` | `int` | Nombre maximal de tentatives avant que la tâche soit enterrée. Remplace la valeur `max_attempts` définie au moment de l’envoi. |
| `$timeout` | `int` | Durée maximale d’exécution en secondes. Remplace la constante globale `QUEUETY_MAX_EXECUTION_TIME`. Nécessite `pcntl` et n’est pas encore appliquée (voir [Délai d’expiration](https://queuety.inline0.dev/fr/docs/jobs#timeout)). |
| `$backoff` | `array` | Tableau des délais en secondes pour chaque nouvelle tentative. La dernière valeur est réutilisée pour les tentatives suivantes. |
| `$concurrency_group` | `string` | Groupe partagé d’admission des workers pour les tâches coûteuses. |
| `$concurrency_limit` | `int` | Nombre maximal de tâches `processing` simultanées dans ce groupe. Ignoré sans `$concurrency_group`. |
| `$cost_units` | `int` | Coût d’exécution relatif utilisé par les budgets de workflow et l’admission des workers. |

Le tableau `$backoff` définit des délais croissants entre les nouvelles tentatives. Par exemple, `[10, 30, 60, 120]` signifie que la première nouvelle tentative attend 10 secondes, la deuxième 30 secondes, la troisième 60 secondes, et que les suivantes attendent 120 secondes.

Les propriétés liées aux ressources sont utiles lorsque les classes de tâches représentent différents niveaux de coût. Un rafraîchissement rapide du cache et un appel coûteux à un fournisseur peuvent tous deux constituer « une tâche », mais ils ne devraient pas forcément être comptabilisés de la même manière lorsqu’un budget de workflow ou une décision d’admission des workers vise à maintenir le processus dans des limites sûres.

Pour découvrir le modèle complet, y compris les budgets de workflow et l’admission côté worker, consultez [Gestion des ressources](https://queuety.inline0.dev/fr/docs/features/resource-management).

### Hook d’échec

Lorsqu’une tâche est enterrée après avoir épuisé toutes ses tentatives, Queuety appelle une méthode `failed()` sur l’instance de la tâche, si elle est définie. Utilisez-la pour le nettoyage, les alertes ou la journalisation.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ProcessPaymentJob implements Job {
    use Dispatchable;

    public int $tries = 3;

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

    public function handle(): void {
        // Process the payment...
    }

    public function failed( \Throwable $exception ): void {
        // Notify the team that payment processing failed permanently.
        error_log( "Payment failed for order {$this->order_id}: {$exception->getMessage()}" );
    }
}
```

La méthode `failed()` reçoit l’exception à l’origine de l’échec final. L’instance de la tâche est désérialisée avec sa charge utile d’origine ; toutes ses propriétés publiques sont donc disponibles.

### Envoi conditionnel

Le trait `Dispatchable` fournit `dispatch_if()` et `dispatch_unless()` pour effectuer des envois conditionnels. Les deux méthodes acceptent une condition booléenne comme premier argument, suivie des arguments du constructeur de la tâche.

```php
// Only dispatch if the user has opted in to notifications.
SendEmailJob::dispatch_if( $user->wants_notifications, $user->email, 'Update', $body );

// Dispatch unless the feature flag is disabled.
SyncInventoryJob::dispatch_unless( $maintenance_mode, $sku );
```

`dispatch_if()` envoie la tâche si la condition est `true` et renvoie le `PendingJob`. Elle renvoie `null` si la condition est `false`.

`dispatch_unless()` envoie la tâche si la condition est `false` et renvoie le `PendingJob`. Elle renvoie `null` si la condition est `true`.

Les deux méthodes acceptent les options `PendingJob` chaînables lors de l’envoi :

```php
SendEmailJob::dispatch_if( $should_send, $to, $subject, $body )
    ?->on_queue( 'emails' )
    ?->with_priority( Priority::High );
```

### Envoi synchrone

Utilisez `Queuety::dispatch_sync()` pour exécuter immédiatement une tâche dans le processus actuel, sans l’ajouter à la file d’attente. C’est utile pour les tests et les tâches qui doivent s’exécuter en ligne.

```php
use Queuety\Queuety;

// With a Job instance
Queuety::dispatch_sync( new SendEmailJob( to: 'user@example.com', subject: 'Hello', body: 'Hi!' ) );

// With a handler name and payload
Queuety::dispatch_sync( 'send_email', [ 'to' => 'user@example.com' ] );
```

La méthode `handle()` de la tâche est appelée directement. Aucune ligne n’est créée dans la base de données, aucun worker n’intervient et aucun intergiciel n’est appliqué.

### Enchaînement de tâches

L’enchaînement de tâches exécute une série de tâches l’une après l’autre. Chaque tâche de la chaîne dépend de la réussite de la précédente. Si une tâche échoue, les suivantes restent en attente.

#### Utiliser `Queuety::chain()`

```php
use Queuety\Queuety;

$first_job_id = Queuety::chain( [
    new FetchDataJob( $url ),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->catch( ChainFailedHandler::class )
    ->dispatch();
```

`Queuety::chain()` renvoie un `ChainBuilder` avec des méthodes chaînables pour définir la file d’attente et un gestionnaire d’échec. La méthode `dispatch()` crée toutes les tâches avec des relations `depends_on` séquentielles et renvoie l’ID de la première tâche.

#### Utiliser `with_chain()`

Le trait `Dispatchable` fournit une méthode `with_chain()`. Elle est actuellement équivalente à `Queuety::chain( $jobs )` : la classe sur laquelle elle est appelée n’est pas ajoutée à la chaîne ([#13](https://github.com/inline0/queuety/issues/13)). Il faut donc aussi indiquer la première tâche dans le tableau :

```php
FetchDataJob::with_chain( [
    new FetchDataJob(),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->dispatch();
```

#### Gestion des échecs de chaîne

Enregistrez une classe de gestion des échecs avec `catch()`. Cette classe doit disposer d’une méthode `handle()`. Elle est appelée lorsqu’une tâche de la chaîne est définitivement enterrée. Le worker prend en charge `handle()`, `handle( Job $job )` et `handle( Job $job, \Throwable $exception )`.

```php
class ChainFailedHandler {
    public function handle(): void {
        // Alert, clean up, etc.
    }
}
```

### Ajouter des middlewares

Les classes de tâches peuvent définir une méthode `middleware()` qui renvoie les middlewares chargés d’encapsuler l’exécution :

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

class CallOpenAIJob implements Job {
    use Dispatchable;

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

    public function handle(): void {
        // Call the API...
    }

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

Consultez [Middlewares](https://queuety.inline0.dev/fr/docs/features/middleware) pour en savoir plus sur les middlewares intégrés et la création de middlewares personnalisés.

### Comparaison avec les tâches fondées sur des gestionnaires

| | Fondées sur des gestionnaires (v0.1+) | Fondées sur des classes (v0.6+) |
|---|---|---|
| Envoi | `Queuety::dispatch( 'send_email', $payload )` | `SendEmailJob::dispatch( $to, $subject )` |
| Gestionnaire | Classe distincte implémentant `Handler` | La même classe implémente `Contracts\Job` |
| Charge utile | `array $payload` non typée | Propriétés publiques typées |
| Inscription | `Queuety::register()` ou `#[QueuetyHandler]` | Non nécessaire (utilise FQCN) |
| Middleware | Non disponible | Méthode `middleware()` facultative |
| Configuration | Méthode `config()` | Propriétés de tâche + chaîne `PendingJob` |

Les deux approches sont entièrement prises en charge. Les tâches existantes fondées sur des gestionnaires continuent de fonctionner sans modification. Vous pouvez combiner les deux styles dans un même projet.

## Cycle de vie d’une tâche

```
dispatch() -> pending -> processing -> completed
                  |           |
                  |           +-> exception -> pending (retry after backoff)
                  |                       |
                  |                       +-> buried (max attempts reached)
                  |
                  +-> delayed (available_at in the future)
```

| Statut | Description |
|---|---|
| `pending` | En attente de prise en charge par un worker |
| `processing` | En cours d’exécution |
| `completed` | Terminée avec succès |
| `failed` | Réservée ; le worker ne définit pas ce statut actuellement. Une tentative échouée qui sera relancée revient à `pending` |
| `buried` | Toutes les tentatives ont été épuisées |
