xref: /plugin/struct/meta/CSVImporter.php (revision eaab469c00cfe321e34b8028f8f0004daa52b91e)
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        while(($data = fgetcsv($this->fh)) !== false) {
128            $this->sqlite->query('BEGIN TRANSACTION');
129            $this->line++;
130            $this->importLine($data, $single, $multi);
131            $this->sqlite->query('COMMIT TRANSACTION');
132        }
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     * INSERT $values into data_* table
183     *
184     * @param string[] $values
185     * @param string $single SQL for single table
186     *
187     * @return string last_insert_rowid()
188     */
189    protected function insertIntoSingle($values, $single) {
190        $this->sqlite->query($single, $values);
191        $res = $this->sqlite->query('SELECT last_insert_rowid()');
192        $pid = $this->sqlite->res2single($res);
193        $this->sqlite->res_close($res);
194
195        return $pid;
196    }
197
198    /**
199     * INSERT one row into multi_* table
200     *
201     * @param string $multi SQL for multi table
202     * @param $pid string
203     * @param $column string
204     * @param $row string
205     * @param $value string
206     */
207    protected function insertIntoMulti($multi, $pid, $column, $row, $value) {
208        $this->sqlite->query($multi, array($pid, $column->getColref(), $row + 1, $value));
209    }
210
211    /**
212     * Save one CSV line into database
213     *
214     * @param string[] $values parsed line values
215     * @param string $single SQL for single table
216     * @param string $multi SQL for multi table
217     */
218    protected function saveLine($values, $line, $single, $multi) {
219        // insert into single value table (and create pid)
220        $pid = $this->insertIntoSingle($values, $single);
221
222        // insert all the multi values
223        foreach($this->columns as $i => $column) {
224            if(!$column->isMulti()) continue;
225            foreach($line[$i] as $row => $value) {
226                $this->insertIntoMulti($multi, $pid, $column, $row, $value);
227            }
228        }
229    }
230
231    /**
232     * Imports one line into the schema
233     *
234     * @param string[] $line the parsed CSV line
235     * @param string $single SQL for single table
236     * @param string $multi SQL for multi table
237     */
238    protected function importLine($line, $single, $multi) {
239        //read values, false if no validation
240        $values = $this->readLine($line);
241
242        if($values) {
243            $this->saveLine($values, $line, $single, $multi);
244        } else foreach($this->errors as $error) {
245            msg($error, -1);
246        }
247    }
248}
249