laravelqueuesenumsphp

Enums en Colas Laravel: Tipado Fuerte sin Errores

Enums en Colas Laravel: Tipado Fuerte sin Errores

Las colas (queues) son fundamentales en cualquier aplicación Laravel moderna. Sin embargo, un problema recurrente es manejar overlap keys —identificadores que previenen que jobs duplicados se ejecuten simultáneamente— de forma segura y mantenible.

Desde Laravel 12.64.0, puedes usar Enums como overlap keys en lugar de strings frágiles. Esto añade tipado fuerte, autocomplete en tu IDE y previene errores en tiempo de ejecución.

En este artículo aprenderás cómo implementar esta característica y transformar tus colas en código más robusto y profesional.

¿Qué es una Overlap Key en Laravel?

Una overlap key es un identificador único que Laravel usa para evitar que múltiples instancias del mismo job se ejecuten al mismo tiempo. Sin esto, podrían procesarse solicitudes duplicadas en paralelo.

Normalmente, las definías así:

class ProcessPayment extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            (new WithoutOverlapping("payment-{$this->userId}"))
                ->releaseAfter(3600)
        ];
    }
}

El problema: strings mágicos, sin validación, propensos a typos.

La Nueva Forma: Enums Tipados

Desde Laravel 12.64, puedes crear un Enum que represente tus overlap keys:

enum QueueOverlapKey: string
{
    case PAYMENT_PROCESSING = 'payment_processing';
    case USER_SYNC = 'user_sync';
    case EMAIL_BATCH = 'email_batch';
    case REPORT_GENERATION = 'report_generation';
}

Ahora tu job usa el Enum directamente:

class ProcessPayment extends Job implements ShouldQueue
{
    public function __construct(
        public int $userId,
        public float $amount
    ) {}

    public function middleware(): array
    {
        return [
            (new WithoutOverlapping(QueueOverlapKey::PAYMENT_PROCESSING))
                ->releaseAfter(3600)
        ];
    }

    public function handle(): void
    {
        // Procesa el pago
        PaymentService::process($this->userId, $this->amount);
    }
}

Ventajas del Tipado Fuerte

1. Autocomplete en tu IDE

Tu IDE ahora sugiere automáticamente todas las overlap keys disponibles:

// ✅ Autocomplete perfecto
(new WithoutOverlapping(QueueOverlapKey::PA...))

// ❌ Con strings, sin ayuda
(new WithoutOverlapping("payment_..."))

2. Refactoring Seguro

Si necesitas renombrar una overlap key:

enum QueueOverlapKey: string
{
    case PAYMENT_PROCESSING = 'payment_processing_v2'; // Cambio detectado
}

Tu IDE encontrará todos los lugares donde se usa y podrás refactorizar con confianza.

3. Prevención de Typos

Sin Enums:

(new WithoutOverlapping("paymnet_processing")) // ¡Typo, nuevo job creado!

Con Enums:

(new WithoutOverlapping(QueueOverlapKey::PAYMNET_PROCESSING)) // Error de compilación

Caso Práctico: Sistema de Sincronización

Imagina un sistema donde sincronizas datos de múltiples usuarios. Sin Enums, esto es frágil:

// ❌ Versión frágil
class SyncUserData extends Job implements ShouldQueue
{
    public function __construct(public int $userId) {}

    public function middleware(): array
    {
        return [
            new WithoutOverlapping("sync-user-{$this->userId}")
        ];
    }
}

Problemas:

  • El prefijo “sync-user-” está duplicado en múltiples jobs
  • Si cambias el patrón, debe hacerse en varios lugares
  • No hay validación de qué prefijos son válidos

Con Enums, lo resuelves así:

enum UserOperationKey: string
{
    case SYNC_DATA = 'user_sync_data';
    case DELETE_ACCOUNT = 'user_delete_account';
    case EXPORT_DATA = 'user_export_data';
}

class SyncUserData extends Job implements ShouldQueue
{
    public function __construct(public int $userId) {}

    public function middleware(): array
    {
        return [
            (new WithoutOverlapping(
                UserOperationKey::SYNC_DATA->value . ":{$this->userId}"
            ))
                ->releaseAfter(1800)
                ->dontRelease()
        ];
    }

    public function handle(): void
    {
        Log::info("Sincronizando usuario {$this->userId}");
        // Lógica de sincronización
    }
}

Combinando Enums con Parámetros Dinámicos

A menudo necesitas incluir identificadores dinámicos (como user ID) en la overlap key. Los Enums te dan lo mejor de ambos mundos:

enum ResourceLockKey: string
{
    case IMPORT_USERS = 'import_users';
    case PROCESS_ORDER = 'process_order';
    case GENERATE_INVOICE = 'generate_invoice';
}

class ProcessOrder extends Job implements ShouldQueue
{
    public function __construct(
        public int $orderId,
        public string $source
    ) {}

    public function middleware(): array
    {
        // Combina el Enum con datos dinámicos
        $lockKey = sprintf(
            "%s:%s:%s",
            ResourceLockKey::PROCESS_ORDER->value,
            $this->source,
            $this->orderId
        );

        return [
            (new WithoutOverlapping($lockKey))
                ->releaseAfter(600)
        ];
    }

    public function handle(): void
    {
        OrderService::process($this->orderId);
    }
}

Testing con Enums

Tus tests ahora son más claros y seguros:

use Laravel\Framework\Testing\Concerns\InteractsWithQueue;

