File: /var/www/dtw.bradford/app/Imports/AbstractChunksImport backup.php
<?php
namespace App\Imports;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterImport;
abstract class AbstractChunksImport implements ToCollection, WithHeadingRow, WithMultipleSheets, WithChunkReading, WithEvents
{
/**
* 🔧 OPTIMIZACIÓN DE IMPORTACIÓN: AJUSTES SEGÚN RECURSOS DEL SERVIDOR
*
* Esta clase implementa una importación por chunks, con escritura temporal en disco
* para evitar sobrecarga de memoria y CPU. Puedes ajustar los siguientes parámetros
* según las capacidades de tu servidor.
*
* 📌 Parámetros clave:
*
* 1. chunkSize() → 100
* - Nº de filas procesadas por lote (para lectura de Excel).
* - Aumentar (150–300): si el servidor tiene ≥ 4 núcleos y >8GB RAM.
* - Mantener (100): ideal para servidores con 2 núcleos y ~8GB RAM.
* - Reducir (50–80): si hay cuellos de botella de CPU o poca memoria.
*
* 2. $maxChunkSize → 500
* - Registros mantenidos en memoria antes de escribir a archivo temporal.
* - Aumentar (1000+): si tienes mucha RAM libre.
* - Reducir (100–300): si ves uso elevado de memoria.
*
* 3. $insertChunkSize → 100
* - Registros por lote al insertar en la base de datos.
* - Aumentar (200–500): si la BD es rápida y estable.
* - Reducir (50–80): si notas latencia alta en BD o errores por tiempo.
*
* 4. Pausas programadas:
* - Cada 10 chunks: `usleep(100000)` (100ms).
* - Cada 500 filas: `usleep(30000)` (30ms).
* - Estas pausas alivian la carga de CPU. Puedes reducirlas si el servidor está desocupado.
*
* 🧠Recomendaciones:
* - Monitorea con `htop`, `free -h`, y laravel.log durante importación.
* - Ajusta valores de a poco, según el tamaño de tus archivos reales.
* - Considera agregar SWAP si trabajas con archivos muy grandes (>500k filas).
*/
public $errorMessage;
public $successMessage;
public $register_id;
public $error_data = [];
public $records_processed = 0;
public $realHeaders;
protected $chunkOffset = 0;
protected $hasErrors = false;
protected $headersValidated = false;
protected $tempFile;
protected $tempFilePath;
protected $currentChunk = [];
protected $maxChunkSize = 500;
protected $insertChunkSize = 300;
public function __construct()
{
$this->tempFilePath = tempnam(sys_get_temp_dir(), 'laravel_import_');
$this->tempFile = fopen($this->tempFilePath, 'w+');
}
abstract protected function getTableName(): string;
abstract protected function getExpectedHeaders(): array;
abstract protected function getSheetName(); // string|int
abstract protected function normalizeRow(array $row, int $numRow): ?array;
protected int $headingRowIndex = 1; // La fila donde están los encabezados
public function sheets(): array
{
return [$this->getSheetName() => $this];
}
public function headingRow(): int
{
return $this->headingRowIndex;
}
public function setChunkOffset(int $offset) { $this->chunkOffset = $offset; }
public function chunkOffset(): int { return $this->chunkOffset; }
public function collection(Collection $rows)
{
Log::info("[IMPORT] Procesando chunk offset: {$this->chunkOffset} - Filas: " . count($rows));
if ($this->chunkOffset % 10 === 0) {
usleep(100000);
}
if ($rows->isEmpty()) {
$this->errorMessage = ['type' => 'ERROR DE VALIDACIÓN', 'message' => 'El documento está vacÃo'];
$this->hasErrors = true;
throw new Exception("El documento está vacÃo");
}
if (!$this->headersValidated) {
$this->realHeaders = $rows->first()->keys()->toArray();
Log::info("Headers del documento: " . json_encode($this->realHeaders));
foreach ($this->getExpectedHeaders() as $header) {
if (!in_array($header, $this->realHeaders)) {
$this->errorMessage = [
'type' => 'ERROR DE VALIDACIÓN',
'message' => 'Falta el encabezado requerido: ' . $header
];
$this->hasErrors = true;
throw new Exception("Falta el encabezado requerido: " . $header);
return;
}
}
$this->headersValidated = true;
}
$this->records_processed += count($rows);
$num_row = $this->chunkOffset + 2;
foreach ($rows as $row) {
if ($num_row % 500 === 0) {
usleep(30000);
Log::info("Fila $num_row - Memoria: " . round(memory_get_usage() / 1024 / 1024, 2) . ' MB');
}
try {
$normalized = $this->normalizeRow($row->toArray(), $num_row);
if ($normalized) {
$this->currentChunk[] = $normalized;
if (count($this->currentChunk) >= $this->maxChunkSize) {
$this->writeChunkToTempFile();
}
}
} catch (\Exception $e) {
$this->hasErrors = true;
$this->error_data[] = [
'numero_de_linea' => $num_row,
'errores' => $e->getMessage(),
'fecha_de_carga' => date('d-m-Y H:i'),
];
}
unset($row);
$num_row++;
}
if (!empty($this->currentChunk)) {
Log::info("[IMPORT] Escritura final de chunk pendiente antes de finishImport()");
$this->writeChunkToTempFile();
}
}
protected function writeChunkToTempFile()
{
if (!empty($this->currentChunk)) {
try {
fwrite($this->tempFile, json_encode($this->currentChunk, JSON_UNESCAPED_UNICODE) . PHP_EOL);
Log::info("[IMPORT] Se escribieron " . count($this->currentChunk) . " registros al archivo temporal");
} catch (\Throwable $e) {
Log::error("[IMPORT] Error al serializar chunk: " . $e->getMessage());
Log::error("[IMPORT] Dump del chunk: " . json_encode($this->currentChunk));
throw $e;
}
$this->currentChunk = [];
gc_collect_cycles();
} else {
Log::info("[IMPORT] CurrentChunk vacÃo");
}
}
protected function readFromTempFile()
{
rewind($this->tempFile);
while (!feof($this->tempFile)) {
$line = fgets($this->tempFile);
if ($line !== false) {
yield json_decode(trim($line), true);
}
}
}
public function chunkSize(): int
{
return 200;
}
public function registerEvents(): array
{
return [
AfterImport::class => function (AfterImport $event) {
Log::info('[IMPORT] Evento AfterImport ejecutado: todos los chunks han sido procesados');
},
];
}
public function finishImport(): void
{
Log::info("[DEBUG] Entrando en finishImport() en tiempo real: " . now());
try {
if (!$this->hasErrors) {
$start = microtime(true);
$totalInserted = 0;
$chunkCounter = 0;
Log::info("Procediendo a leer archivo temporal por lotes...");
foreach ($this->readFromTempFile() as $fileChunk) {
Log::info("[IMPORT] Leyendo chunk desde archivo temporal con " . count($fileChunk) . " registros");
if (!empty($fileChunk)) {
foreach (array_chunk($fileChunk, $this->insertChunkSize) as $insertChunk) {
Log::info("[IMPORT] Insertando chunk de " . count($insertChunk) . " registros");
DB::table($this->getTableName())->insert($insertChunk);
$totalInserted += count($insertChunk);
$chunkCounter++;
Log::info("[IMPORT] Insert chunk $chunkCounter completado. Memoria actual: " . round(memory_get_usage() / 1024 / 1024, 2) . " MB");
unset($insertChunk);
gc_collect_cycles();
}
}
unset($fileChunk);
gc_collect_cycles();
}
$time = round(microtime(true) - $start, 2);
Log::info("[IMPORT] Inserción final completa. Total: $totalInserted registros en $time segundos");
$this->successMessage = 'Importación realizada correctamente.';
Log::info("[IMPORT] finishImport() completado correctamente");
} elseif (!empty($this->error_data)) {
$this->errorMessage = [
'type' => 'ERROR DE VALIDACIÓN',
'message' => 'Error de validación de datos. Revise informe de carga.'
];
} else {
$this->errorMessage = [
'type' => 'ERROR DE SISTEMA',
'message' => 'Ocurrió un error inesperado durante la importación.'
];
Log::info("[IMPORT] Error desconocido en finishImport()");
}
} catch (\Exception $e) {
$this->errorMessage = [
'type' => 'ERROR DE BASE DE DATOS',
'message' => 'Error al insertar datos: ' . $e->getMessage()
];
$this->hasErrors = true;
Log::error("[IMPORT] Excepción capturada en finishImport: " . $e->getMessage());
} finally {
if (is_resource($this->tempFile)) {
fclose($this->tempFile);
$this->tempFile = null;
}
if (file_exists($this->tempFilePath)) {
unlink($this->tempFilePath);
$this->tempFilePath = null;
}
}
}
}