Filament: importar CSV com jobs na fila e mostrar progresso em Livewire
Por John Pires · 19 de março de 2026

Filament: importar CSV com Jobs (queue) e mostrar progresso em Livewire
1) Por que aprender isso em 2026
Importar dados por CSV ainda é um dos padrões mais usados em sistemas administrativos. Em 2026 isso continua relevante: planilhas são ubiquitárias, equipes enviam dados grandes e a UX exige feedback de progresso. Usar filas (jobs) evita timeout e mantém o servidor responsivo. Filament é a interface admin popular para Laravel e Livewire muda a experiência do frontend sem SPA complexa.
Este artigo foca em "mostrar, não contar": vamos montar do zero um fluxo funcional onde um administrador envia um CSV pela interface do Filament, um Job processa o arquivo em background e um componente Livewire mostra um progresso contínuo.
Analogia simples: imagine a importação como uma esteira em uma fábrica. Você coloca uma caixa (CSV) na esteira (upload). Um trabalhador (a fila) pega a caixa e processa item por item. A Livewire é uma janela que permite ver quantas caixas já passaram pela esteira.
Ao final você terá conhecimentos práticos sobre:
- Configurar driver de fila (sync, database, redis)
- Criar um Job que processa CSV sem estourar memória
- Atualizar progresso usando cache/redis acessível por Livewire
- Integrar upload no Filament Forms
- Testes básicos para o Job e dicas de deploy com supervisor/horizon
Resumo do que verá
Veja o passo a passo prático: instalação mínima, configuração de queue, código do Job, Livewire componente com progress bar, e integração no Filament Resource. Código real de exemplo em cada etapa.
Requisitos prévios
Para acompanhar você precisa de PHP 8.1+, Composer, Laravel 10+, Filament e Livewire instalados num projeto Laravel. Usaremos Redis para progresso (pode usar database cache também).
2) Configuracao do ambiente
Comece criando um projeto Laravel se ainda não tem um. Aqui vai o comando mínimo e as dependências que usaremos.
composer create-project laravel/laravel filament-import --prefer-dist
cd filament-import
composer require filament/filament livewire/livewire
composer require predis/predis --with-all-dependencies
O bloco acima cria o projeto e instala Filament e Livewire. Predis é para usar Redis localmente. Depois execute a instalação do Filament conforme a documentação oficial (gera admin user etc.).
Configurar .env:
APP_NAME=FilamentImport
QUEUE_CONNECTION=database # ou redis
CACHE_DRIVER=redis
SESSION_DRIVER=file
REDIS_HOST=127.0.0.1
Se escolher QUEUE_CONNECTION=database, crie a migration de filas e rode:
php artisan queue:table
php artisan migrate
Se escolher redis (recomendado), garanta que o Redis está rodando e defina QUEUE_CONNECTION=redis. Para desenvolvimento local é aceitável usar sync, mas nesse caso não haverá processamento assíncrono — o propósito do tutorial é filas.
Rodando worker
Em dev, rode o worker com:
php artisan queue:work --sleep=3 --tries=3
Em produção prefira Supervisor ou Laravel Horizon. Iremos comentar em "erros comuns" sobre como manter o worker ativo.
Estrutura de arquivos que criaremos
Arquivos principais:
- app/Jobs/ImportCsvJob.php
- app/Http/Livewire/CsvImportProgress.php
- app/Filament/Resources/ImportResource.php (ou uso direto em Resource existente)
- storage/app/imports/ (onde salvamos uploads)
3) Hello World progressivo
Aqui fazemos um primeiro exemplo mínimo: upload de CSV via Filament (FileUpload), despacho de Job que lê as linhas e grava progresso em cache. Livewire irá fazer polling para obter o estado.
Formulário Filament com FileUpload
Supondo que você já tem um Resource no Filament (ex: Users), você pode adicionar uma Action que abre um modal com upload. Alternativamente, crie uma página customizada. Exemplo de uma action no Resource:
// app/Filament/Resources/UserResource.php (trecho dentro de tableActions ou formActions)
use Filamentormsuilderormileupload;
use Filament
esourcesormsorm;
// Exemplo rápido (pseudo):
Action::make('import_csv')
->label('Importar CSV')
->form([
FormsieldsileUpload('csv_file')->directory('imports')->disk('local')->rules('required|mimes:csv,txt')
])
->action(function (array $data) {
$path = $data['csv_file'];
$importId = (string) Str::uuid();
// Disparar Job
ImportCsvJob::dispatch($path, $importId);
return Notification::make()->title('Importação iniciada')->success()->send();
});
O código acima adiciona uma Action que recebe o arquivo e dispara o Job com um importId (UUID). Nós armazenamos o arquivo via disk local em storage/app/imports.
Job que processa CSV e atualiza progresso
Crie o Job com: php artisan make:job ImportCsvJob. Conteúdo:
// app/Jobs/ImportCsvJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Cache;
class ImportCsvJob implements ShouldQueue
{
use Dispatchable, Queueable;
public string $path;
public string $importId;
public function __construct(string $path, string $importId)
{
$this->path = $path;
$this->importId = $importId;
}
public function handle()
{
$fullPath = storage_path('app/' . $this->path);
if (!file_exists($fullPath)) {
Cache::put("import:{$this->importId}", ['error' => 'file_not_found'], 3600);
return;
}
$handle = fopen($fullPath, 'r');
$total = 0;
// Conta linhas sem carregar tudo na memória
while (!feof($handle)) { if (fgets($handle) !== false) $total++; }
rewind($handle);
$processed = 0;
while (($row = fgetcsv($handle, 0, ',')) !== false) {
// Aqui processe a linha (ex: criar model)
// Example: User::create([...]);
$processed++;
// Atualiza progresso no cache (percentual e contagem)
Cache::put("import:{$this->importId}", [
'total' => $total,
'processed' => $processed,
'percent' => intval(($processed / max(1, $total)) * 100)
], 3600);
}
fclose($handle);
Cache::put("import:{$this->importId}", ['total' => $total, 'processed' => $processed, 'percent' => 100, 'finished' => true], 3600);
}
}
O que o bloco faz: abre o CSV no storage, conta linhas (para calcular total), depois itera com fgetcsv processando linha a linha e atualizando o cache com as chaves import:{importId}. Ao terminar salva finished=true.
Depois do Job rodar, o cache conterá estado que um cliente pode ler para atualizar uma barra de progresso.
Componente Livewire que mostra progresso
Vamos criar um componente Livewire simples que recebe o importId e faz polling a cada segundo para atualizar a UI. Em produção pode usar broadcasting, mas polling é mais simples e funcional para iniciante.
// app/Http/Livewire/CsvImportProgress.php
namespace App\Http\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class CsvImportProgress extends Component
{
public $importId;
public $state = [];
protected $listeners = ['startPolling' => 'poll'];
public function mount($importId)
{
$this->importId = $importId;
$this->poll();
}
public function poll()
{
$this->state = Cache::get("import:{$this->importId}", ['percent' => 0, 'processed' => 0]);
if (empty($this->state['finished'])) {
$this->dispatchBrowserEvent('poll'); // opcional
$this->emitSelf('startPolling');
}
}
public function render()
{
return view('livewire.csv-import-progress', ['state' => $this->state]);
}
}
Contexto: o componente usa Cache para ler a chave criada pelo Job. A view blade deve ter um pequeno script para chamar Livewire.polling via setInterval (ou usar Alpine para simplificar).
Exemplo de view resources/views/livewire/csv-import-progress.blade.php:
<div>
<div>Processados: {{ $state['processed'] ?? 0 }} / {{ $state['total'] ?? '-' }}</div>
<div style="width:100%;background:#eee;height:24px;border-radius:4px;margin-top:8px;">
<div style="width:{{ $state['percent'] ?? 0 }}%;background:#38a169;height:100%;border-radius:4px;transition:width .3s;"> </div>
</div>
<script>
setInterval(function(){ Livewire.emit('startPolling'); }, 1000);
</script>
</div>
O que acontece: a cada segundo o frontend emite startPolling, o Livewire atualiza o estado lendo o cache e re-renderiza a barra. É simples e robusto para iniciantes.
4) Mini-projeto pratico
Agora juntamos tudo: uma página no Filament onde o admin envia o CSV, a action retorna um modal com importId e o componente Livewire mostrando progresso. Também adicionaremos testes mínimos e dicas de produção.
Filament Page com upload e retorno do importId
Crie uma Filament Page (ou Resource action) que usa Forms. Ao submeter, retornamos importId e exibimos o componente Livewire dentro do modal.
// app/Filament/Pages/CsvImporter.php (exemplo compacto)
namespace App\Filament\Pages;
use Filament\Pages\Page;
use Livewire\Component as LivewireComponent;
use Illuminate\Support\Str;
use App\Jobs\ImportCsvJob;
class CsvImporter extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-upload';
public function import(array $data)
{
$path = $data['csv_file'];
$importId = (string) Str::uuid();
ImportCsvJob::dispatch($path, $importId);
// Retorna view parcial que inclui Livewire::mount('csv-import-progress', ['importId' => $importId])
// Em Filament você pode disparar um modal customizado que renderiza o componente
}
}
Detalhe importante: o componente Livewire precisa do importId para começar a monitorar. Você pode montar o componente no modal ou em uma seção da página que seja visível após o upload.
Testes básicos
Crie um teste que garante que o Job é despachado quando o upload é feito. Use Bus::fake.
// tests/Feature/CsvImportTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Bus;
use App\Jobs\ImportCsvJob;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class CsvImportTest extends TestCase
{
public function test_dispatches_import_job()
{
Bus::fake();
Storage::fake('local');
$file = UploadedFile::fake()->createWithContent('users.csv', "name,email\nJohn,john@example.com\n");
// Simule request para rota onde Filament action dispara
$response = $this->post('/filament/import-csv', ['csv_file' => $file]);
Bus::assertDispatched(ImportCsvJob::class);
}
}
O que o teste faz: finge o sistema de filas e garante que o Job foi enfileirado quando o endpoint de upload for acionado. Isso evita testar processamento real na fila.
Erros comuns, armadilhas e dicas práticas
Vários pontos dão dor de cabeça para iniciantes. Listei os mais comuns e soluções diretas:
- Arquivo não encontrado no Job: lembre-se que o path recebido pela action do Filament é relativo ao disk. Sempre use storage_path('app/' . $path) se estiver usando disk local.
- Fila em sync: se QUEUE_CONNECTION estiver em sync, o Job roda na mesma requisição — isso anula o objetivo. Configure queue:work ou use redis/database.
- Timeouts do worker: jobs longos podem exceder timeout do PHP-FPM? Não, worker roda em CLI; porém configure retry e timeout adequados no worker e em supervisor.
- Consumo de memória: não carregue o CSV todo na memória. Use fopen/fgetcsv para streaming.
- Progresso inconsistentes: use uma chave única por import (UUID) e expire a chave para limpar: Cache::put(..., ttl).
Se você usa Redis para cache e fila, o desempenho e consistência do progresso melhora. Para ambientes sem Redis, o cache database funciona mas é mais lento.
5) Recursos para continuar estudando
Referências rápidas para aprofundar:
- Documentação do Laravel sobre queues: https://laravel.com/docs/queues
- Filament docs para Actions e Pages: https://filamentphp.com/docs
- Livewire docs para componentes e polling: https://laravel-livewire.com/docs
- Deploy de workers com Supervisor: https://laravel.com/docs/queues#supervisor-configuration
- Laravel Horizon para Redis: https://laravel.com/docs/horizon
Estude também como usar Laravel-Excel (Maatwebsite) para importações mais complexas que querem mapeamento, validação de linha e chunking automático. Para iniciantes, fgetcsv é suficiente e ensina os fundamentos.
Conclusão prática
Passamos por um fluxo completo: configurar filas, criar Job que processa CSV linha a linha, gravar progresso em cache e exibir esse progresso em um componente Livewire integrado ao Filament. O padrão é robusto: evita timeouts, melhora UX e escala melhor que processamento síncrono.
Checklist final para aplicar em um projeto real:
- Escolher driver de fila adequado (redis ou database) e rodar workers com supervisor/Horizon;
- Salvar uploads de forma segura e com paths previsíveis;
- Atualizar progresso por importId, expirar chaves no cache e limpar arquivos após processamento;
- Adicionar validações e testes (Bus::fake para jobs);
- Em produção, configurar logs e monitoramento do worker.
Próximo passo sugerido: implemente validação por linha e registro de erros por linha (arquivo de erros) para permitir reprocessamento seletivo. Isso exige gravar status por linha em um DB temporário ou em arquivos separados.
Se quiser, posso gerar um repositório exemplo com todo o código pronto, incluindo views Filament e arquivos de configuração do Supervisor/Horizon.
Bom trabalho — importe com filas, monitore com Livewire, e mantenha a interface suave para seus usuários administrativos.
Assuntos: Laravel, filament, livewire, queues, csv, PHP
Leia também
Laravel 10: consumir APIs externas com Http Client, retries, fallback e cache
Guia prático para iniciantes em Laravel 10 sobre como consumir APIs externas usando Http Client: timeout, retries, cache e fallback, com exemplos errados e…
Testes de Jobs e Queues no Laravel: Queue::fake(), dispatch e assertions
Guia prático e passo a passo para testar jobs e queues no Laravel usando Queue::fake(), dispatch e assertions. Aprenda a isolar dispatches, validar payloads e…
Upload de arquivos grandes com Livewire e S3: chunked, presigned e progresso
Arquitetura prática para uploads grandes: evite enviar o arquivo inteiro ao servidor. Use Livewire para UX, S3 multipart com presigned URLs e um uploader JS…
Circuit breaker para APIs externas com Laravel Http Client e Redis
Implementação prática de circuit breaker em Laravel usando Redis e Http Client. Mostra o jeito errado (flag em memória, retries inline) e a solução robusta…
ETags e cache condicional em APIs Laravel (PHP 8.3): reduzir banda e latência
Como implementar ETags e cache condicional em APIs Laravel (PHP 8.3) para reduzir banda e latência: patterns práticos, middleware, fingerprint por coleção e…
Autenticação de API com Laravel Sanctum para SPA e mobile — passo a passo
Guia prático para autenticar SPA e apps mobile com Laravel Sanctum: compare o jeito errado (token no localStorage) com o certo (cookies para SPA, tokens com…