Search by

andydefer / laravel-task

andydefer

A lightweight, file-based task system for Laravel with async execution, recurring tasks, and JSONL storage

Package info

github.com/andydefer/laravel-task

pkg:composer/andydefer/laravel-task

Statistics

Installs: 3 220

Dependents: 5

Suggesters: 0

Stars: 0

Open Issues: 0

v4.15.8 2026-09-30 07:30 UTC

This package is auto-updated.

Last update: 2026-09-30 07:30:15 UTC


README

Un moteur de tâches persistantes pour Laravel. Planification dynamique, exécution récurrente, état, retry, pause, reprise - avec un simple cron.

PHP Version Laravel Version License

Table des matières

  1. Installation
  2. Pourquoi Laravel Task ?
  3. Architecture et concepts clés
  4. Créer une tâche unique
  5. Créer une tâche récurrente
  6. Le noyau de directives (DirectiveKernel)
  7. Exécuter les tâches
  8. Surveillance continue
  9. Exécution parallèle
  10. Gestion des tâches
  11. Directive tasks:list — Lister les tâches
  12. Directive tasks:search — Rechercher une tâche par alias
  13. Circuit Breaker — Protéger les appels critiques
  14. Mode test et fixtures
  15. Cas d'usage concrets
  16. Intégration avec les cron jobs
  17. Bonnes pratiques

Installation

composer require andydefer/laravel-task

php artisan vendor:publish --tag=task-migrations
php artisan migrate

Prérequis : PHP 8.2+ | Laravel 12.x, 13.x, 14.x ou 15.x

Pourquoi Laravel Task ?

Le problème : Vous devez envoyer un email 30 minutes après chaque inscription. Avec Laravel Queue, il vous faut un worker permanent, Supervisor, et généralement un VPS. Sur un hébergement mutualisé, c'est impossible.

La solution : Laravel Task. Des tâches persistantes avec un cycle de vie complet, qui fonctionnent avec un simple cron.

# Un seul cron suffit
* * * * * cd /chemin/projet && ./bin/task tasks:watch --mute

Comparatif rapide

Besoin Scheduler Queue Laravel Task
Tâche "dans 5 minutes" ❌ ✅ ✅
Tâche récurrente avec date de fin ❌ ❌ ✅
Pause / Reprise ❌ ❌ ✅
Retry automatique ❌ ✅ ✅
État et historique ❌ ❌ ✅
Exécution parallèle ❌ ✅ ✅
Fonctionne sur hébergement SHARED ✅ ❌ ✅
Circuit breaker intégré ❌ ❌ ✅

Architecture et concepts clés

Le noyau (DirectiveKernel)

Le package repose sur un noyau de directives (DirectiveKernel) qui orchestre toute l'exécution. Il agit comme un micro-framework de commandes intégré à Laravel.

use AndyDefer\Directive\DirectiveKernel;

$kernel = DirectiveKernel::init($app);
$kernel->addSource('/path/to/directives');

// Exécution d'une directive
$exitCode = $kernel->run(['directive', 'tasks:process']);

Fonctionnalités clés :

  • ✅ Découverte automatique des directives
  • ✅ Indexation BK-Tree pour les suggestions de commandes
  • ✅ Contexte partagé entre les directives
  • ✅ Journalisation des exécutions en JSONL
  • ✅ Mode verbose pour le débogage
  • ✅ Détection de circularité
  • ✅ Arguments variadiques pour filtrer par FQCN

Les directives

Le package fournit cinq directives principales :

Directive Description Utilisation
tasks:process Exécution unique en lot ./bin/task tasks:process
tasks:watch Surveillance continue ./bin/task tasks:watch
tasks:list Liste les tâches persistées ./bin/task tasks:list
tasks:search Recherche par alias ./bin/task tasks:search
fixture:register-tasks Création de tâches de test ./bin/task fixture:register-tasks

Filtrage par FQCN (Arguments variadiques)

Les directives tasks:process et tasks:watch supportent les arguments variadiques pour filtrer les tâches par leur FQCN (Fully Qualified Class Name).

# Exécuter uniquement les tâches spécifiées
./bin/task tasks:process [App.Tasks.SyncUsersTask, App.Tasks.ImportProductsTask]

# Avec limite
./bin/task tasks:process 50 [App.Tasks.SyncUsersTask]

# Dans tasks:watch, le filtre se place après les arguments positionnels
./bin/task tasks:watch 10 300 100 1 [App.Tasks.SyncUsersTask, App.Tasks.ImportProductsTask]

# Avec des flags
./bin/task tasks:process [App.Tasks.SyncUsersTask] --unique-only --verbose

Utilisation des points (.) au lieu des backslashes () :

# ✅ Avec des points (recommandé)
./bin/task tasks:process [App.Tasks.SyncUsersTask, App.Tasks.ImportProductsTask]

