1a0b3799eSAndreas Gohr<?php 2a0b3799eSAndreas Gohr 3a0b3799eSAndreas Gohrnamespace dokuwiki\plugin\struct\meta; 4a0b3799eSAndreas Gohr 5a0b3799eSAndreas Gohr/** 6f36cc634SAndreas Gohr * Class CSVImporter 7a0b3799eSAndreas Gohr * 8f36cc634SAndreas Gohr * Imports CSV data into a lookup schema 9a0b3799eSAndreas Gohr * 10a0b3799eSAndreas Gohr * @package dokuwiki\plugin\struct\meta 11a0b3799eSAndreas Gohr */ 121fc2361fSSzymon Olewniczakabstract class CSVImporter { 13a0b3799eSAndreas Gohr 14a0b3799eSAndreas Gohr /** @var Schema */ 15a0b3799eSAndreas Gohr protected $schema; 16a0b3799eSAndreas Gohr 17a0b3799eSAndreas Gohr /** @var resource */ 18a0b3799eSAndreas Gohr protected $fh; 19a0b3799eSAndreas Gohr 20a0b3799eSAndreas Gohr /** @var \helper_plugin_sqlite */ 21a0b3799eSAndreas Gohr protected $sqlite; 22a0b3799eSAndreas Gohr 23a0b3799eSAndreas Gohr /** @var Column[] The single values to store index => col */ 24a0b3799eSAndreas Gohr protected $columns = array(); 25a0b3799eSAndreas Gohr 26a0b3799eSAndreas Gohr /** @var int current line number */ 27a0b3799eSAndreas Gohr protected $line = 0; 28a0b3799eSAndreas Gohr 291fc2361fSSzymon Olewniczak /** @var list of headers */ 301fc2361fSSzymon Olewniczak protected $header; 311fc2361fSSzymon Olewniczak 321fc2361fSSzymon Olewniczak /** @var array list of validation errors */ 331fc2361fSSzymon Olewniczak protected $errors; 341fc2361fSSzymon Olewniczak 35a0b3799eSAndreas Gohr /** 36a0b3799eSAndreas Gohr * CSVImporter constructor. 37a0b3799eSAndreas Gohr * 38a0b3799eSAndreas Gohr * @throws StructException 39a0b3799eSAndreas Gohr * @param string $table 40a0b3799eSAndreas Gohr * @param string $file 41a0b3799eSAndreas Gohr */ 42a0b3799eSAndreas Gohr public function __construct($table, $file) { 43a0b3799eSAndreas Gohr $this->fh = fopen($file, 'r'); 44a0b3799eSAndreas Gohr if(!$this->fh) throw new StructException('Failed to open CSV file for reading'); 45a0b3799eSAndreas Gohr 46a0b3799eSAndreas Gohr $this->schema = new Schema($table); 47a0b3799eSAndreas Gohr if(!$this->schema->getId()) throw new StructException('Schema does not exist'); 48a0b3799eSAndreas Gohr 49a0b3799eSAndreas Gohr /** @var \helper_plugin_struct_db $db */ 50a0b3799eSAndreas Gohr $db = plugin_load('helper', 'struct_db'); 51a0b3799eSAndreas Gohr $this->sqlite = $db->getDB(true); 521fc2361fSSzymon Olewniczak } 53a0b3799eSAndreas Gohr 541fc2361fSSzymon Olewniczak /** 551fc2361fSSzymon Olewniczak * Import the data from file. 561fc2361fSSzymon Olewniczak * 571fc2361fSSzymon Olewniczak * @throws StructException 581fc2361fSSzymon Olewniczak */ 591fc2361fSSzymon Olewniczak public function import() { 60a0b3799eSAndreas Gohr // Do the import 61a0b3799eSAndreas Gohr $this->readHeaders(); 62a0b3799eSAndreas Gohr $this->importCSV(); 63a0b3799eSAndreas Gohr } 64a0b3799eSAndreas Gohr 65a0b3799eSAndreas Gohr /** 66a0b3799eSAndreas Gohr * Read the CSV headers and match it with the Schema columns 671fc2361fSSzymon Olewniczak * 681fc2361fSSzymon Olewniczak * @return array headers of file 69a0b3799eSAndreas Gohr */ 70a0b3799eSAndreas Gohr protected function readHeaders() { 71a0b3799eSAndreas Gohr $header = fgetcsv($this->fh); 72a0b3799eSAndreas Gohr if(!$header) throw new StructException('Failed to read CSV'); 73a0b3799eSAndreas Gohr $this->line++; 74a0b3799eSAndreas Gohr 75a0b3799eSAndreas Gohr foreach($header as $i => $head) { 76a0b3799eSAndreas Gohr $col = $this->schema->findColumn($head); 77a0b3799eSAndreas Gohr if(!$col) continue; 78a0b3799eSAndreas Gohr if(!$col->isEnabled()) continue; 79a0b3799eSAndreas Gohr $this->columns[$i] = $col; 80a0b3799eSAndreas Gohr } 81a0b3799eSAndreas Gohr 82a0b3799eSAndreas Gohr if(!$this->columns) { 83a0b3799eSAndreas Gohr throw new StructException('None of the CSV headers matched any of the schema\'s fields'); 84a0b3799eSAndreas Gohr } 851fc2361fSSzymon Olewniczak 861fc2361fSSzymon Olewniczak $this->header = $header; 87a0b3799eSAndreas Gohr } 88a0b3799eSAndreas Gohr 89a0b3799eSAndreas Gohr /** 90a0b3799eSAndreas Gohr * Creates the insert string for the single value table 91a0b3799eSAndreas Gohr * 92a0b3799eSAndreas Gohr * @return string 93a0b3799eSAndreas Gohr */ 94a0b3799eSAndreas Gohr protected function getSQLforAllValues() { 95a0b3799eSAndreas Gohr $colnames = array(); 96a0b3799eSAndreas Gohr $placeholds = array(); 97a0b3799eSAndreas Gohr foreach($this->columns as $i => $col) { 98a0b3799eSAndreas Gohr $colnames[] = 'col' . $col->getColref(); 99a0b3799eSAndreas Gohr $placeholds[] = '?'; 100a0b3799eSAndreas Gohr } 101a0b3799eSAndreas Gohr $colnames = join(', ', $colnames); 102a0b3799eSAndreas Gohr $placeholds = join(', ', $placeholds); 103a0b3799eSAndreas Gohr $table = $this->schema->getTable(); 104a0b3799eSAndreas Gohr 105a0b3799eSAndreas Gohr return "INSERT INTO data_$table ($colnames) VALUES ($placeholds)"; 106a0b3799eSAndreas Gohr } 107a0b3799eSAndreas Gohr 108a0b3799eSAndreas Gohr /** 109a0b3799eSAndreas Gohr * Creates the insert string for the multi value table 110a0b3799eSAndreas Gohr * 111a0b3799eSAndreas Gohr * @return string 112a0b3799eSAndreas Gohr */ 113a0b3799eSAndreas Gohr protected function getSQLforMultiValue() { 114a0b3799eSAndreas Gohr $table = $this->schema->getTable(); 115a0b3799eSAndreas Gohr /** @noinspection SqlResolve */ 116a0b3799eSAndreas Gohr return "INSERT INTO multi_$table (pid, colref, row, value) VALUES (?,?,?,?)"; 117a0b3799eSAndreas Gohr } 118a0b3799eSAndreas Gohr 119a0b3799eSAndreas Gohr /** 120a0b3799eSAndreas Gohr * Walks through the CSV and imports 121a0b3799eSAndreas Gohr */ 122a0b3799eSAndreas Gohr protected function importCSV() { 123a0b3799eSAndreas Gohr 124a0b3799eSAndreas Gohr $single = $this->getSQLforAllValues(); 125a0b3799eSAndreas Gohr $multi = $this->getSQLforMultiValue(); 126a0b3799eSAndreas Gohr 127a0b3799eSAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 128a0b3799eSAndreas Gohr while(($data = fgetcsv($this->fh)) !== false) { 129a0b3799eSAndreas Gohr $this->line++; 130a0b3799eSAndreas Gohr $this->importLine($data, $single, $multi); 131a0b3799eSAndreas Gohr } 132a0b3799eSAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 133a0b3799eSAndreas Gohr } 134a0b3799eSAndreas Gohr 135a0b3799eSAndreas Gohr /** 1361fc2361fSSzymon Olewniczak * The errors that occured during validation 137a0b3799eSAndreas Gohr * 1381fc2361fSSzymon Olewniczak * @return string[] already translated error messages 139a0b3799eSAndreas Gohr */ 1401fc2361fSSzymon Olewniczak public function getErrors() { 1411fc2361fSSzymon Olewniczak return $this->errors; 1421fc2361fSSzymon Olewniczak } 1431fc2361fSSzymon Olewniczak 1441fc2361fSSzymon Olewniczak /** 1451fc2361fSSzymon Olewniczak * Validate a single value 1461fc2361fSSzymon Olewniczak * 1471fc2361fSSzymon Olewniczak * @param Column $col the column of that value 1481fc2361fSSzymon Olewniczak * @param mixed &$rawvalue the value, will be fixed according to the type 1491fc2361fSSzymon Olewniczak * @return bool true if the data validates, otherwise false 1501fc2361fSSzymon Olewniczak */ 1511fc2361fSSzymon Olewniczak protected function validateValue(Column $col, &$rawvalue) { 1521fc2361fSSzymon Olewniczak //by default no validation 1531fc2361fSSzymon Olewniczak return true; 1541fc2361fSSzymon Olewniczak } 1551fc2361fSSzymon Olewniczak 1561fc2361fSSzymon Olewniczak /** 1571fc2361fSSzymon Olewniczak * Read and validate CSV parsed line 1581fc2361fSSzymon Olewniczak * 1591fc2361fSSzymon Olewniczak * @param &$line 1601fc2361fSSzymon Olewniczak */ 1611fc2361fSSzymon Olewniczak protected function readLine(&$line) { 162a0b3799eSAndreas Gohr // prepare values for single value table 163a0b3799eSAndreas Gohr $values = array(); 164a0b3799eSAndreas Gohr foreach($this->columns as $i => $column) { 165a0b3799eSAndreas Gohr if(!isset($line[$i])) throw new StructException('Missing field at CSV line %d', $this->line); 166a0b3799eSAndreas Gohr 1671fc2361fSSzymon Olewniczak if(!$this->validateValue($column, $line[$i])) return false; 1681fc2361fSSzymon Olewniczak 169a0b3799eSAndreas Gohr if($column->isMulti()) { 170a0b3799eSAndreas Gohr // multi values get split on comma 171a0b3799eSAndreas Gohr $line[$i] = array_map('trim', explode(',', $line[$i])); 172a0b3799eSAndreas Gohr $values[] = $line[$i][0]; 173a0b3799eSAndreas Gohr } else { 174a0b3799eSAndreas Gohr $values[] = $line[$i]; 175a0b3799eSAndreas Gohr } 176a0b3799eSAndreas Gohr } 1771fc2361fSSzymon Olewniczak //if no ok don't import 1781fc2361fSSzymon Olewniczak return $values; 1791fc2361fSSzymon Olewniczak } 180a0b3799eSAndreas Gohr 1811fc2361fSSzymon Olewniczak /** 182*69c07c1bSSzymon Olewniczak * INSERT $values into data_* table 183*69c07c1bSSzymon Olewniczak * 1841fc2361fSSzymon Olewniczak * @param string[] $values 1851fc2361fSSzymon Olewniczak * @param string $single SQL for single table 1861fc2361fSSzymon Olewniczak * 187*69c07c1bSSzymon Olewniczak * @return string last_insert_rowid() 1881fc2361fSSzymon Olewniczak */ 1891fc2361fSSzymon Olewniczak protected function insertIntoSingle($values, $single) { 190a0b3799eSAndreas Gohr $this->sqlite->query($single, $values); 191a0b3799eSAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 192a0b3799eSAndreas Gohr $pid = $this->sqlite->res2single($res); 193a0b3799eSAndreas Gohr $this->sqlite->res_close($res); 194a0b3799eSAndreas Gohr 1951fc2361fSSzymon Olewniczak return $pid; 1961fc2361fSSzymon Olewniczak } 1971fc2361fSSzymon Olewniczak 1981fc2361fSSzymon Olewniczak /** 199*69c07c1bSSzymon Olewniczak * INSERT one row into multi_* table 200*69c07c1bSSzymon Olewniczak * 2011fc2361fSSzymon Olewniczak * @param string $multi SQL for multi table 2021fc2361fSSzymon Olewniczak * @param $pid string 2031fc2361fSSzymon Olewniczak * @param $column string 2041fc2361fSSzymon Olewniczak * @param $row string 2051fc2361fSSzymon Olewniczak * @param $value string 2061fc2361fSSzymon Olewniczak */ 2071fc2361fSSzymon Olewniczak protected function insertIntoMulti($multi, $pid, $column, $row, $value) { 2081fc2361fSSzymon Olewniczak $this->sqlite->query($multi, array($pid, $column->getColref(), $row + 1, $value)); 2091fc2361fSSzymon Olewniczak } 2101fc2361fSSzymon Olewniczak 2111fc2361fSSzymon Olewniczak /** 212*69c07c1bSSzymon Olewniczak * Save one CSV line into database 213*69c07c1bSSzymon Olewniczak * 2141fc2361fSSzymon Olewniczak * @param string[] $values parsed line values 2151fc2361fSSzymon Olewniczak * @param string $single SQL for single table 2161fc2361fSSzymon Olewniczak * @param string $multi SQL for multi table 2171fc2361fSSzymon Olewniczak */ 2181fc2361fSSzymon Olewniczak protected function saveLine($values, $line, $single, $multi) { 2191fc2361fSSzymon Olewniczak // insert into single value table (and create pid) 2201fc2361fSSzymon Olewniczak $pid = $this->insertIntoSingle($values, $single); 2211fc2361fSSzymon Olewniczak 222a0b3799eSAndreas Gohr // insert all the multi values 223a0b3799eSAndreas Gohr foreach($this->columns as $i => $column) { 224a0b3799eSAndreas Gohr if(!$column->isMulti()) continue; 225a0b3799eSAndreas Gohr foreach($line[$i] as $row => $value) { 2261fc2361fSSzymon Olewniczak $this->insertIntoMulti($multi, $pid, $column, $row, $value); 227a0b3799eSAndreas Gohr } 228a0b3799eSAndreas Gohr } 229a0b3799eSAndreas Gohr } 230a0b3799eSAndreas Gohr 2311fc2361fSSzymon Olewniczak /** 2321fc2361fSSzymon Olewniczak * Imports one line into the schema 2331fc2361fSSzymon Olewniczak * 2341fc2361fSSzymon Olewniczak * @param string[] $line the parsed CSV line 2351fc2361fSSzymon Olewniczak * @param string $single SQL for single table 2361fc2361fSSzymon Olewniczak * @param string $multi SQL for multi table 2371fc2361fSSzymon Olewniczak */ 2381fc2361fSSzymon Olewniczak protected function importLine($line, $single, $multi) { 2391fc2361fSSzymon Olewniczak //read values, false if no validation 2401fc2361fSSzymon Olewniczak $values = $this->readLine($line); 2411fc2361fSSzymon Olewniczak 2421fc2361fSSzymon Olewniczak if($values) { 2431fc2361fSSzymon Olewniczak $this->saveLine($values, $line, $single, $multi); 2441fc2361fSSzymon Olewniczak } else foreach($this->errors as $error) { 2451fc2361fSSzymon Olewniczak msg($error, -1); 2461fc2361fSSzymon Olewniczak } 2471fc2361fSSzymon Olewniczak } 248a0b3799eSAndreas Gohr} 249