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