Version: current

      1 <?php
      2 /**
      3  * Uses the fgetcsv() function to process CSV input.
      4  * The input is expected to be UTF8.
      5  *
      6  * @see http://rfc.net/rfc4180.html
      7  * @package cms
      8  * @subpackage bulkloading
      9  * @author Ingo Schommer, Silverstripe Ltd. (<myfirstname>@silverstripe.com)
     10  *
     11  * @todo Support for deleting existing records not matched in the import (through relation checks)
     12  */
     13 class CsvBulkLoader extends BulkLoader {
     14
     15     /**
     16      * Delimiter character (Default: comma).
     17      *
     18      * @var string
     19      */
     20     public $delimiter = ',';
     21
     22     /**
     23      * Enclosure character (Default: doublequote)
     24      *
     25      * @var string
     26      */
     27     public $enclosure = '"';
     28
     29     /**
     30      * Identifies if the has a header row.
     31      * @var boolean
     32      */
     33     public $hasHeaderRow = true;
     34
     35     protected function processAll($filepath, $preview = false) {
     36         $results = new BulkLoader_Result();
     37
     38         $csv = new CSVParser($filepath, $this->delimiter, $this->enclosure);
     39
     40         // ColumnMap has two uses, depending on whether hasHeaderRow is set
     41         if($this->columnMap) {
     42             if($this->hasHeaderRow) $csv->mapColumns($this->columnMap);
     43             else $csv->provideHeaderRow($this->columnMap);
     44         }
     45
     46         foreach($csv as $row) {
     47             $this->processRecord($row, $this->columnMap, $results, $preview);
     48         }
     49
     50         return $results;
     51     }
     52
     53     /**
     54      * @todo Better messages for relation checks and duplicate detection
     55      * Note that columnMap isn't used
     56      */
     57     protected function processRecord($record, $columnMap, &$results, $preview = false) {
     58         $class = $this->objectClass;
     59
     60         // find existing object, or create new one
     61         $existingObj = $this->findExistingObject($record, $columnMap);
     62         $obj = ($existingObj) ? $existingObj : new $class();
     63
     64         // first run: find/create any relations and store them on the object
     65         // we can't combine runs, as other columns might rely on the relation being present
     66         $relations = array();
     67         foreach($record as $fieldName => $val) {
     68             // don't bother querying of value is not set
     69             if($this->isNullValue($val)) continue;
     70
     71             // checking for existing relations
     72             if(isset($this->relationCallbacks[$fieldName])) {
     73                 // trigger custom search method for finding a relation based on the given value
     74                 // and write it back to the relation (or create a new object)
     75                 $relationName = $this->relationCallbacks[$fieldName]['relationname'];
     76                 if($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
     77                     $relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}($obj, $val, $record);
     78                 } elseif($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) {
     79                     $relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
     80                 }
     81                 if(!$relationObj || !$relationObj->exists()) {
     82                     $relationClass = $obj->has_one($relationName);
     83                     $relationObj = new $relationClass();
     84                     $relationObj->write();
     85                 }
     86                 $obj->setComponent($relationName, $relationObj);
     87                 $obj->{"{$relationName}ID"} = $relationObj->ID;
     88                 $obj->write();
     89                 $obj->flushCache(); // avoid relation caching confusion
     90
     91             } elseif(strpos($fieldName, '.') !== false) {
     92                 // we have a relation column with dot notation
     93                 list($relationName,$columnName) = explode('.', $fieldName);
     94                 $relationObj = $obj->getComponent($relationName); // always gives us an component (either empty or existing)
     95                 $obj->setComponent($relationName, $relationObj);
     96                 $relationObj->write();
     97                 $obj->{"{$relationName}ID"} = $relationObj->ID;
     98                 $obj->write();
     99                 $obj->flushCache(); // avoid relation caching confusion
    100             }
    101
    102         }
    103
    104         // second run: save data
    105         foreach($record as $fieldName => $val) {
    106             if($this->isNullValue($val, $fieldName)) continue;
    107             if(strpos($fieldName, '->') !== FALSE) {
    108                 $funcName = substr($fieldName, 2);
    109                 $this->$funcName($obj, $val, $record);
    110             } else if($obj->hasMethod("import{$fieldName}")) {
    111                 $obj->{"import{$fieldName}"}($val, $record);
    112             } else {
    113                 $obj->update(array($fieldName => $val));
    114             }
    115         }
    116
    117         // write record
    118         $id = ($preview) ? 0 : $obj->write();
    119
    120         // @todo better message support
    121         $message = '';
    122
    123         // save to results
    124         if($existingObj) {
    125             $results->addUpdated($obj, $message);
    126         } else {
    127             $results->addCreated($obj, $message);
    128         }
    129
    130         $objID = $obj->ID;
    131
    132         $obj->destroy();
    133
    134         // memory usage
    135         unset($existingObj);
    136         unset($obj);
    137
    138         return $objID;
    139     }
    140
    141     /**
    142      * Find an existing objects based on one or more uniqueness
    143      * columns specified via {@link self::$duplicateChecks}
    144      *
    145      * @param array $record CSV data column
    146      * @return unknown
    147      */
    148     public function findExistingObject($record) {
    149         $SNG_objectClass = singleton($this->objectClass);
    150
    151         // checking for existing records (only if not already found)
    152         foreach($this->duplicateChecks as $fieldName => $duplicateCheck) {
    153             if(is_string($duplicateCheck)) {
    154                 $SQL_fieldName = Convert::raw2sql($duplicateCheck);
    155                 if(!isset($record[$fieldName])) {
    156                     return false;
    157                     //user_error("CsvBulkLoader:processRecord: Couldn't find duplicate identifier '{$fieldName}' in columns", E_USER_ERROR);
    158                 }
    159                 $SQL_fieldValue = $record[$fieldName];
    160                 $existingRecord = DataObject::get_one($this->objectClass, "\"$SQL_fieldName\" = '{$SQL_fieldValue}'");
    161                 if($existingRecord) return $existingRecord;
    162             } elseif(is_array($duplicateCheck) && isset($duplicateCheck['callback'])) {
    163                 if($this->hasMethod($duplicateCheck['callback'])) {
    164                     $existingRecord = $this->{$duplicateCheck['callback']}($record[$fieldName], $record);
    165                 } elseif($SNG_objectClass->hasMethod($duplicateCheck['callback'])) {
    166                     $existingRecord = $SNG_objectClass->{$duplicateCheck['callback']}($record[$fieldName], $record);
    167                 } else {
    168                     user_error("CsvBulkLoader::processRecord(): {$duplicateCheck['callback']} not found on importer or object class.", E_USER_ERROR);
    169                 }
    170
    171                 if($existingRecord) return $existingRecord;
    172             } else {
    173                 user_error('CsvBulkLoader::processRecord(): Wrong format for $duplicateChecks', E_USER_ERROR);
    174             }
    175         }
    176
    177         return false;
    178     }
    179
    180
    181     /**
    182      * Determine wether any loaded files should be parsed
    183      * with a header-row (otherwise we rely on {@link self::$columnMap}.
    184      *
    185      * @return boolean
    186      */
    187     public function hasHeaderRow() {
    188         return ($this->hasHeaderRow || isset($this->columnMap));
    189     }
    190
    191 }
    192 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -