laravel-importer maintained by dantepiazza
Polymorphic Importer for Laravel
Un importador de archivos Excel y CSV para Laravel, diseñado para ser 100% agnóstico y polimórfico. Permite procesar grandes volúmenes de datos en segundo plano (Queues) vinculando el proceso a cualquier modelo de tu aplicación.
Características Principales
- Agnóstico y Polimórfico: Un solo motor para importar cualquier modelo (
Affiliate,Product,User, etc.). - Procesamiento por Chunks: Maneja archivos de cientos de miles de filas sin agotar la memoria RAM.
- Data Cleansing (Filters): Soporta limpieza de datos mediante métodos en el modelo o clases de filtrado dedicadas.
- Silent Mode (Default): Por seguridad y performance, las importaciones no disparan eventos de Eloquent ni Observers por defecto.
- Sistema de Cancelación: Permite abortar importaciones en tiempo real mediante un sistema de Cache/Redis.
- Reintentos: una importación fallida se puede reintentar (
Importer::retry()) hastamax_attemptsveces sin tener que resubir el archivo. - Campos fijos por importación (
extraFields): mergea valores de contexto (ej: la FK del padre — una página, un tenant) en cada fila importada, sin que tengan que venir en el archivo. - Eventos de Laravel:
ImportStarted,ImportCompleted,ImportFailed,ImportRowFailed— para que la app consumidora reaccione (notificaciones, websockets, etc) sin acoplarse a los hooks del modelo. - Seguimiento Real-time: Persistencia del progreso (%), filas creadas, actualizadas, fallidas y logs de errores.
- Logging Integrado: Todos los eventos del ciclo de vida se registran en el canal de log configurado.
- Tabla namespaced: la tabla de tracking se llama
importer_importspor defecto (configurable víaIMPORTER_TABLE) para no chocar con una tablaimportsde dominio que la app consumidora ya tenga.
Requisitos
- PHP >= 8.2
- Laravel 10.x o 11.x
- maatwebsite/excel ^3.1
- Un driver de Queue configurado (database, Redis, etc.)
- Un driver de Cache configurado (para el sistema de cancelación)
Instalación
- Instala el paquete vía composer:
composer require dantepiazza/laravel-importer
- Publica y ejecuta las migraciones:
php artisan vendor:publish --tag="importer-migrations"
php artisan migrate
- (Opcional) Publica el archivo de configuración:
php artisan vendor:publish --tag="importer-config"
Variables de Entorno
Agrega estas variables a tu .env según necesites:
# Disco de almacenamiento (debe estar en config/filesystems.php)
IMPORTER_DISK=local
# Cola donde se despachan los jobs
IMPORTER_QUEUE=default
# Filas que Maatwebsite lee por iteración (mantener <= 500 para archivos grandes)
IMPORTER_CHUNK_SIZE=200
# Cada cuántas filas se actualiza el progreso en DB
IMPORTER_PROGRESS_INTERVAL=50
# Cada cuántas filas se consulta el Cache para detectar cancelación
IMPORTER_CANCEL_INTERVAL=50
# Canal de log del paquete (null para deshabilitar)
IMPORTER_LOG_CHANNEL=stack
# Máximo de errores guardados en la columna errors
IMPORTER_MAX_ERRORS=100
# Nombre de la tabla de tracking (namespaced por defecto para no chocar
# con una tabla `imports` de dominio que ya tengas)
IMPORTER_TABLE=importer_imports
# Máximo de reintentos permitidos por importador::retry()
IMPORTER_MAX_ATTEMPTS=3
# Timeout del Job de procesamiento, en segundos
IMPORTER_JOB_TIMEOUT=3600
Configuración del Modelo
Cualquier modelo que desees importar debe implementar la interfaz Importable. Usa el trait CanBeImported para obtener implementaciones vacías de los hooks.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use DantePiazza\LaravelImporter\Contracts\Importable;
use DantePiazza\LaravelImporter\Traits\CanBeImported;
class Product extends Model implements Importable
{
use CanBeImported;
public static function getImportConfig(): array
{
return [
'unique_key' => 'sku',
'default_mapping' => [
// Mapeo simple: columna Excel => campo del modelo
'Code' => 'sku',
'Name' => 'name',
// Mapeo con filtro (método del modelo)
'Description' => ['field' => 'description', 'filter' => 'sanitizeHTML'],
// Mapeo con clase de filtro dedicada (debe ser invocable con __invoke)
'Date publish' => ['field' => 'published_at', 'filter' => \App\Filters\DateFilter::class],
],
];
}
// Ejemplo de filtro interno
public function sanitizeHTML(mixed $value): mixed
{
return strip_tags((string) $value);
}
}
Nota sobre el mapping: Las claves del mapping no distinguen mayúsculas/minúsculas ni espacios al inicio/final.
'Date publish','date publish'y' DATE PUBLISH 'son equivalentes.
Uso Básico
Iniciar una Importación
use DantePiazza\LaravelImporter\Facades\Importer;
use App\Models\Product;
// Guardar el archivo primero
$path = $request->file('archivo')->store('imports');
$import = Importer::execute(
modelClass: Product::class,
filePath: $path,
uniqueKey: '', // Opcional: sobreescribe el unique_key del modelo
mapping: [], // Opcional: sobreescribe el default_mapping del modelo
userId: auth()->id(),
silentMode: true, // Por defecto true
extraFields: [], // Opcional: valores fijos que se mergean en cada fila
importableId: null, // Opcional: ID de una entidad relacionada (relación `importable`)
);
// $import->id lo usarás para consultar el progreso o cancelar
Campos fijos por importación (extraFields)
Útil cuando todas las filas importadas pertenecen a una misma entidad padre que no viene (ni debería venir) como columna en el archivo — por ejemplo, importar productos de un catálogo que pertenecen todos a una misma página/tienda:
$import = Importer::execute(
modelClass: Product::class,
filePath: $path,
extraFields: ['page_id' => $page->id], // se mergea en cada fila, gana sobre el mapping
importableId: $page->id, // opcional: referencia en la relación `importable`
);
Cancelar una Importación
use DantePiazza\LaravelImporter\Facades\Importer;
use DantePiazza\LaravelImporter\Models\Import;
$import = Import::find($id);
Importer::cancel($import); // Lanza LogicException si ya está en estado terminal
Reintentar una Importación Fallida
$import = Import::find($id);
if ($import->is_retryable) {
Importer::retry($import); // relee el archivo completo, resetea contadores/errores
}
Monitoreo y Estados
$import = Import::find($id);
$import->status; // pending | processing | completed | failed | cancelled
$import->progress; // 0.00 a 100.00
$import->total_rows; // Total de filas detectadas
$import->processed_rows; // Filas procesadas hasta ahora
$import->created_count; // Registros nuevos insertados
$import->updated_count; // Registros existentes modificados
$import->failed_count; // Filas que fallaron
$import->errors; // Array de ['row' => N, 'message' => '...']
// Accessors de estado
$import->is_processing; // bool
$import->is_completed; // bool
$import->is_cancelled; // bool
$import->is_terminal; // bool — true si completed, failed o cancelled
$import->is_retryable; // bool — true si status=failed y attempts < max_attempts
$import->failure_rate; // float — % de filas fallidas sobre procesadas
$import->attempts; // int — cuántas veces se procesó (1 la primera vez)
Eventos
El paquete dispara eventos estándar de Laravel en los puntos clave del ciclo de vida — registrá
Listeners en tu EventServiceProvider (o vía Event::listen()) para reaccionar sin acoplarte a
los hooks del modelo:
use DantePiazza\LaravelImporter\Events\ImportStarted;
use DantePiazza\LaravelImporter\Events\ImportCompleted;
use DantePiazza\LaravelImporter\Events\ImportFailed;
use DantePiazza\LaravelImporter\Events\ImportRowFailed;
Event::listen(ImportCompleted::class, function (ImportCompleted $event) {
// $event->import->created_count, ->updated_count, ->failed_count...
Notification::send($event->import->user, new ImportFinishedNotification($event->import));
});
Event::listen(ImportRowFailed::class, function (ImportRowFailed $event) {
// $event->rowIndex, $event->message — útil para un feed de progreso en vivo (websockets)
});
Hooks Disponibles
En tu modelo podés definir lógica adicional:
// Se ejecuta UNA vez antes de procesar la primera fila
public function beforeImport(): void
{
// Backup, reset de tabla, inicialización...
}
// Se ejecuta UNA vez al completar exitosamente (NO se llama si fue cancelada o falló)
public function afterImport(): void
{
// Notificaciones, recálculo de agregados, limpieza de caché...
}
Clase de Filtro Personalizada
namespace App\Filters;
class DateFilter
{
public function __invoke(mixed $value): ?string
{
if (empty($value)) return null;
try {
return \Carbon\Carbon::parse($value)->toDateString();
} catch (\Exception) {
return null;
}
}
}
Créditos
Dante Piazza Quiroga · Clousis
Licencia
La Licencia MIT (MIT). Consulte el archivo de licencia para más información.