xref: /plugin/struct/meta/SchemaBuilder.php (revision 38edbfed4369e996ab2fd8e621d25df72db0d574)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5/**
6 * Class SchemaBuilder
7 *
8 * This class builds and updates the schema definitions for our tables. This includes CREATEing and ALTERing
9 * the actual data tables as well as updating the meta information in our meta data tables.
10 *
11 * To use, simply instantiate a new object of the Builder and run the build() method on it.
12 *
13 * Note: even though data tables use a data_ prefix in the database, this prefix is internal only and should
14 *       never be passed as $table anywhere!
15 *
16 * @package dokuwiki\plugin\struct\meta
17 */
18class SchemaBuilder {
19
20    /**
21     * @var array The posted new data for the schema
22     * @see Schema::AdminEditor()
23     */
24    protected $data = array();
25
26    protected $user;
27
28    /**
29     * @var string The table name associated with the schema
30     */
31    protected $table = '';
32
33    /**
34     * @var Schema the previously valid schema for this table
35     */
36    protected $oldschema;
37
38    /** @var int the ID of the newly created schema */
39    protected $newschemaid = 0;
40
41    /** @var \helper_plugin_struct_db */
42    protected $helper;
43
44    /** @var \helper_plugin_sqlite|null  */
45    protected $sqlite;
46
47    /** @var int the time for which this schema should be created - default to time() can be overriden for tests */
48    protected $time = 0;
49
50    /**
51     * SchemaBuilder constructor.
52     *
53     * @param string $table The table's name
54     * @param array $data The defining of the table (basically what get's posted in the schema editor form)
55     * @see Schema::AdminEditor()
56     */
57    public function __construct($table, $data) {
58        $this->table = $table;
59        $this->data = $data;
60        $this->oldschema = new Schema($table);
61
62        $this->helper = plugin_load('helper', 'struct_db');
63        $this->sqlite = $this->helper->getDB();
64        $this->user = $_SERVER['REMOTE_USER'];
65    }
66
67    /**
68     * Create the new schema
69     *
70     * @param int $time when to create this schema 0 for now
71     * @return bool|int the new schema id on success
72     */
73    public function build($time=0) {
74        $this->time = $time;
75        $this->fixLabelUniqueness();
76
77        $this->sqlite->query('BEGIN TRANSACTION');
78        $ok = true;
79        // create the data table if new schema
80        if(!$this->oldschema->getId()) {
81            $ok = $this->newDataTable();
82        }
83
84        // create a new schema
85        $ok = $ok && $this->newSchema();
86
87        // update column info
88        $ok = $ok && $this->updateColumns();
89        $ok = $ok && $this->addColumns();
90
91        if (!$ok) {
92            $this->sqlite->query('ROLLBACK TRANSACTION');
93            return false;
94        }
95        $this->sqlite->query('COMMIT TRANSACTION');
96
97        return $this->newschemaid;
98    }
99
100    /**
101     * Makes sure all labels in the schema to save are unique
102     */
103    protected function fixLabelUniqueness() {
104        $labels = array();
105
106        if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) {
107            $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
108        }
109
110        if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) {
111            $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
112        }
113    }
114
115    /**
116     * Creates a unique label from the given one
117     *
118     * @param string $wantedlabel
119     * @param array $labels list of already assigned labels (will be filled)
120     * @return string
121     */
122    protected function fixLabel($wantedlabel, &$labels) {
123        $wantedlabel = trim($wantedlabel);
124        $fixedlabel = $wantedlabel;
125        $idx = 1;
126        while(isset($labels[utf8_strtolower($fixedlabel)])) {
127            $fixedlabel = $wantedlabel.$idx++;
128        }
129        // did we actually do a rename? apply it.
130        if($fixedlabel != $wantedlabel) {
131            msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1);
132            $this->data['cols']['label'] = $fixedlabel;
133        }
134        $labels[utf8_strtolower($fixedlabel)] = 1;
135        return $fixedlabel;
136    }
137
138    /**
139     * Creates a new schema
140     *
141     * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!?
142     */
143    protected function newSchema() {
144        if(!$this->time) $this->time = time();
145
146        $sql = "INSERT INTO schemas (tbl, ts, user) VALUES (?, ?, ?)";
147        $this->sqlite->query($sql, $this->table, $this->time, $this->user);
148        $res = $this->sqlite->query('SELECT last_insert_rowid()');
149        $this->newschemaid = $this->sqlite->res2single($res);
150        $this->sqlite->res_close($res);
151        if(!$this->newschemaid) return false;
152        return true;
153    }
154
155    /**
156     * Updates all the existing column infos and adds them to the new schema
157     */
158    protected function updateColumns() {
159        foreach($this->oldschema->getColumns() as $column) {
160            $oldEntry = $column->getType()->getAsEntry();
161            $oldTid   = $column->getTid();
162            $newEntry = $oldEntry;
163            $newTid   = $oldTid;
164            $sort = $column->getSort();
165            if(isset($this->data['cols'][$column->getColref()])){
166                // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place
167                $newEntry['config'] = $this->data['cols'][$column->getColref()]['config'];
168                $newEntry['label'] = $this->data['cols'][$column->getColref()]['label'];
169                $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti'];
170                $newEntry['class'] = $this->data['cols'][$column->getColref()]['class'];
171                $sort = $this->data['cols'][$column->getColref()]['sort'];
172                $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled'];
173
174                // when the type definition has changed, we create a new one
175                if(array_diff_assoc($oldEntry, $newEntry)) {
176                    $ok = $this->sqlite->storeEntry('types', $newEntry);
177                    if(!$ok) return false;
178                    $res = $this->sqlite->query('SELECT last_insert_rowid()');
179                    if(!$res) return false;
180                    $newTid = $this->sqlite->res2single($res);
181                    $this->sqlite->res_close($res);
182                }
183            } else {
184                $enabled = false; // no longer there for some reason
185            }
186
187            // add this type to the schema columns
188            $schemaEntry = array(
189                'sid' => $this->newschemaid,
190                'colref' => $column->getColref(),
191                'enabled' => $enabled,
192                'tid' => $newTid,
193                'sort' => $sort
194            );
195            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
196            if(!$ok) return false;
197        }
198        return true;
199    }
200
201    /**
202     * Adds new columns to the new schema
203     *
204     * @return bool
205     */
206    protected function addColumns() {
207        if(!isset($this->data['new'])) return true;
208
209        $colref = count($this->oldschema->getColumns())+1;
210
211        foreach($this->data['new'] as $column) {
212            if(!$column['isenabled']) continue; // we do not add a disabled column
213
214            // todo this duplicates the hardcoding as in  the function above
215            $newEntry = array();
216            $newEntry['config'] = $column['config'];
217            $newEntry['label'] = $column['label'];
218            $newEntry['ismulti'] = $column['ismulti'];
219            $newEntry['class'] = $column['class'];
220            $sort = $column['sort'];
221
222
223            // only save if the column got a name
224            if(!$newEntry['label']) continue;
225
226            // add new column to the data table
227            if(!$this->addDataTableColumn($colref)) {
228                return false;
229            }
230
231            // save the type
232            $ok = $this->sqlite->storeEntry('types', $newEntry);
233            if(!$ok) return false;
234            $res = $this->sqlite->query('SELECT last_insert_rowid()');
235            if(!$res) return false;
236            $newTid = $this->sqlite->res2single($res);
237            $this->sqlite->res_close($res);
238
239
240            // add this type to the schema columns
241            $schemaEntry = array(
242                'sid' => $this->newschemaid,
243                'colref' => $colref,
244                'enabled' => true,
245                'tid' => $newTid,
246                'sort' => $sort
247            );
248            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
249            if(!$ok) return false;
250            $colref++;
251        }
252
253        return true;
254    }
255
256    /**
257     * Create a completely new data table with no columns yet also create the appropriate
258     * multi value table for the schema
259     *
260     * @todo how do we want to handle indexes?
261     * @return bool
262     */
263    protected function newDataTable() {
264        $ok = true;
265
266        $tbl = 'data_' . $this->table;
267        $sql = "CREATE TABLE $tbl (
268                    pid NOT NULL,
269                    rev INTEGER NOT NULL,
270                    latest BOOLEAN NOT NULL DEFAULT 0,
271                    PRIMARY KEY(pid, rev)
272                )";
273        $ok = $ok && (bool) $this->sqlite->query($sql);
274
275        $tbl = 'multi_' . $this->table;
276        $sql = "CREATE TABLE $tbl (
277                    colref INTEGER NOT NULL,
278                    pid NOT NULL,
279                    rev INTEGER NOT NULL,
280                    row INTEGER NOT NULL,
281                    value,
282                    PRIMARY KEY(colref, pid, rev, row)
283                );";
284        $ok = $ok && (bool) $this->sqlite->query($sql);
285
286        return $ok;
287    }
288
289    /**
290     * Add an additional column to the existing data table
291     *
292     * @param int $index the new column index to add
293     * @return bool
294     */
295    protected function addDataTableColumn($index) {
296        $tbl = 'data_' . $this->table;
297        $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''";
298        if(! $this->sqlite->query($sql)) {
299            return false;
300        }
301        return true;
302    }
303
304    /**
305     * @param string $user
306     * @return SchemaBuilder
307     */
308    public function setUser($user) {
309        $this->user = $user;
310        return $this;
311    }
312
313
314}
315