# ✅ Avec des backslashes (également accepté)
./bin/task tasks:process [App\\Tasks\\SyncUsersTask, App\\Tasks\\ImportProductsTask]

Dans le code :

// Le filtre FQCN est automatiquement appliqué aux services
$fqcns = $this->getFqcnFilters(); // TaskFqcnVOCollection

$result = $this->limit !== null
    ? $service->process(new LimitVO($this->limit), $callback, $fqcns)
    : $service->process(new LimitVO, $callback, $fqcns);

Les services de tâches

Le package expose deux services principaux qui utilisent des records pour la configuration :

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Contracts\Services\RecurringTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\Records\RecurringTaskConfigRecord;

class MyService
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $uniqueService,
        private readonly RecurringTaskServiceInterface $recurringService
    ) {}
}

Créer une tâche unique

1. Créer la classe

<?php

namespace App\Tasks;

use AndyDefer\Task\Abstract\AbstractUniqueTask;
use AndyDefer\Task\ValueObjects\DescriptionVO;
use AndyDefer\DomainStructures\Utils\StrictDataObject;

class SendWelcomeEmailTask extends AbstractUniqueTask
{
    // ✅ Hook exécuté avant process() - idéal pour la validation
    protected function before(StrictDataObject $payload): void
    {
        if (!$payload->has('email')) {
            throw new \InvalidArgumentException('Email is required');
        }
    }

    // ✅ La logique métier de votre tâche
    protected function process(): void
    {
        $payload = $this->context->getPayload();
        
        $this->info(new DescriptionVO("Sending email to {$payload->email}..."));
        
        // Votre code métier ici
        // Mail::to($payload->email)->send(new WelcomeEmail($payload->name));
        
        $this->info(new DescriptionVO("Email sent to {$payload->email}"));
    }

    // ✅ Hook exécuté après process() - idéal pour la notification
    protected function after(bool $success, ?DescriptionVO $error = null): void
    {
        if ($success) {
            $this->info(new DescriptionVO('Task completed successfully'));
        } else {
            $this->error(new DescriptionVO("Task failed: {$error->getValue()}"));
            // Envoyer une alerte, logger, etc.
        }
    }
}

2. Enregistrer la tâche via le service

<?php

namespace App\Http\Controllers;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\DomainStructures\Utils\StrictDataObject;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;
use AndyDefer\Task\ValueObjects\DurationVO;

class UserController extends Controller
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $taskService
    ) {}

    public function store(Request $request)
    {
        // Création de l'utilisateur...
        
        // ✅ Enregistrement de la tâche avec UniqueTaskConfigRecord
        $config = UniqueTaskConfigRecord::from([
            'scheduled_at' => new Iso8601DateTimeVO(now()->addMinutes(5)),
            'max_attempts' => new MaxAttemptsVO(3),
            'grace_period' => new DurationVO(3600), // 1h
        ]);

        $payload = StrictDataObject::from([
            'email' => $request->email,
            'name' => $request->name,
        ]);

        $alias = $this->taskService->register(
            new UniqueTaskFqcnVO(SendWelcomeEmailTask::class),
            $payload,
            $config
        );

        return response()->json([
            'message' => 'Email planifié dans 5 minutes',
            'task_alias' => $alias->getValue(),
        ]);
    }
}

Créer une tâche récurrente

1. Créer la classe

<?php

namespace App\Tasks;

use AndyDefer\Task\Abstract\AbstractRecurringTask;
use AndyDefer\Task\ValueObjects\DescriptionVO;

class CleanExpiredCacheTask extends AbstractRecurringTask
{
    protected function process(): void
    {
        $this->info(new DescriptionVO('Starting cache cleanup...'));
        
        // ✅ Ici votre code métier exécuté à chaque intervalle
        // Cache::cleanExpired();
        
        $this->info(new DescriptionVO('Cache cleaned successfully'));
    }

    protected function after(bool $success, ?DescriptionVO $error = null): void
    {
        if (!$success) {
            $this->error(new DescriptionVO("Cleanup failed: {$error->getValue()}"));
            // ✅ Alerter l'équipe, envoyer un email, etc.
        }
    }
}

2. Enregistrer la tâche

<?php

namespace App\Console\Commands;

use AndyDefer\Task\Contracts\Services\RecurringTaskServiceInterface;
use AndyDefer\Task\Records\RecurringTaskConfigRecord;
use AndyDefer\Task\ValueObjects\RecurringTaskFqcnVO;
use AndyDefer\DomainStructures\Utils\StrictDataObject;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxFailedAttemptsVO;

class SetupTasksCommand extends Command
{
    public function __construct(
        private readonly RecurringTaskServiceInterface $taskService
    ) {
        parent::__construct();
    }

