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