silverstripe-framework/dev/CsvBulkLoader.php
Sam Minnee cbc228f9a2 API CHANGE: Created CSVParser class and updated CSVBulkLoader to use it
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@63563 467b73ca-7a2a-4603-9d3b-597d59a354a9
2008-10-03 00:27:58 +00:00

175 lines
5.3 KiB
PHP

<?php
/**
* Uses the fgetcsv() function to process CSV input.
* The input is expected to be UTF8.
*
* @see http://rfc.net/rfc4180.html
* @package cms
* @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.
* If a {@link self::$columnMap} is passed, we assume
* the file has no headerrow, unless explicitly noted.
*
* @var boolean
*/
public $hasHeaderRow = false;
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, array(), $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'];
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName);
$relationObj = new $relationClass();
$relationObj->write();
}
$obj->setComponent($relationName, $relationObj);
$obj->{"{$relationName}ID"} = $relationObj->ID;
$obj->write();
} elseif(strpos($fieldName, '.') !== false) {
// we have a relation column with dot notation
list($relationName,$columnName) = split('\.', $fieldName);
$relationObj = $obj->getComponent($relationName); // always gives us an component (either empty or existing)
$obj->setComponent($relationName, $relationObj);
$relationObj->write();
$obj->{"{$relationName}ID"} = $relationObj->ID;
$obj->write();
}
$obj->flushCache(); // avoid relation caching confusion
}
$id = ($preview) ? 0 : $obj->write();
// second run: save data
foreach($record as $fieldName => $val) {
if($this->isNullValue($val, $fieldName)) continue;
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);
}
// memory usage
unset($existingObj);
unset($obj);
}
/**
* 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) {
// 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[$fieldName])) {
return false;
//user_error("CsvBulkLoader:processRecord: Couldn't find duplicate identifier '{$fieldName}' in columns", E_USER_ERROR);
}
$SQL_fieldValue = $record[$fieldName];
$existingRecord = DataObject::get_one($this->objectClass, "`$SQL_fieldName` = '{$SQL_fieldValue}'");
if($existingRecord) return $existingRecord;
} elseif(is_array($duplicateCheck) && isset($duplicateCheck['callback'])) {
$existingRecord = singleton($this->objectClass)->{$duplicateCheck['callback']}($record[$fieldName], $record);
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));
}
}
?>