    public function handle()
    {
        // ✅ Nettoyage toutes les heures, pendant 30 jours
        $config = RecurringTaskConfigRecord::from([
            'interval_seconds' => new DurationVO(3600), // Toutes les heures
            'start_at' => new Iso8601DateTimeVO(now()->toIso8601String()),
            'end_at' => new Iso8601DateTimeVO(now()->addDays(30)->toIso8601String()),
            'max_attempts' => new MaxFailedAttemptsVO(3),
        ]);

        $alias = $this->taskService->register(
            new RecurringTaskFqcnVO(CleanExpiredCacheTask::class),
            StrictDataObject::from(['enabled' => true]),
            $config
        );

        $this->info("Task registered: {$alias->getValue()}");
    }
}

Le noyau de directives (DirectiveKernel)

Le DirectiveKernel est le cœur de l'exécution. Il permet de :

1. Exécuter une directive programmatiquement

<?php

use AndyDefer\Directive\DirectiveKernel;
use AndyDefer\Directive\Enums\ExitCode;

$kernel = DirectiveKernel::init($app);

// Par signature complète
$exitCode = $kernel->runSignature('tasks:process 50 --unique-only --verbose');

// Par FQCN
$exitCode = $kernel->runDirective(
    'AndyDefer\Task\Directives\TasksProcessDirective',
    ['50', '--unique-only']
);

// Par arguments bruts (comme en ligne de commande)
$exitCode = $kernel->run(['directive', 'tasks:process', '50', '--unique-only']);

2. Utiliser le contexte partagé

<?php

$kernel = DirectiveKernel::init($app);

// Définir des données dans le contexte
$context = $kernel->getContext();
$context->put('user_id', 12345);
$context->put('batch_id', 'batch-abc-123');

// Exécuter une directive qui utilise le contexte
$kernel->run(['directive', 'process:user']);

// Récupérer les résultats du contexte
$result = $context->get('process_result');

Exécuter les tâches

Une seule fois (tasks:process)

# Traiter toutes les tâches
./bin/task tasks:process

# Traiter jusqu'à 50 tâches
./bin/task tasks:process 50

# Uniquement les tâches uniques
./bin/task tasks:process --unique-only

# Uniquement les tâches récurrentes
./bin/task tasks:process --recurring-only

# Mode verbeux (voir les erreurs)
./bin/task tasks:process --verbose

# Mode silencieux (pour cron)
./bin/task tasks:process --mute

# Filtrer par FQCN (arguments variadiques)
./bin/task tasks:process [App.Tasks.SyncUsersTask, App.Tasks.ImportProductsTask]

# Combinaison : limite + filtre FQCN
./bin/task tasks:process 50 [App.Tasks.SyncUsersTask]

Surveillance continue (tasks:watch)

La directive tasks:watch exécute tasks:process en boucle avec un intervalle configurable.

# Toutes les 60 secondes (illimité)
./bin/task tasks:watch

# Pendant 1 heure, toutes les 30 secondes
./bin/task tasks:watch 30 3600

# Avec 4 workers parallèles
./bin/task tasks:watch 5 600 100 4 --verbose

# Filtrer par FQCN (arguments variadiques)
./bin/task tasks:watch 10 300 100 1 [App.Tasks.SyncUsersTask, App.Tasks.ImportProductsTask]

# Ignorer un argument avec _ (utiliser la valeur par défaut)
./bin/task tasks:watch 10 _ _ 2 [App.Tasks.SyncUsersTask]

Arguments

Argument Description Défaut
interval Intervalle entre les cycles (minimum 2s) 60
duration Durée totale d'exécution en secondes Illimité
limit Nombre max de tâches par cycle 100
parallel Nombre de workers parallèles 1
fqcnNames* Liste des FQCN à filtrer (variadique) Aucun

Astuce : Utilisez _ (underscore) pour ignorer un argument positionnel et utiliser sa valeur par défaut.

# Seulement parallel et FQCNs (interval et duration par défaut)
./bin/task tasks:watch _ _ _ 4 [App.Tasks.SyncUsersTask]

Exécution parallèle

Le package supporte l'exécution parallèle des tâches via l'option --parallel.

# Exécution séquentielle (par défaut)
./bin/task tasks:watch

# Exécution avec 4 workers parallèles
./bin/task tasks:watch 10 300 100 4 --verbose

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    tasks:watch (parent)                         │
│              CycleCalculator + ResultAggregator                 │
└────────────────────────┬────────────────────────────────────────┘
                         │
          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
┌─────────────────┐┌─────────────────┐┌─────────────────┐
│    Worker 1     ││    Worker 2     ││    Worker 3     │
│   tasks:process ││   tasks:process ││   tasks:process │
│   --unique-only ││   --unique-only ││   --unique-only │
│   --limit=33    ││   --limit=33    ││   --limit=34    │
│   --mute        ││   --mute        ││   --mute        │
└─────────────────┘└─────────────────┘└─────────────────┘

Gestion des tâches

États des tâches uniques

