Queues e Jobs no Laravel

1. Introdução às Queues no Laravel

Queues (filas) são um dos recursos mais poderosos do Laravel para processamento assíncrono de tarefas. Em vez de executar operações demoradas — como envio de e-mails, geração de relatórios ou processamento de imagens — diretamente na requisição HTTP, você pode adiá-las para serem processadas em segundo plano. Isso libera o servidor web rapidamente, melhorando a experiência do usuário.

Em aplicações PHP tradicionais, tudo é síncrono. O usuário espera até que o servidor termine de processar e devolva a resposta. Com filas, você coloca a tarefa em uma fila e retorna imediatamente ao usuário. Um worker separado processa a tarefa assincronamente.

O Laravel suporta diversos drivers de fila: Redis (rápido, recomendado para produção), Database (simples, ótimo para desenvolvimento), Amazon SQS (serviço gerenciado da AWS), Beanstalkd e Synchronous (para testes). A escolha depende da sua infraestrutura e necessidades de escalabilidade.

2. Configuração e Preparação do Ambiente

A configuração principal fica em config/queue.php. Vamos usar o driver Database para este exemplo prático, pois não requer dependências externas.

// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),

'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90,
        'after_commit' => true,
    ],
    // ... outros drivers
],

Crie a tabela de jobs com o comando Artisan:

php artisan queue:table
php artisan migrate

Isso gera uma migration que cria a tabela jobs, com colunas para payload, tentativas, reserva e timestamps. A variável de ambiente QUEUE_CONNECTION no .env define qual conexão usar.

3. Criando e Despachando Jobs

Jobs são classes que encapsulam a lógica a ser executada na fila. Crie um job com:

php artisan make:job SendWelcomeEmail

A classe gerada contém um método handle() e propriedades públicas para dados injetados:

<?php

namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public User $user
    ) {}

    public function handle(): void
    {
        Mail::to($this->user->email)->send(new WelcomeMail($this->user));
    }
}

Despache o job de várias formas:

// Despacho padrão (vai para a fila)
SendWelcomeEmail::dispatch($user);

// Execução síncrona (útil para testes)
SendWelcomeEmail::dispatchSync($user);

// Executar após enviar a resposta HTTP
SendWelcomeEmail::dispatchAfterResponse($user);

// Com atraso de 10 minutos
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));

// Em uma fila específica com prioridade
SendWelcomeEmail::dispatch($user)->onQueue('high');

4. Processamento e Gerenciamento de Filas

Para processar os jobs, execute o worker:

php artisan queue:work

Opções importantes do worker:

# Processar apenas a fila 'high'
php artisan queue:work --queue=high,default

# Máximo de 3 tentativas por job
php artisan queue:work --tries=3

# Timeout de 60 segundos para cada job
php artisan queue:work --timeout=60

# Dormir 3 segundos entre verificações
php artisan queue:work --sleep=3

Para produção, use o Supervisor para manter o worker rodando continuamente. Exemplo de configuração do Supervisor:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/var/log/supervisor/worker.log

Para ver jobs falhos:

php artisan queue:failed
php artisan queue:retry all
php artisan queue:flush

5. Tratamento de Falhas e Retentativas

Configure tentativas máximas na classe do job:

class SendWelcomeEmail implements ShouldQueue
{
    public $tries = 5; // ou use maxAttempts

    public function failed(Throwable $exception): void
    {
        // Logar o erro ou notificar equipe
        Log::error('Falha ao enviar e-mail', [
            'user' => $this->user->id,
            'error' => $exception->getMessage()
        ]);
    }
}

Para jobs com timeout personalizado:

public $timeout = 120; // segundos

Middlewares de fila permitem lógica antes/depois da execução:

// App/Jobs/Middleware/RateLimited.php
public function handle(Job $job, $next)
{
    $this->limiter->throttle('email-sending', 10, function () use ($job, $next) {
        $next($job);
    });
}

6. Recursos Avançados de Jobs

Job Chaining (encadeamento): execute jobs em sequência, parando se um falhar:

Bus::chain([
    new ProcessPodcast($podcast),
    new OptimizePodcast($podcast),
    new ReleasePodcast($podcast),
])->dispatch();

Jobs únicos (ShouldBeUnique): evita duplicatas na fila:

class SendWelcomeEmail implements ShouldBeUnique
{
    public function uniqueId(): string
    {
        return $this->user->id;
    }

    public $uniqueFor = 3600; // segundos
}

Rate limiting em jobs:

public function middleware(): array
{
    return [
        new RateLimited('email-sending')
    ];
}

Batch Jobs (lotes): processe grupos de jobs com progresso:

$batch = Bus::batch([
    new ProcessUser($user1),
    new ProcessUser($user2),
    new ProcessUser($user3),
])->then(function (Batch $batch) {
    // Todos os jobs completaram com sucesso
})->catch(function (Batch $batch, Throwable $e) {
    // Primeiro job falhou
})->finally(function (Batch $batch) {
    // Lote finalizou (sucesso ou falha)
})->dispatch();

// Rastrear progresso
$batch->progress(); // 0 a 100

7. Eventos e Notificações Relacionados a Filas

O Laravel dispara eventos durante o ciclo de vida dos jobs:

// App/Providers/AppServiceProvider.php
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobFailed;

public function boot(): void
{
    Queue::before(function (JobProcessing $event) {
        Log::info('Processando job: ' . $event->job->getName());
    });

    Queue::after(function (JobProcessed $event) {
        // Job processado com sucesso
    });

    Queue::failing(function (JobFailed $event) {
        Log::error('Job falhou: ' . $event->job->getName(), [
            'exception' => $event->exception
        ]);
    });
}

Notificações em fila são automáticas quando a notificação implementa ShouldQueue:

use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    public function via($notifiable): array
    {
        return ['mail'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->line('Sua fatura foi paga!');
    }
}

// Despachar notificação em background
$user->notify(new InvoicePaid($invoice));

Exemplo prático: e-mail em background com fila:

public function register(Request $request): RedirectResponse
{
    $user = User::create($request->validated());

    // Enfileira o e-mail de boas-vindas
    SendWelcomeEmail::dispatch($user);

    return redirect()->route('dashboard')
        ->with('success', 'Conta criada! Você receberá um e-mail em breve.');
}

8. Boas Práticas e Performance

Quando usar filas:
- Envio de e-mails e notificações
- Processamento de imagens/vídeos
- Geração de relatórios e PDFs
- Integrações com APIs externas
- Qualquer tarefa que demore mais de 500ms

Armadilhas a evitar:
- Evite serializar objetos Eloquent com relacionamentos carregados (use apenas o ID)
- Use mutex para operações concorrentes que alteram o mesmo recurso
- Cuidado com deadlocks em transações de banco de dados dentro de jobs

Monitoramento com Horizon (Redis):

composer require laravel/horizon
php artisan horizon:install
php artisan horizon

Horizon fornece dashboard em tempo real, métricas de throughput e balanceamento automático de workers.

Testando jobs com Pest:

test('send welcome email job', function () {
    Queue::fake();

    $user = User::factory()->create();

    SendWelcomeEmail::dispatch($user);

    Queue::assertPushed(SendWelcomeEmail::class, function ($job) use ($user) {
        return $job->user->id === $user->id;
    });

    // Processar job manualmente
    (new SendWelcomeEmail($user))->handle();

    Mail::assertSent(WelcomeMail::class);
});

Referências