diff --git a/dev/CSVParser.php b/dev/CSVParser.php index cee48e5d0..5cf20d645 100644 --- a/dev/CSVParser.php +++ b/dev/CSVParser.php @@ -1,9 +1,13 @@ * $parser = new CSVParser('myfile.csv'); * $parser->mapColumns( @@ -23,50 +27,80 @@ * @subpackage bulkloading */ class CSVParser extends Object implements Iterator { + + /** + * @var string $filename + */ protected $filename; + + /** + * @var resource $fileHandle + */ protected $fileHandle; /** - * Map of source columns to output columns - * Once they get into this variable, all of the source columns are in lowercase + * Map of source columns to output columns. + * + * Once they get into this variable, all of the source columns are in + * lowercase. + * + * @var array */ protected $columnMap = array(); /** - * The header row used to map data in the CSV file - * To begin with, this is null. Once it has been set, data will get returned from the CSV file + * The header row used to map data in the CSV file. + * + * To begin with, this is null. Once it has been set, data will get + * returned from the CSV file. + * + * @var array */ protected $headerRow = null; /** - * A custom header row provided by the caller + * A custom header row provided by the caller. + * + * @var array */ protected $providedHeaderRow = null; /** - * The data of the current row + * The data of the current row. + * + * @var array */ protected $currentRow = null; /** - * The current row number - * 1 is the first data row in the CSV file; the header row, if it exists, is ignored + * The current row number. + * + * 1 is the first data row in the CSV file; the header row, if it exists, + * is ignored. + * + * @var int */ protected $rowNum = 0; /** - * The character for separating columns + * The character for separating columns. + * + * @var string */ protected $delimiter = ","; /** - * The character for quoting colums + * The character for quoting columns. + * + * @var string */ protected $enclosure = '"'; /** * Open a CSV file for parsing. - * You can use the object returned in a foreach loop to extract the data + * + * You can use the object returned in a foreach loop to extract the data. + * * @param $filename The name of the file. If relative, it will be relative to the site's base dir * @param $delimiter The character for seperating columns * @param $enclosure The character for quoting or enclosing columns @@ -76,24 +110,28 @@ class CSVParser extends Object implements Iterator { $this->filename = $filename; $this->delimiter = $delimiter; $this->enclosure = $enclosure; + parent::__construct(); } /** * Re-map columns in the CSV file. - * This can be useful for identifying synonyms in the file - * For example: + * + * This can be useful for identifying synonyms in the file. For example: + * * * $csv->mapColumns(array( * 'firstname' => 'FirstName', * 'last name' => 'Surname', * )); * + * + * @param array */ public function mapColumns($columnMap) { if($columnMap) { $lowerColumnMap = array(); - + foreach($columnMap as $k => $v) { $lowerColumnMap[strtolower($k)] = $v; } @@ -101,22 +139,26 @@ class CSVParser extends Object implements Iterator { $this->columnMap = array_merge($this->columnMap, $lowerColumnMap); } } - + /** - * If your CSV file doesn't have a header row, then you can call this function to provide one. - * If you call this function, then the first row of the CSV will be included in the data returned. + * If your CSV file doesn't have a header row, then you can call this + * function to provide one. + * + * If you call this function, then the first row of the CSV will be + * included in the data returned. + * + * @param array */ public function provideHeaderRow($headerRow) { $this->providedHeaderRow = $headerRow; } /** - * Open the CSV file for reading + * Open the CSV file for reading. */ protected function openFile() { ini_set('auto_detect_line_endings',1); $this->fileHandle = fopen($this->filename,'r'); - if($this->providedHeaderRow) { $this->headerRow = $this->remapHeader($this->providedHeaderRow); @@ -124,12 +166,14 @@ class CSVParser extends Object implements Iterator { } /** - * Close the CSV file and re-set all of the internal variables + * Close the CSV file and re-set all of the internal variables. */ protected function closeFile() { - if($this->fileHandle) fclose($this->fileHandle); - $this->fileHandle = null; + if($this->fileHandle) { + fclose($this->fileHandle); + } + $this->fileHandle = null; $this->rowNum = 0; $this->currentRow = null; $this->headerRow = null; @@ -137,20 +181,34 @@ class CSVParser extends Object implements Iterator { /** - * Get a header row from the CSV file + * Get a header row from the CSV file. */ protected function fetchCSVHeader() { - $srcRow = fgetcsv($this->fileHandle, 0, $this->delimiter, $this->enclosure); + $srcRow = fgetcsv( + $this->fileHandle, + 0, + $this->delimiter, + $this->enclosure + ); + $this->headerRow = $this->remapHeader($srcRow); } /** - * Map the contents of a header array using $this->mappedColumns + * Map the contents of a header array using $this->mappedColumns. + * + * @param array + * + * @return array */ protected function remapHeader($header) { $mappedHeader = array(); + foreach($header as $item) { - if(isset($this->columnMap[strtolower($item)])) $item = $this->columnMap[strtolower($item)]; + if(isset($this->columnMap[strtolower($item)])) { + $item = $this->columnMap[strtolower($item)]; + } + $mappedHeader[] = $item; } return $mappedHeader; @@ -158,23 +216,42 @@ class CSVParser extends Object implements Iterator { /** * Get a row from the CSV file and update $this->currentRow; + * + * @return array */ protected function fetchCSVRow() { - if(!$this->fileHandle) $this->openFile(); - if(!$this->headerRow) $this->fetchCSVHeader(); + if(!$this->fileHandle) { + $this->openFile(); + } + + if(!$this->headerRow) { + $this->fetchCSVHeader(); + } $this->rowNum++; - $srcRow = fgetcsv($this->fileHandle, 0, $this->delimiter, $this->enclosure); + $srcRow = fgetcsv( + $this->fileHandle, + 0, + $this->delimiter, + $this->enclosure + ); + if($srcRow) { $row = array(); + foreach($srcRow as $i => $value) { // Allow escaping of quotes and commas in the data $value = str_replace( - array('\\'.$this->enclosure,'\\'.$this->delimiter), - array($this->enclosure,$this->delimiter),$value); + array('\\'.$this->enclosure,'\\'.$this->delimiter), + array($this->enclosure, $this->delimiter), + $value + ); + if(array_key_exists($i, $this->headerRow)) { - if($this->headerRow[$i]) $row[$this->headerRow[$i]] = $value; + if($this->headerRow[$i]) { + $row[$this->headerRow[$i]] = $value; + } } else { user_error("No heading for column $i on row $this->rowNum", E_USER_WARNING); } @@ -184,6 +261,7 @@ class CSVParser extends Object implements Iterator { } else { $this->closeFile(); } + return $this->currentRow; } @@ -223,6 +301,7 @@ class CSVParser extends Object implements Iterator { */ public function next() { $this->fetchCSVRow(); + return $this->currentRow; } @@ -232,5 +311,4 @@ class CSVParser extends Object implements Iterator { public function valid() { return $this->currentRow ? true : false; } -} - +} \ No newline at end of file diff --git a/dev/CsvBulkLoader.php b/dev/CsvBulkLoader.php index dae0f906c..d5fb50545 100644 --- a/dev/CsvBulkLoader.php +++ b/dev/CsvBulkLoader.php @@ -1,14 +1,18 @@ @silverstripe.com) * - * @todo Support for deleting existing records not matched in the import (through relation checks) + * @todo Support for deleting existing records not matched in the import + * (through relation checks) */ class CsvBulkLoader extends BulkLoader { @@ -48,15 +52,15 @@ class CsvBulkLoader extends BulkLoader { $results = new BulkLoader_Result(); $csv = new CSVParser( - $filepath, - $this->delimiter, + $filepath, + $this->delimiter, $this->enclosure ); // ColumnMap has two uses, depending on whether hasHeaderRow is set if($this->columnMap) { // if the map goes to a callback, use the same key value as the map - // value, rather than function name as multiple keys may use the + // value, rather than function name as multiple keys may use the // same callback foreach($this->columnMap as $k => $v) { if(strpos($v, "->") === 0) { @@ -82,7 +86,14 @@ class CsvBulkLoader extends BulkLoader { /** * @todo Better messages for relation checks and duplicate detection - * Note that columnMap isn't used + * Note that columnMap isn't used. + * + * @param array $record + * @param array $columnMap + * @param BulkLoader_Result $results + * @param boolean $preview + * + * @return int */ protected function processRecord($record, $columnMap, &$results, $preview = false) { $class = $this->objectClass; @@ -128,13 +139,13 @@ class CsvBulkLoader extends BulkLoader { $relationObj = $obj->getComponent($relationName); if (!$preview) $relationObj->write(); $obj->{"{$relationName}ID"} = $relationObj->ID; + //write if we are not previewing if (!$preview) { $obj->write(); $obj->flushCache(); // avoid relation caching confusion } } - } // second run: save data @@ -184,24 +195,31 @@ class CsvBulkLoader extends BulkLoader { } /** - * Find an existing objects based on one or more uniqueness - * columns specified via {@link self::$duplicateChecks} + * Find an existing objects based on one or more uniqueness columns + * specified via {@link self::$duplicateChecks}. * * @param array $record CSV data column - * @return unknown + * + * @return mixed */ public function findExistingObject($record) { $SNG_objectClass = singleton($this->objectClass); // checking for existing records (only if not already found) + foreach($this->duplicateChecks as $fieldName => $duplicateCheck) { if(is_string($duplicateCheck)) { $SQL_fieldName = Convert::raw2sql($duplicateCheck); + if(!isset($record[$SQL_fieldName]) || empty($record[$SQL_fieldName])) { //skip current duplicate check if field value is empty continue; } + $SQL_fieldValue = Convert::raw2sql($record[$SQL_fieldName]); $existingRecord = DataObject::get_one($this->objectClass, "\"$SQL_fieldName\" = '{$SQL_fieldValue}'"); - if($existingRecord) return $existingRecord; + + if($existingRecord) { + return $existingRecord; + } } elseif(is_array($duplicateCheck) && isset($duplicateCheck['callback'])) { if($this->hasMethod($duplicateCheck['callback'])) { $existingRecord = $this->{$duplicateCheck['callback']}($record[$fieldName], $record); @@ -211,6 +229,7 @@ class CsvBulkLoader extends BulkLoader { user_error("CsvBulkLoader::processRecord():" . " {$duplicateCheck['callback']} not found on importer or object class.", E_USER_ERROR); } + if($existingRecord) { return $existingRecord; } @@ -218,17 +237,17 @@ class CsvBulkLoader extends BulkLoader { user_error('CsvBulkLoader::processRecord(): Wrong format for $duplicateChecks', E_USER_ERROR); } } + return false; } /** - * Determine wether any loaded files should be parsed - * with a header-row (otherwise we rely on {@link self::$columnMap}. + * Determine whether any loaded files should be parsed with a + * header-row (otherwise we rely on {@link self::$columnMap}. * * @return boolean */ public function hasHeaderRow() { return ($this->hasHeaderRow || isset($this->columnMap)); } - }