PENDING ──(verrouillage)──▶ IN_PROGRESS
    │                           │
    ├──(succès)───────────────▶ COMPLETED
    │
    ├──(échec max)───────────▶ FAILED
    │
    ├──(annulation)──────────▶ CANCELED
    │
    └──(expiration)──────────▶ FAILED

États des tâches récurrentes

WAITING ──(start_at)──▶ PLAYING
                           │
               ┌───────────┼───────────┐
               │           │           │
               ▼           ▼           ▼
            PAUSED     FINISHED    CANCELED

API de gestion

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Contracts\Services\RecurringTaskServiceInterface;
use AndyDefer\Task\ValueObjects\TaskAliasVO;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\Task\ValueObjects\DescriptionVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\LimitVO;

class TaskManager
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $uniqueService,
        private readonly RecurringTaskServiceInterface $recurringService
    ) {}

    // === TÂCHES UNIQUES ===

    // ✅ Annuler une tâche unique
    public function cancelUnique(string $alias): void
    {
        $this->uniqueService->cancel(
            new TaskAliasVO($alias),
            new DescriptionVO('Canceled by admin')
        );
    }

    // ✅ Reprogrammer à une autre date
    public function reschedule(string $alias, Iso8601DateTimeVO $newDate): void
    {
        $this->uniqueService->reschedule(
            new TaskAliasVO($alias),
            $newDate
        );
    }

    // ✅ Prolonger la période de grâce
    public function extendGracePeriod(string $alias, DurationVO $extraSeconds): void
    {
        $this->uniqueService->extendGracePeriod(
            new TaskAliasVO($alias),
            $extraSeconds
        );
    }

    // ✅ Exécuter une tâche unique manuellement
    public function runUnique(string $alias): TaskRunResultRecord
    {
        return $this->uniqueService->run(new TaskAliasVO($alias));
    }

    // === TÂCHES RÉCURRENTES ===

    // ✅ Mettre en pause
    public function pause(string $alias): void
    {
        $this->recurringService->pause(new TaskAliasVO($alias));
    }

    // ✅ Reprendre
    public function resume(string $alias): void
    {
        $this->recurringService->resume(new TaskAliasVO($alias));
    }

    // ✅ Changer l'intervalle
    public function changeInterval(string $alias, DurationVO $interval): void
    {
        $this->recurringService->changeInterval(
            new TaskAliasVO($alias),
            $interval
        );
    }

    // ✅ Terminer définitivement
    public function finish(string $alias): void
    {
        $this->recurringService->finish(new TaskAliasVO($alias));
    }

    // ✅ Prolonger la date de fin
    public function extendEndAt(string $alias, Iso8601DateTimeVO $newEndAt): void
    {
        $this->recurringService->extendEndAt(
            new TaskAliasVO($alias),
            $newEndAt
        );
    }

    // === INSPECTION ===

    // ✅ Récupérer une tâche
    public function findUnique(string $alias): ?UniqueTaskRecord
    {
        return $this->uniqueService->find(new TaskAliasVO($alias));
    }

    public function findRecurring(string $alias): ?RecurringTaskRecord
    {
        return $this->recurringService->find(new TaskAliasVO($alias));
    }

    // ✅ Compter les tâches
    public function getStats(): array
    {
        return [
            'unique_pending' => $this->uniqueService->countPending()->getValue(),
            'unique_completed' => $this->uniqueService->countCompleted()->getValue(),
            'unique_failed' => $this->uniqueService->countFailed()->getValue(),
            'unique_canceled' => $this->uniqueService->countCanceled()->getValue(),
            'recurring_waiting' => $this->recurringService->countWaiting()->getValue(),
            'recurring_playing' => $this->recurringService->countPlaying()->getValue(),
            'recurring_paused' => $this->recurringService->countPaused()->getValue(),
            'recurring_finished' => $this->recurringService->countFinished()->getValue(),
            'recurring_canceled' => $this->recurringService->countCanceled()->getValue(),
        ];
    }
}

Directive tasks:list — Lister les tâches

La directive tasks:list affiche les tâches uniques et récurrentes persistées dans un tableau, avec filtres par type, statut et FQCN.

Signature

tasks:list
    {limit=50}#"Maximum number of tasks to display"
    {fqcns*}#"Filter by fully qualified class names (dots instead of backslashes)"
    {kinds*>[unique,recurring]}#"Task kinds to display"
    {unique_statuses*>[pending,completed,in_progress,failed,canceled]}#"Unique task statuses to include"
    {recurring_statuses*>[waiting,playing,paused,finished,canceled]}#"Recurring task statuses to include"
Argument Type Défaut Description
limit int 50 Nombre maximum de tâches par statut
fqcns array<string> [] Filtre par FQCN (notation pointée)
kinds array<string> [unique,recurring] Types à afficher
unique_statuses array<string> Tous Statuts de tâches uniques
recurring_statuses array<string> Tous Statuts de tâches récurrentes

Alias

  • tasks:ls
  • t:ls

