dantepiazza / laravel-importer
Importador polimórfico de archivos Excel/CSV para Laravel
Requires
- php: ^8.2
- laravel/framework: ^10.0|^11.0|^12.0|^13.0
- maatwebsite/excel: ^3.1
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0|^11.0
README
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.