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