Ordre des arguments positionnels

Les variadics sont positionnels. Pour ignorer un variadic, passer [].

Position Argument
1 limit
2 fqcns
3 kinds
4 unique_statuses
5 recurring_statuses

Utilisation

# Lister les 50 premières tâches (uniques + récurrentes)
./bin/task tasks:list

# Lister uniquement les tâches récurrentes
./bin/task tasks:list 50 [] [recurring]

# Lister les tâches uniques en échec
./bin/task tasks:list 50 [] [unique] [failed]

# Lister les tâches récurrentes en playing
./bin/task tasks:list 50 [] [recurring] [] [playing]

# Filtrer par FQCN
./bin/task tasks:list 50 [App.Tasks.MyUniqueTask]

# Combiner FQCN, type et statut
./bin/task tasks:list 20 [App.Tasks.MyUniqueTask] [unique] [pending]

# Limiter à 10 résultats
./bin/task tasks:list 10

# Alias
./bin/task t:ls

Colonnes de sortie

Colonne Source unique Source récurrente
Kind unique recurring
Alias alias alias
FQCN fqcn fqcn
Status status status
Next / Last run scheduled_at last_run_at sinon start_at
Attempts attempts failed_attempts

Comportement

  • Filtrage FQCN : en mémoire, après récupération des résultats. Chaque tâche est comparée par égalité stricte.
  • Conversion FQCN : App.Tasks.MyTask → App\Tasks\MyTask.
  • Limite : appliquée par statut, pas globalement.
  • Aucun résultat : affiche ⚠️ No data to display.

Exemple de sortie

🗂️ Tasks
┌───────────┬────────────────────┬─────────────────┬──────────┬──────────────────────┬──────────┐
│ Kind      │ Alias              │ FQCN            │ Status   │ Next / Last run      │ Attempts │
├───────────┼────────────────────┼─────────────────┼──────────┼──────────────────────┼──────────┤
│ unique    │ unique@0192f3a1-...│ App\FooTask     │ pending  │ 2026-06-23T10:00:00Z │ 0        │
│ recurring │ recurring@0192f3a2-│ App\BarTask     │ playing  │ 2026-06-23T11:00:00Z │ 0        │
└───────────┴────────────────────┴─────────────────┴──────────┴──────────────────────┴──────────┘

Directive tasks:search — Rechercher une tâche par alias

La directive tasks:search permet de retrouver une ou plusieurs tâches par leur alias. Elle cherche d'abord dans les tâches uniques, puis dans les tâches récurrentes.

Signature

tasks:search {aliases*}#"Task aliases to search"
Argument Type Obligatoire Description
aliases array<string> ✅ Liste d'alias à rechercher

Les alias sont fournis sous forme de variadic : [alias1, alias2, ...].

Alias

  • tasks:find
  • t:find

Utilisation

# Rechercher une tâche unique
./bin/task tasks:search [unique@0192f3a1-...]

# Rechercher une tâche récurrente
./bin/task tasks:search [recurring@0192f3a2-...]

# Rechercher plusieurs tâches en une fois
./bin/task tasks:search [unique@0192f3a1-..., recurring@0192f3a2-...]

# Rechercher un alias introuvable (retourne FAILURE)
./bin/task tasks:search [unknown@00000000-0000-0000-0000-000000000000]

# Alias
./bin/task tasks:find [unique@0192f3a1-...]
./bin/task t:find [unique@0192f3a1-...]

Ordre de recherche

Priorité Service Condition
1 UniqueTaskServiceInterface::find() Si retour non-null, l'alias est considéré trouvé
2 RecurringTaskServiceInterface::find() Consulté uniquement si le précédent retourne null

Un alias n'est jamais recherché deux fois : la première correspondance gagne.

Comportement

Cas ExitCode Sortie
Toutes les alias trouvées SUCCESS Tableau des tâches
Certaines alias non trouvées FAILURE Tableau + avertissements
Aucune alias trouvée FAILURE Erreur + avertissements
Aucun alias fourni (exception kernel) At least one alias is required.

Colonnes de sortie

Colonne Source unique Source récurrente
Kind unique recurring
Alias alias alias
FQCN fqcn fqcn
Status status status
Next / Last run scheduled_at last_run_at sinon start_at
Attempts attempts failed_attempts

Exemple de sortie

🔎 Tasks
┌───────────┬─────────────────────────┬───────────────┬──────────┬──────────────────────┬──────────┐
│ Kind      │ Alias                   │ FQCN          │ Status   │ Next / Last run      │ Attempts │
├───────────┼─────────────────────────┼───────────────┼──────────┼──────────────────────┼──────────┤
│ unique    │ unique@0192f3a1-...     │ App\FooTask   │ pending  │ 2026-06-23T10:00:00Z │ 0        │
│ recurring │ recurring@0192f3a2-...  │ App\BarTask   │ playing  │ 2026-06-23T11:00:00Z │ 0        │
└───────────┴─────────────────────────┴───────────────┴──────────┴──────────────────────┴──────────┘

