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