File: D:/HostingSpaces/SBogers95/rentman.io/app/Komma/Kms/Transfer/AbstractCsvImportService.php
<?php
namespace App\Komma\Kms\Transfer;
use Illuminate\Support\MessageBag;
abstract class AbstractCsvImportService extends AbstractCsvTransferService
{
/** @var string A name of an attribute key that will receive the import errors if any */
protected $errorsAttributeKey = '';
/**
* Imports the row into the database
*
* @param array $row
* @param int $lineNumber
* @return string A success or fail message
*/
abstract public function import(array $row, int $lineNumber):string;
/**
* Validates a row and returns an array with
* validation errors if any or an empty array if no errors
*
* @param array $row
* @param int $lineNumber
* @return array
*/
public function validate(array $row, int $lineNumber): array
{
$mismatchErrors = $this->checkForColumnCountMismatches($row, $lineNumber);
if (count($mismatchErrors) > 0) {
return $mismatchErrors;
}
return $this->validateRowData($row, $lineNumber);
}
/**
* Loops over all column maps and checks if their column numbers really map to a value.
*
* @param array $row
* @param int $lineNumber
* @return array
*/
private function checkForColumnCountMismatches(array $row, int $lineNumber):array
{
$columnMapsCollection = collect($this->getColumnMaps());
$headerString = implode(', ', $this->getHeaderNames());
$validationErrors = $columnMapsCollection->flatten()->map(function (ColumnMap $columnMap) use ($row, $lineNumber, $headerString) {
if ($columnMap->getColumnNumber() > count($row)) {
$error = __('kms/transfer.validation_error.column_count_mismatch', ['headers' => $headerString]);
return $error;
}
})->filter(function ($errorOrNull) {
return $errorOrNull !== null;
})->toArray();
if (count($validationErrors) == 0) {
return [];
}
return $validationErrors;
}
/**
* Validates the rows data and returns an array with
* validation errors if any or an empty array if no errors
*
* @param array $row
* @param int $lineNumber
* @return array
*/
private function validateRowData(array $row, int $lineNumber):array
{
$columnMapsCollection = collect($this->getColumnMaps());
$validationErrors = $columnMapsCollection->flatten()->map(function (ColumnMap $columnMap) use ($row, $lineNumber) {
$validationRegex = $columnMap->getValidationRegex();
$value = $row[$columnMap->getColumnNumber()];
$value = preg_replace("/\r|\n/", '', $value);
if ($value == '' && $columnMap->isOptional()) {
return null;
}
try {
if ($validationRegex == '') {
return null;
} //If no validation regex is defined we skip the validation and assume the data is right
$match = preg_match($validationRegex, $value);
if ($match == false) {
return __('kms/transfer.validation_error.error_on_line', ['row_number' => $lineNumber]).': '.$columnMap->getValidationErrorText();
}
} catch (\ErrorException $exception) {
return __('kms/transfer.validation_error.error_on_line', ['row_number' => $lineNumber]).': '.$columnMap->getValidationErrorText().'. Regex: "'.$validationRegex.'". Exception: '.$exception->getMessage();
}
})->filter(function ($value) {
return $value !== null;
})->toArray();
return $validationErrors;
}
/**
* Loads a file containing CSV data and passes each row to the import method.
*
* @param string $filePath
* @return MessageBag with keys 'errors' and 'success'
*/
public function importFromFile(string $filePath)
{
return $this->readCsvFileAndStartProcessing($filePath);
}
/**
* Reads a csv file and delegates importing.
* If an error occurs it redirect backs with errors. Use key row_validation_errors for more info
*
* @param string $filePath
* @return MessageBag with two keys 'errors' and 'success'. That represent success and error messages for the imported lines
*/
private function readCsvFileAndStartProcessing(string $filePath):MessageBag
{
\Log::debug('AbstractCsvImportService:114');
$fileHandle = fopen($filePath, 'r');
$lineNumber = 1;
$messageBag = new MessageBag();
// echo __('kms/transfer.processing').'<br/>';
$bigErrorCounter = 0;
$bigSuccessCounter = 0;
$emptyRowCounter = 0;
//Prevent memory exhaustion errors
$maxMemoryForImport = 128000000; //128 Megabytes (128000000 bytes). Php default
\Debugbar::disable();
set_time_limit(0);
ini_set('memory_limit', $maxMemoryForImport);
while (($row = fgetcsv($fileHandle, 100000, ';')) !== false) {
if (memory_get_usage() >= $maxMemoryForImport * .8) {
$messageBag->add('errors', __('kms/transfer.memory_exhaustion_import'));
return $messageBag;
}
//Skip a row that is completely empty
$rowAsCSV = implode(';', $row);
if (str_replace(';', '', $rowAsCSV) == '') {
unset($rowAsCSV); //Force garbage collection
$emptyRowCounter++;
if ($emptyRowCounter >= 10) {
break;
}
continue;
}
//Import a row
if (! self::HASHEADERROW || ($lineNumber > 1 && self::HASHEADERROW)) {
\DB::transaction(function () use ($row, $lineNumber, &$messageBag, &$bigErrorCounter, &$bigSuccessCounter) {
$errorsFromValidation = $this->validate($row, $lineNumber);
if (count($errorsFromValidation) > 0) {
foreach ($errorsFromValidation as $error) {
if ($messageBag->count() < 1000) { //we only log the first x amount or so messages. Over that we risk memory exhaustion via the KmsFlashMessageComposer concatenation
$messageBag->add('errors', $error);
} else {
$bigErrorCounter++;
}
}
} else {
$successFromImport = $this->import($row, $lineNumber);
if ($messageBag->count() < 1000) { //we only log the first x amount or so messages. Over that we risk memory exhaustion via the KmsFlashMessageComposer concatenation
$messageBag->add('successes', $successFromImport);
} else {
$bigSuccessCounter++;
}
}
});
}
unset($row); //Force garbage collection
$lineNumber++;
}
fclose($fileHandle);
if ($bigSuccessCounter > 0) {
$messageBag->add('successes', __('kms/transfer.general_extra_success_amount', ['amount' => $bigSuccessCounter]));
}
if ($bigErrorCounter > 0) {
$messageBag->add('errors', __('kms/transfer.general_extra_error_amount', ['amount' => $bigErrorCounter]));
}
return $messageBag;
}
}