Points d'attention

  • Pas de _ — chaque alias est une valeur réelle dans le variadic.
  • Recherche séquentielle : unique d'abord, puis recurring.
  • Signale les alias introuvables un par un, sans interrompre le traitement.

Circuit Breaker — Protéger les appels critiques

Le package embarque un circuit breaker prêt à l'emploi, basé sur le cache Laravel. Il protège n'importe quelle opération dont l'échec répété est coûteux : appel HTTP, accès base de données, lecture de fichier partagé, requête sur un service interne.

Principe

CLOSED ──── (≥ failureThreshold échecs) ────▶ OPEN
OPEN   ──── (≥ openSeconds écoulées) ──────▶ HALF_OPEN
HALF_OPEN ─ (≥ successThreshold succès) ───▶ CLOSED
HALF_OPEN ─ (1 échec) ─────────────────────▶ OPEN
  • CLOSED : fonctionnement normal. Les échecs sont comptés.
  • OPEN : toute exécution est refusée par CircuitOpenException.
  • HALF_OPEN : après openSeconds, quelques essais sont autorisés. successThreshold succès consécutifs referment le circuit.

Utilisation dans une tâche

Ajoute le trait WithCircuitBreaker à ta tâche :

<?php

declare(strict_types=1);

namespace App\Tasks;

use AndyDefer\Task\Abstract\AbstractUniqueTask;
use AndyDefer\Task\CircuitBreaker\Concerns\WithCircuitBreaker;
use AndyDefer\Task\ValueObjects\DescriptionVO;

final class SendFcmNotificationTask extends AbstractUniqueTask
{
    use WithCircuitBreaker;

    protected function process(): void
    {
        $this->withBreaker('firebase.fcm', function (): void {
            // Appel à l'API HTTP v1 de FCM
        });

        $this->info(new DescriptionVO('FCM notification sent.'));
    }
}

Opt-in : les tâches qui n'utilisent pas le trait ne sont pas affectées. Aucune signature, aucun constructeur, aucun contrat d'interface n'est modifié.

Utilisation directe

<?php

declare(strict_types=1);

use AndyDefer\Task\CircuitBreaker\CircuitBreaker;
use AndyDefer\Task\CircuitBreaker\ValueObjects\CircuitBreakerKeyVO;
use Illuminate\Contracts\Cache\Repository as CacheRepository;

$breaker = CircuitBreaker::create(
    new CircuitBreakerKeyVO('webhook.partner-x'),
    app(CacheRepository::class),
    failureThreshold: 5,
    successThreshold: 2,
    openSeconds: 60,
);

$payload = $breaker->execute(function (): array {
    return Http::timeout(5)->get('https://partner.example.com/hook')->json();
});

Choix de la clé

La clé identifie la ressource protégée, pas la tâche. Deux appels avec la même clé partagent le même breaker.

Clé suggérée Ressource protégée
firebase.fcm API Firebase Cloud Messaging
webpush.mozilla Service Push de Mozilla
db.replica-eu Réplique DB Europe
storage.s3 Bucket S3
webhook.partner-x Webhook partenaire X
mail.smtp Serveur SMTP

Gestion des erreurs

use AndyDefer\Task\CircuitBreaker\Exceptions\CircuitOpenException;

try {
    $breaker->execute(fn () => $this->callExternalApi());
} catch (CircuitOpenException $e) {
    // Le circuit est ouvert : ne pas insister.
    logger()->warning($e->getMessage());
}

Comportement en cas d'échec

  • Toute exception levée par le callback est propagée après incrément du compteur d'échecs.
  • L'exception d'origine n'est jamais remplacée par une exception du breaker.
  • Le compteur d'échecs est réinitialisé à chaque succès en état CLOSED.

API

Méthode Description
CircuitBreaker::create(...) Construit un breaker (seuils clampés à ≥ 1)
execute(callable $callback) Exécute protégé par le breaker
state(): CircuitBreakerState État courant (CLOSED, OPEN, HALF_OPEN)
recordSuccess() Enregistre un succès manuellement
recordFailure() Enregistre un échec manuellement
reset() Force l'état à CLOSED et efface les compteurs

Configuration

Aucune configuration obligatoire. Le breaker utilise le store de cache par défaut. Pour cibler un store spécifique :

$breaker = CircuitBreaker::create(
    new CircuitBreakerKeyVO('my.resource'),
    Cache::store('redis'),
    failureThreshold: 5,
    successThreshold: 2,
    openSeconds: 60,
);

Points d'attention

  • Backend de cache : redis et database offrent des incréments atomiques. array est local au processus.
  • Concurrence : deux workers peuvent incrémenter simultanément ; le seuil peut être franchi avec une tolérance de quelques unités.
  • Aucun verrou : le breaker est conçu pour être tolérant aux races bénignes.
  • Clés de cache : préfixées par task:circuit: pour éviter les collisions.

