Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed importer when header is not present and row numbers are used. #546

Open
wants to merge 4 commits into
base: release-0.11
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/Exceptions/CsvColumnMismatchException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Exceptions;

use Exception;

class CsvColumnMismatchException extends Exception {
public $headerLine;
public $dataLine;
public $headerColumns;
public $dataColumns;

public function __construct(string $headerLine, string $dataLine, int $headerColumns, int $dataColumns) {
$this->headerLine = $headerLine;
$this->dataLine = $dataLine;
$this->headerColumns = $headerColumns;
$this->dataColumns = $dataColumns;
}
}
1 change: 0 additions & 1 deletion app/Exceptions/Structs/AttributeImportExceptionStruct.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use JsonSerializable;

class AttributeImportExceptionStruct implements JsonSerializable {

public $type;
public $columnIndex;
public $columnName;
Expand Down
75 changes: 41 additions & 34 deletions app/File/Csv.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace App\File;

class Csv extends Parser {
use App\Exceptions\CsvColumnMismatchException;

class Csv extends Parser {
private string $delimiter;
private bool $hasHeaderRow;
private array $headers = [];
private int $headerCount = 0;
private string $encoding = 'UTF-8';
private int $rows = 0;

Expand All @@ -16,24 +18,23 @@ public function __construct(bool $hasHeaderRow = false, string $delimiter = ",",
$this->encoding = $encoding;
}

public function getHeaders() {
public function getHeaders(): array {
return $this->headers;
}



/**
* Parses a CSV file.
*
*
* @param resource $fileHandle - The file handle to the CSV file
* @param callable $$rowCallback -
*/
public function parse($fileHandle, $rowCallback) {
public function parse($fileHandle, callable $rowCallback): void {
$rowIndex = 0;
$this->rows = 0;
// We don't use the fgetcsvfunction to change the file encoding to UTF-8.
while (($row = fgets($fileHandle)) !== false) {
// We don't use the fgetcsv function to change the file encoding to UTF-8.
while(($row = fgets($fileHandle)) !== false) {
$this->rows++;
if ($this->hasHeaderRow && $rowIndex === 0) {
if($this->hasHeaderRow && $rowIndex === 0) {
$rowIndex++;
continue;
}
Expand All @@ -47,38 +48,40 @@ public function parse($fileHandle, $rowCallback) {

/**
* Gets the headers from the CSV file.
*
*
* If there is no header (hasHeaderRow = false), it will return an array of numbers (1, 2, 3, ...)
* and the array will not be changed.
*
* @param array $lines - The lines of the CSV file
*
* @param resource $lines - The lines of the CSV file
* @return array $headers - The headers of the CSV file
*/
public function parseHeaders($fileHandle) {
public function parseHeaders($fileHandle): array {
$headers = null;

$row = fgets($fileHandle);
if (!$row) {
if(!$row) {
throw new \Exception("File is empty.");
}

if ($this->hasHeaderRow) {
if($this->hasHeaderRow) {
$utf8Line = $this->toUtf8($row);
$headers = $this->fromCsvLine($utf8Line);
$headers = array_map(fn ($header) => trim($header), $headers);
} else {
$headers = $this->generateNumberHeaders($row);
}
$this->headerCount = count($headers);

rewind($fileHandle);
$this->headers = $headers;
return $headers;
}

private function generateNumberHeaders($row) {
private function generateNumberHeaders(string $row): array {
$headers = [];

for ($i = 1; $i <= count($row); $i++) {
// $parts = explode($this->delimiter, $row);
$parts = $this->fromCsvLine($row);
for($i = 1; $i <= count($parts); $i++) {
$headers[] = "#$i";
}

Expand All @@ -87,36 +90,41 @@ private function generateNumberHeaders($row) {

/**
* Gets a single row from a CSV line string.
*
*
* @param string $line - The line from the CSV file
* @return array $row - Associative array using the headers as keys and the values from the line as values
*/
private function parseRow($line) {
if (empty($this->headers)) {
private function parseRow(string $line): array {
if(empty($this->headers)) {
throw new \Exception("Headers are not set. Run parseHeaders first.");
}

$row = [];

$arr = $this->fromCsvLine($line);
for ($i = 0; $i < count($arr); $i++) {
$row[$this->headers[$i]] = trim($arr[$i]);
$cols = $this->fromCsvLine($line);
$colCount = count($cols);
if($this->headerCount != $colCount) {
$headerRow = implode($this->delimiter, $this->headers);
$dataRow = implode($this->delimiter, $cols);
throw new CsvColumnMismatchException($headerRow, $dataRow, $this->headerCount, $colCount);
}
for($i = 0; $i < $colCount; $i++) {
$row[$this->headers[$i]] = trim($cols[$i]);
}
return $row;
}


/**
* Converts the data to UTF-8 if it is not already.
*
*
* @param array|string $data - The data to convert
* @return array|string $data - The converted data
*/
private function toUtf8(array|string $data): array|string {
if ($this->isUtf8()) return $data;
if($this->isUtf8()) return $data;

$tgt = 'UTF-8';
if (is_array($data)) {
if(is_array($data)) {
$data = array_map(fn ($str) => iconv($this->encoding, $tgt, $str), $data);
} else {
$data = iconv($this->encoding, $tgt, $data);
Expand All @@ -129,25 +137,24 @@ private function isUtf8(): bool {
return $this->encoding == 'UTF-8';
}


public function getRows() {
public function getRows(): int {
return $this->rows;
}

public function getDataRows() {
public function getDataRows(): int {
return $this->rows - ($this->hasHeaderRow ? 1 : 0);
}

/**
* Helper function to get the CSV from a line.
* Always using the correct delimiter.
*/
private function fromCsvLine($line) {
private function fromCsvLine(string $line): array {
return str_getcsv($line, separator: $this->delimiter);
}

private function verifyColumnExists($column) {
if (!in_array($column, $this->headers)) {
private function verifyColumnExists(string $column): void {
if(!in_array($column, $this->headers)) {
throw new \Exception("Column '$column' does not exist.");
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/File/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
namespace App\File;

abstract class Parser {
abstract public function parse(string $filePath, callable $rowCallback);
abstract public function parse($filePath, callable $rowCallback);
}
17 changes: 13 additions & 4 deletions app/Http/Controllers/EntityController.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ public function importData(Request $request) {
$metadata = json_decode($request->get('metadata'), true);
$data = json_decode($request->get('data'), true);
$handle = fopen($filepath, 'r');

$hasHeaderRow = $metadata["has_header_row"];

// Data values
$nameColumn = trim($data['name_column']);
Expand All @@ -528,22 +530,24 @@ public function importData(Request $request) {
$parentIdx = null;
$nameIdx = null;


// Getting headers
if(($row = fgetcsv($handle, 0, $metadata['delimiter'])) !== false) {
$row = sp_trim_array($row);
try{
$headerRow = $row;
for($i = 0; $i < count($row); $i++) {
if($row[$i] == $nameColumn) {
// Use the provided column name or the column number
$columnName = $hasHeaderRow ? $row[$i] : "#".($i + 1);

if($columnName == $nameColumn) {
$nameIdx = $i;
} else if(isset($parentColumn) && $row[$i] == $parentColumn) {
} else if(isset($parentColumn) && $columnName == $parentColumn) {
$parentIdx = $i;
$hasParent = true;
}

foreach($attributesMapping as $id => $a) {
if($a == $row[$i]) {
if($a == $columnName) {
$attributeIdToColumnIdxMapping[$id] = $i;
$attributeTypes[$id] = Attribute::findOrFail($id)->datatype;
break;
Expand All @@ -559,6 +563,11 @@ public function importData(Request $request) {
], 400);
}
}

// When we have no header row, we need to rewind the file handle
if(!$hasHeaderRow){
rewind($handle);
}

//Processing rows
while(($row = fgetcsv($handle, 0, $metadata['delimiter'])) !== false) {
Expand Down
28 changes: 19 additions & 9 deletions app/Import/EntityImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Import;

use App\Exceptions\CsvColumnMismatchException;
use App\File\Csv;
use App\Entity;
use App\EntityType;
Expand Down Expand Up @@ -40,7 +41,7 @@ public function __construct($metadata, $data) {
$this->entityTypeId = $data['entity_type_id'];
$this->attributesMap = $data['attributes'];

// The parent column is optional, therefore we only set it
// The parent column is optional, therefore we only set it
// to another value than null, if there is valid data set.
if(array_key_exists('parent_column', $data)) {
$parentColumn = trim($data['parent_column']);
Expand Down Expand Up @@ -103,14 +104,23 @@ public function validateImportData($filepath) {
return $this->resolver;
}

$csvTable->parse($handle, function ($row, $index) use (&$result, &$status) {
$namesValid = $this->validateName($row, $index);
if($namesValid) {
// The location depends on the name column. If the name is not correct, we can't check the location.
$this->validateLocation($row, $index);
}
$this->validateAttributesInRow($row, $index);
});
try {
$csvTable->parse($handle, function ($row, $index) {
$namesValid = $this->validateName($row, $index);
if($namesValid) {
// The location depends on the name column. If the name is not correct, we can't check the location.
$this->validateLocation($row, $index);
}
$this->validateAttributesInRow($row, $index);
});
} catch(CsvColumnMismatchException $csvMismatchException) {
return $this->resolver->conflict(__("entity-importer.csv-column-mismatch", [
"data" => $csvMismatchException->dataLine,
"data_count" => $csvMismatchException->dataColumns,
"header_data" => $csvMismatchException->headerLine,
"header_count" => $csvMismatchException->headerColumns,
]));
}

if($csvTable->getDataRows() == 0) {
$this->resolver->conflict(__("entity-importer.empty"));
Expand Down
2 changes: 0 additions & 2 deletions app/Import/ImportResolution.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

namespace App\Import;


enum ImportResolutionType {
case CREATE;
case UPDATE;
case CONFLICT;
}

class ImportResolution {

private array $status;
private array $errors = [];

Expand Down
1 change: 1 addition & 0 deletions lang/de/entity-importer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
'file-not-found' => 'Datei konnte nicht gelesen werden.',
'missing-data' => 'Benötigte Spalte fehlt: :column',
'invalid-data' => 'Ungültige Daten: [:column] => :value',
'csv-column-mismatch' => 'Die Anzahl der Spalten in der aktuellen Zeile \':data\' (:data_count Spalten) stimmt nicht mit den Spalten der Kopfzeile \':header_data\' (:header_count Spalten) überein.',
'attribute-could-not-be-imported' => 'Attribut konnte nicht importiert werden: :attributeErrors',
'attribute-id-does-not-exist' => 'Die Attribut-ID existiert nicht: :attributes',
'attribute-column-does-not-exist' => 'Die Attribut-Spalten existieren nicht: :columns',
Expand Down
2 changes: 2 additions & 0 deletions lang/en/entity-importer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
'file-not-found' => 'File could not be read.',
'missing-data' => 'Required column is missing: :column',
'invalid-data' => 'Invalid data: [:column] => :value',
'csv-column-mismatch' => 'Column count of current row \':data\' (:data_count columns) does not match column count in header row \':header_data\' (:header_count columns).',
'attribute-could-not-be-imported' => 'Attribute could not be imported: :attribute',
'attribute-could-not-be-imported' => 'Attribute could not be imported: :attributeErrors',
'attribute-id-does-not-exist' => 'The attribute id does not exist: :attributes',
'attribute-column-does-not-exist' => 'The attribute columns do not exist: :columns',
Expand Down
Loading
Loading