1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5/** 6 * Class CSVImporter 7 * 8 * Imports CSV data into a lookup schema 9 * 10 * @package dokuwiki\plugin\struct\meta 11 */ 12abstract class CSVImporter { 13 14 /** @var Schema */ 15 protected $schema; 16 17 /** @var resource */ 18 protected $fh; 19 20 /** @var \helper_plugin_sqlite */ 21 protected $sqlite; 22 23 /** @var Column[] The single values to store index => col */ 24 protected $columns = array(); 25 26 /** @var int current line number */ 27 protected $line = 0; 28 29 /** @var list of headers */ 30 protected $header; 31 32 /** @var array list of validation errors */ 33 protected $errors; 34 35 /** 36 * CSVImporter constructor. 37 * 38 * @throws StructException 39 * @param string $table 40 * @param string $file 41 */ 42 public function __construct($table, $file) { 43 $this->fh = fopen($file, 'r'); 44 if(!$this->fh) throw new StructException('Failed to open CSV file for reading'); 45 46 $this->schema = new Schema($table); 47 if(!$this->schema->getId()) throw new StructException('Schema does not exist'); 48 49 /** @var \helper_plugin_struct_db $db */ 50 $db = plugin_load('helper', 'struct_db'); 51 $this->sqlite = $db->getDB(true); 52 } 53 54 /** 55 * Import the data from file. 56 * 57 * @throws StructException 58 */ 59 public function import() { 60 // Do the import 61 $this->readHeaders(); 62 $this->importCSV(); 63 } 64 65 /** 66 * Read the CSV headers and match it with the Schema columns 67 * 68 * @return array headers of file 69 */ 70 protected function readHeaders() { 71 $header = fgetcsv($this->fh); 72 if(!$header) throw new StructException('Failed to read CSV'); 73 $this->line++; 74 75 foreach($header as $i => $head) { 76 $col = $this->schema->findColumn($head); 77 if(!$col) continue; 78 if(!$col->isEnabled()) continue; 79 $this->columns[$i] = $col; 80 } 81 82 if(!$this->columns) { 83 throw new StructException('None of the CSV headers matched any of the schema\'s fields'); 84 } 85 86 $this->header = $header; 87 } 88 89 /** 90 * Creates the insert string for the single value table 91 * 92 * @return string 93 */ 94 protected function getSQLforAllValues() { 95 $colnames = array(); 96 $placeholds = array(); 97 foreach($this->columns as $i => $col) { 98 $colnames[] = 'col' . $col->getColref(); 99 $placeholds[] = '?'; 100 } 101 $colnames = join(', ', $colnames); 102 $placeholds = join(', ', $placeholds); 103 $table = $this->schema->getTable(); 104 105 return "INSERT INTO data_$table ($colnames) VALUES ($placeholds)"; 106 } 107 108 /** 109 * Creates the insert string for the multi value table 110 * 111 * @return string 112 */ 113 protected function getSQLforMultiValue() { 114 $table = $this->schema->getTable(); 115 /** @noinspection SqlResolve */ 116 return "INSERT INTO multi_$table (pid, colref, row, value) VALUES (?,?,?,?)"; 117 } 118 119 /** 120 * Walks through the CSV and imports 121 */ 122 protected function importCSV() { 123 124 $single = $this->getSQLforAllValues(); 125 $multi = $this->getSQLforMultiValue(); 126 127 $this->sqlite->query('BEGIN TRANSACTION'); 128 while(($data = fgetcsv($this->fh)) !== false) { 129 $this->line++; 130 $this->importLine($data, $single, $multi); 131 } 132 $this->sqlite->query('COMMIT TRANSACTION'); 133 } 134 135 /** 136 * The errors that occured during validation 137 * 138 * @return string[] already translated error messages 139 */ 140 public function getErrors() { 141 return $this->errors; 142 } 143 144 /** 145 * Validate a single value 146 * 147 * @param Column $col the column of that value 148 * @param mixed &$rawvalue the value, will be fixed according to the type 149 * @return bool true if the data validates, otherwise false 150 */ 151 protected function validateValue(Column $col, &$rawvalue) { 152 //by default no validation 153 return true; 154 } 155 156 /** 157 * Read and validate CSV parsed line 158 * 159 * @param &$line 160 */ 161 protected function readLine(&$line) { 162 // prepare values for single value table 163 $values = array(); 164 foreach($this->columns as $i => $column) { 165 if(!isset($line[$i])) throw new StructException('Missing field at CSV line %d', $this->line); 166 167 if(!$this->validateValue($column, $line[$i])) return false; 168 169 if($column->isMulti()) { 170 // multi values get split on comma 171 $line[$i] = array_map('trim', explode(',', $line[$i])); 172 $values[] = $line[$i][0]; 173 } else { 174 $values[] = $line[$i]; 175 } 176 } 177 //if no ok don't import 178 return $values; 179 } 180 181 /** 182 * @param string[] $values 183 * @param string $single SQL for single table 184 * 185 * @return string pid 186 */ 187 protected function insertIntoSingle($values, $single) { 188 $this->sqlite->query($single, $values); 189 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 190 $pid = $this->sqlite->res2single($res); 191 $this->sqlite->res_close($res); 192 193 return $pid; 194 } 195 196 /** 197 * @param string $multi SQL for multi table 198 * @param $pid string 199 * @param $column string 200 * @param $row string 201 * @param $value string 202 */ 203 protected function insertIntoMulti($multi, $pid, $column, $row, $value) { 204 $this->sqlite->query($multi, array($pid, $column->getColref(), $row + 1, $value)); 205 } 206 207 /** 208 * @param string[] $values parsed line values 209 * @param string $single SQL for single table 210 * @param string $multi SQL for multi table 211 */ 212 protected function saveLine($values, $line, $single, $multi) { 213 // insert into single value table (and create pid) 214 $pid = $this->insertIntoSingle($values, $single); 215 216 // insert all the multi values 217 foreach($this->columns as $i => $column) { 218 if(!$column->isMulti()) continue; 219 foreach($line[$i] as $row => $value) { 220 $this->insertIntoMulti($multi, $pid, $column, $row, $value); 221 } 222 } 223 } 224 225 /** 226 * Imports one line into the schema 227 * 228 * @param string[] $line the parsed CSV line 229 * @param string $single SQL for single table 230 * @param string $multi SQL for multi table 231 */ 232 protected function importLine($line, $single, $multi) { 233 //read values, false if no validation 234 $values = $this->readLine($line); 235 236 if($values) { 237 $this->saveLine($values, $line, $single, $multi); 238 } else foreach($this->errors as $error) { 239 msg($error, -1); 240 } 241 } 242} 243