Cas d'usage concrets

1. SaaS - Abonnements et facturation

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\DomainStructures\Utils\StrictDataObject;

class SubscriptionService
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $taskService
    ) {}

    public function createSubscription(User $user, Plan $plan): void
    {
        // ✅ Rappel J-1 avant expiration
        $this->taskService->register(
            new UniqueTaskFqcnVO(RenewalReminderTask::class),
            StrictDataObject::from([
                'user_id' => $user->id,
                'email' => $user->email,
                'plan' => $plan->name,
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO($user->subscription_end_at->subDay()),
                'max_attempts' => new MaxAttemptsVO(2),
                'grace_period' => new DurationVO(3600),
            ])
        );

        // ✅ Désactivation à la date d'expiration
        $this->taskService->register(
            new UniqueTaskFqcnVO(ExpireSubscriptionTask::class),
            StrictDataObject::from([
                'user_id' => $user->id,
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO($user->subscription_end_at),
                'max_attempts' => new MaxAttemptsVO(3),
                'grace_period' => new DurationVO(7200),
            ])
        );

        // ✅ Relance en cas de paiement échoué
        $this->taskService->register(
            new UniqueTaskFqcnVO(PaymentRetryTask::class),
            StrictDataObject::from([
                'user_id' => $user->id,
                'payment_id' => $payment->id,
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO(now()->addHours(24)),
                'max_attempts' => new MaxAttemptsVO(3),
                'grace_period' => new DurationVO(86400),
            ])
        );
    }
}

2. E-commerce - Paniers abandonnés

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\DomainStructures\Utils\StrictDataObject;

class AbandonedCartService
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $uniqueService
    ) {}

    public function handleAbandonedCart(Cart $cart, User $user): void
    {
        // ✅ Email de relance 30 min après abandon
        $this->uniqueService->register(
            new UniqueTaskFqcnVO(AbandonedCartReminderTask::class),
            StrictDataObject::from([
                'cart_id' => $cart->id,
                'user_id' => $user->id,
                'items' => $cart->items,
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO(now()->addMinutes(30)),
                'max_attempts' => new MaxAttemptsVO(2),
                'grace_period' => new DurationVO(3600),
            ])
        );

        // ✅ Email de suivi J+3
        $this->uniqueService->register(
            new UniqueTaskFqcnVO(FollowUpEmailTask::class),
            StrictDataObject::from([
                'user_id' => $user->id,
                'cart_id' => $cart->id,
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO(now()->addDays(3)),
                'max_attempts' => new MaxAttemptsVO(2),
                'grace_period' => new DurationVO(3600),
            ])
        );
    }
}

3. Intégrations API - Webhooks avec retry et circuit breaker

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;
use AndyDefer\Task\ValueObjects\DurationVO;

class WebhookService
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $uniqueService
    ) {}

    public function sendWebhook($event, $data): string
    {
        // ✅ Appel API avec retry automatique
        // La tâche elle-même utilisera WithCircuitBreaker pour protéger l'appel.
        $config = UniqueTaskConfigRecord::from([
            'scheduled_at' => new Iso8601DateTimeVO(now()->addSeconds(5)),
            'max_attempts' => new MaxAttemptsVO(5),
            'grace_period' => new DurationVO(7200), // 2h
        ]);

        $alias = $this->uniqueService->register(
            new UniqueTaskFqcnVO(SendWebhookTask::class),
            StrictDataObject::from([
                'url' => config('webhooks.endpoint'),
                'event' => $event,
                'data' => $data,
            ]),
            $config
        );

        return $alias->getValue();
    }
}

4. Maintenance - Nettoyage et backups

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\RecurringTaskServiceInterface;
use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\RecurringTaskConfigRecord;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\RecurringTaskFqcnVO;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxFailedAttemptsVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;

class MaintenanceService
{
    public function __construct(
        private readonly RecurringTaskServiceInterface $recurringService,
        private readonly UniqueTaskServiceInterface $uniqueService
    ) {}

    public function scheduleMaintenance(): void
    {
        // ✅ Nettoyage toutes les heures
        $this->recurringService->register(
            new RecurringTaskFqcnVO(CacheCleanTask::class),
            StrictDataObject::from(['enabled' => true]),
            RecurringTaskConfigRecord::from([
                'interval_seconds' => new DurationVO(3600),
                'start_at' => new Iso8601DateTimeVO(now()->toIso8601String()),
                'max_attempts' => new MaxFailedAttemptsVO(3),
            ])
        );

        // ✅ Backup DB à 2h du matin
        $this->uniqueService->register(
            new UniqueTaskFqcnVO(BackupDatabaseTask::class),
            StrictDataObject::from([
                'database' => config('database.connections.mysql.database'),
                'backup_path' => storage_path('backups'),
            ]),
            UniqueTaskConfigRecord::from([
                'scheduled_at' => new Iso8601DateTimeVO(Carbon::now()->setTime(2, 0)),
                'max_attempts' => new MaxAttemptsVO(1),
                'grace_period' => new DurationVO(3600),
            ])
        );
    }
}