class ProcessPaymentTest extends TestCase
{
    use InteractsWithQueue;

    public function test_payment_job_has_overlap_prevention(): void
    {
        Queue::fake();

        dispatch(new ProcessPayment(
            userId: 123,
            amount: 99.99
        ));

        // Laravel detecta automáticamente el Enum
        Queue::assertPushed(ProcessPayment::class, fn ($job) =>
            $job->middleware()[0]->key ===
            QueueOverlapKey::PAYMENT_PROCESSING->value
        );
    }

    public function test_concurrent_payments_prevented(): void
    {
        Queue::fake();

        // Despacha dos jobs con el mismo Enum
        dispatch(new ProcessPayment(123, 50.00));
        dispatch(new ProcessPayment(123, 30.00));

        // Solo uno debería procesarse
        Queue::assertPushed(ProcessPayment::class, 2);
    }
}

Buenas Prácticas

1. Organiza Enums por Contexto

// app/Enums/QueueKeys/PaymentQueueKey.php
enum PaymentQueueKey: string
{
    case PROCESS_PAYMENT = 'payment_process';
    case REFUND_PAYMENT = 'payment_refund';
}

// app/Enums/QueueKeys/UserQueueKey.php
enum UserQueueKey: string
{
    case SYNC_DATA = 'user_sync';
    case DELETE_ACCOUNT = 'user_delete';
}

2. Documenta el Propósito

enum QueueOverlapKey: string
{
    /**
     * Previene procesamiento paralelo de pagos.
     * Libera después de 1 hora si falla.
     */
    case PAYMENT_PROCESSING = 'payment_processing';

    /**
     * Solo un export por usuario a la vez.
     * Libera después de 2 horas.
     */
    case USER_DATA_EXPORT = 'user_export';
}

3. Reutiliza en Múltiples Jobs

class ProcessPayment extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            new WithoutOverlapping(QueueOverlapKey::PAYMENT_PROCESSING)
        ];
    }
}

class RetryFailedPayment extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        // Mismo Enum = mismo lock
        return [
            new WithoutOverlapping(QueueOverlapKey::PAYMENT_PROCESSING)
        ];
    }
}

Esto garantiza que un pago en proceso bloquea reintentos automáticamente.

Migración Desde Strings

Si tienes código existente con strings mágicos, la migración es simple:

// Antes
new WithoutOverlapping("user_sync_{$userId}")

// Después
new WithoutOverlapping(UserQueueKey::SYNC_DATA->value . ":{$userId}")

Hazlo gradualmente, Enum por Enum.

Casos de Uso Reales

Email Marketing

enum EmailQueueKey: string
{
    case CAMPAIGN_SEND = 'email_campaign';
    case TRANSACTIONAL_SEND = 'email_transactional';
    case BOUNCE_PROCESSING = 'email_bounce';
}

class SendCampaign extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            new WithoutOverlapping(EmailQueueKey::CAMPAIGN_SEND)
        ];
    }
}

Webhooks Externos

enum WebhookQueueKey: string
{
    case STRIPE_SYNC = 'webhook_stripe';
    case GITHUB_SYNC = 'webhook_github';
    case SLACK_NOTIFICATIONS = 'webhook_slack';
}

class ProcessStripeWebhook extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            new WithoutOverlapping(WebhookQueueKey::STRIPE_SYNC)
        ];
    }
}

Reportes Pesados

enum ReportQueueKey: string
{
    case DAILY_SUMMARY = 'report_daily';
    case MONTHLY_ANALYTICS = 'report_monthly';
    case CUSTOM_EXPORT = 'report_custom';
}

class GenerateReport extends Job implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            (new WithoutOverlapping(ReportQueueKey::DAILY_SUMMARY))
                ->releaseAfter(3600)
        ];
    }
}

Depuración y Monitoreo

Con Enums, es más fácil monitorear qué jobs están bloqueados:

// En tu comando de monitoreo
class MonitorQueueLocks extends Command
{
    public function handle()
    {
        foreach (QueueOverlapKey::cases() as $key) {
            $locked = Cache::has("overlap_key_{$key->value}");
            $status = $locked ? '🔒 BLOQUEADO' : '✅ LIBRE';
            $this->line("{$key->name}: {$status}");
        }
    }
}

Output:

PAYMENT_PROCESSING: 🔒 BLOQUEADO
USER_SYNC: ✅ LIBRE
EMAIL_BATCH: 🔒 BLOQUEADO

Conclusión

Los Enums en colas Laravel transforman la forma en que manejas overlap keys. Pasas de strings frágiles y propensos a errores a tipado fuerte con autocomplete y refactoring seguro.

La implementación es trivial, la migración es gradual, y los beneficios son inmediatos:

  • Código más legible y autoexplicativo
  • Prevención de typos en tiempo de ejecución
  • Refactoring seguro con IDE support
  • Testing más robusto

Si aún usas strings mágicos en tus colas, esta es la razón perfecta para modernizar tu código. Laravel 12.64+ lo hace trivial.

Puntos Clave

  • Enums tipados previenen errores y ofrecen autocomplete automático
  • Usa un Enum por contexto (pagos, usuarios, reportes, etc.)
  • Combina Enums con parámetros dinámicos usando sprintf/interpolación
  • Migra gradualmente desde strings existentes
  • Los Enums facilitan testing y debugging de colas
  • Reutiliza el mismo Enum en múltiples jobs para lockear operaciones relacionadas
  • Documenta el propósito y tiempo de liberación de cada overlap key