silverstripe-framework/dev/CsvBulkLoader.php
Jeremy Shipman d47b202697 Restored c4eac5310e (merge error)
FIX: Instead of CsvBulkLoader->findExistingRecord out right failing (i.e. no duplicate found) when the duplicate check field is empty, it will now continue on to check other duplicateCheck fields.
Added extra testing data to CSVBulkLoaderTest so that it fails.
2013-05-09 23:06:02 +02:00

205 lines
6.5 KiB
PHP

<?php
/**
* Utility class to facilitate complex CSV-imports by defining column-mappings and custom converters.
* Uses the fgetcsv() function to process CSV input. Accepts a file-handler as input.
*
* @see http://rfc.net/rfc4180.html
* @package framework
* @subpackage bulkloading
* @author Ingo Schommer, Silverstripe Ltd. (<myfirstname>@silverstripe.com)
*
* @todo Support for deleting existing records not matched in the import (through relation checks)
*/
class CsvBulkLoader extends BulkLoader {
/**
* Delimiter character (Default: comma).
*
* @var string
*/
public $delimiter = ',';
/**
* Enclosure character (Default: doublequote)
*
* @var string
*/
public $enclosure = '"';
/**
* Identifies if the has a header row.
* @var boolean
*/
public $hasHeaderRow = true;
/**
* @inheritDoc
*/
public function preview($filepath) {
return $this->processAll($filepath, true);
}
protected function processAll($filepath, $preview = false) {
$results = new BulkLoader_Result();
$csv = new CSVParser($filepath, $this->delimiter, $this->enclosure);
// ColumnMap has two uses, depending on whether hasHeaderRow is set
if($this->columnMap) {
if($this->hasHeaderRow) $csv->mapColumns($this->columnMap);
else $csv->provideHeaderRow($this->columnMap);
}
foreach($csv as $row) {
$this->processRecord($row, $this->columnMap, $results, $preview);
}
return $results;
}
/**
* @todo Better messages for relation checks and duplicate detection
* Note that columnMap isn't used
*/
protected function processRecord($record, $columnMap, &$results, $preview = false) {
$class = $this->objectClass;
// find existing object, or create new one
$existingObj = $this->findExistingObject($record, $columnMap);
$obj = ($existingObj) ? $existingObj : new $class();
// first run: find/create any relations and store them on the object
// we can't combine runs, as other columns might rely on the relation being present
$relations = array();
foreach($record as $fieldName => $val) {
// don't bother querying of value is not set
if($this->isNullValue($val)) continue;
// checking for existing relations
if(isset($this->relationCallbacks[$fieldName])) {
// trigger custom search method for finding a relation based on the given value
// and write it back to the relation (or create a new object)
$relationName = $this->relationCallbacks[$fieldName]['relationname'];
if($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
$relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}($obj, $val, $record);
} elseif($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
}
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName);
$relationObj = new $relationClass();
//write if we aren't previewing
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
}
} elseif(strpos($fieldName, '.') !== false) {
// we have a relation column with dot notation
list($relationName, $columnName) = explode('.', $fieldName);
// always gives us an component (either empty or existing)
$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
foreach($record as $fieldName => $val) {
//break out of the loop if we are previewing
if ($preview) break;
if($this->isNullValue($val, $fieldName)) continue;
if(strpos($fieldName, '->') !== FALSE) {
$funcName = substr($fieldName, 2);
$this->$funcName($obj, $val, $record);
} else if($obj->hasMethod("import{$fieldName}")) {
$obj->{"import{$fieldName}"}($val, $record);
} else {
$obj->update(array($fieldName => $val));
}
}
// write record
$id = ($preview) ? 0 : $obj->write();
// @todo better message support
$message = '';
// save to results
if($existingObj) {
$results->addUpdated($obj, $message);
} else {
$results->addCreated($obj, $message);
}
$objID = $obj->ID;
$obj->destroy();
// memory usage
unset($existingObj);
unset($obj);
return $objID;
}
/**
* Find an existing objects based on one or more uniqueness
* columns specified via {@link self::$duplicateChecks}
*
* @param array $record CSV data column
* @return unknown
*/
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;
} elseif(is_array($duplicateCheck) && isset($duplicateCheck['callback'])) {
if($this->hasMethod($duplicateCheck['callback'])) {
$existingRecord = $this->{$duplicateCheck['callback']}($record[$fieldName], $record);
} elseif($SNG_objectClass->hasMethod($duplicateCheck['callback'])) {
$existingRecord = $SNG_objectClass->{$duplicateCheck['callback']}($record[$fieldName], $record);
} else {
user_error("CsvBulkLoader::processRecord():"
. " {$duplicateCheck['callback']} not found on importer or object class.", E_USER_ERROR);
}
if($existingRecord) {
return $existingRecord;
}
} else {
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}.
*
* @return boolean
*/
public function hasHeaderRow() {
return ($this->hasHeaderRow || isset($this->columnMap));
}
}