5. Workflow - Orchestration en plusieurs étapes

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\UniqueTaskServiceInterface;
use AndyDefer\Task\Records\UniqueTaskConfigRecord;
use AndyDefer\Task\ValueObjects\UniqueTaskFqcnVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxAttemptsVO;
use AndyDefer\Task\ValueObjects\DurationVO;

class OrderWorkflowService
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $uniqueService
    ) {}

    public function processOrder(Order $order): void
    {
        $payload = StrictDataObject::from([
            'order_id' => $order->id,
            'user_id' => $order->user_id,
            'total' => $order->total,
        ]);

        $steps = [
            ValidateOrderTask::class => now()->addSeconds(10),
            ProcessPaymentTask::class => now()->addSeconds(30),
            GenerateInvoiceTask::class => now()->addMinutes(1),
            SendConfirmationTask::class => now()->addMinutes(2),
        ];

        foreach ($steps as $class => $scheduledAt) {
            $this->uniqueService->register(
                new UniqueTaskFqcnVO($class),
                $payload,
                UniqueTaskConfigRecord::from([
                    'scheduled_at' => new Iso8601DateTimeVO($scheduledAt),
                    'max_attempts' => new MaxAttemptsVO(2),
                    'grace_period' => new DurationVO(3600),
                ])
            );
        }
    }
}

6. Campaigns marketing - Newsletters temporaires

<?php

namespace App\Services;

use AndyDefer\Task\Contracts\Services\RecurringTaskServiceInterface;
use AndyDefer\Task\Records\RecurringTaskConfigRecord;
use AndyDefer\Task\ValueObjects\RecurringTaskFqcnVO;
use AndyDefer\Task\ValueObjects\DurationVO;
use AndyDefer\Task\ValueObjects\Iso8601DateTimeVO;
use AndyDefer\Task\ValueObjects\MaxFailedAttemptsVO;

class CampaignService
{
    public function __construct(
        private readonly RecurringTaskServiceInterface $recurringService
    ) {}

    public function startCampaign(Campaign $campaign): void
    {
        // ✅ Newsletter hebdomadaire sur 4 semaines
        $this->recurringService->register(
            new RecurringTaskFqcnVO(NewsletterTask::class),
            StrictDataObject::from([
                'campaign_id' => $campaign->id,
                'template' => $campaign->template,
            ]),
            RecurringTaskConfigRecord::from([
                'interval_seconds' => new DurationVO(604800), // 7 jours
                'start_at' => new Iso8601DateTimeVO(now()->toIso8601String()),
                'end_at' => new Iso8601DateTimeVO(now()->addWeeks(4)->toIso8601String()),
                'max_attempts' => new MaxFailedAttemptsVO(2),
            ])
        );
    }
}

Intégration avec les cron jobs

Configuration de base

# Exécution toutes les minutes
* * * * * cd /var/www/project && ./bin/task tasks:watch 30 --mute >> /var/log/tasks-watch.log 2>&1

# Exécution des tâches uniques toutes les 5 minutes
*/5 * * * * cd /var/www/project && ./bin/task tasks:process 100 --unique-only --mute >> /var/log/tasks-unique.log 2>&1

# Exécution des tâches récurrentes toutes les heures
0 * * * * cd /var/www/project && ./bin/task tasks:process --recurring-only --mute >> /var/log/tasks-recurring.log 2>&1

Bonnes pratiques

✅ Injection de services

// BON
class UserController
{
    public function __construct(
        private readonly UniqueTaskServiceInterface $taskService
    ) {}
}

✅ Utiliser des Value Objects pour les dates

// BON
new Iso8601DateTimeVO(now()->addMinutes(5))

// ÉVITER
$config['scheduled_at'] = now()->addMinutes(5);

✅ Structure des payloads avec StrictDataObject

$payload = StrictDataObject::from([
    'user_id' => $user->id,
    'email' => $user->email,
    'metadata' => [
        'source' => 'registration',
        'timestamp' => now()->toIso8601String(),
    ],
]);

✅ Utiliser des limites

# Éviter les surcharges
./bin/task tasks:process 100
./bin/task tasks:watch 30 300 50

✅ Protéger les appels critiques avec un circuit breaker

final class SendFcmNotificationTask extends AbstractUniqueTask
{
    use WithCircuitBreaker;

    protected function process(): void
    {
        $this->withBreaker('firebase.fcm', function (): void {
            // Appel protégé
        });
    }
}

Une clé = une ressource protégée. Ne pas mélanger plusieurs ressources sous la même clé. Ne pas réutiliser la même clé entre deux services distincts.

Licence

MIT © Andy Defer