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 foreach($this->data['cols'] as $idx => $column) { 99 $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 100 } 101 102 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[$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[$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 $enabled = true; 155 $sort = $column->getSort(); 156 if(isset($this->data['cols'][$column->getColref()])){ 157 // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 158 $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 159 $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 160 $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 161 $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 162 $sort = $this->data['cols'][$column->getColref()]['sort']; 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 FIXME this assumes we remove the entry from the form completely. We might not want to do that 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 // todo this duplicates the hardcoding as in the function above 203 $newEntry = array(); 204 $newEntry['config'] = $column['config']; 205 $newEntry['label'] = $column['label']; 206 $newEntry['ismulti'] = $column['ismulti']; 207 $newEntry['class'] = $column['class']; 208 $sort = $column['sort']; 209 $enabled = true; 210 211 // only save if the column got a name 212 if(!$newEntry['label']) continue; 213 214 // add new column to the data table 215 if(!$this->addDataTableColumn($colref)) { 216 return false; 217 } 218 219 // save the type 220 $ok = $this->sqlite->storeEntry('types', $newEntry); 221 if(!$ok) return false; 222 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 223 if(!$res) return false; 224 $newTid = $this->sqlite->res2single($res); 225 $this->sqlite->res_close($res); 226 227 228 // add this type to the schema columns 229 $schemaEntry = array( 230 'sid' => $this->newschemaid, 231 'colref' => $colref, 232 'enabled' => $enabled, 233 'tid' => $newTid, 234 'sort' => $sort 235 ); 236 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 237 if(!$ok) return false; 238 $colref++; 239 } 240 241 return true; 242 } 243 244 /** 245 * Create a completely new data table with no columns yet also create the appropriate 246 * multi value table for the schema 247 * 248 * @todo how do we want to handle indexes? 249 * @return bool 250 */ 251 protected function newDataTable() { 252 $ok = true; 253 254 $tbl = 'data_' . $this->table; 255 $sql = "CREATE TABLE $tbl ( 256 pid NOT NULL, 257 rev INTEGER NOT NULL, 258 latest BOOLEAN NOT NULL DEFAULT 0, 259 PRIMARY KEY(pid, rev) 260 )"; 261 $ok = $ok && (bool) $this->sqlite->query($sql); 262 263 $tbl = 'multi_' . $this->table; 264 $sql = "CREATE TABLE $tbl ( 265 colref INTEGER NOT NULL, 266 pid NOT NULL, 267 rev INTEGER NOT NULL, 268 row INTEGER NOT NULL, 269 value, 270 PRIMARY KEY(colref, pid, rev, row) 271 );"; 272 $ok = $ok && (bool) $this->sqlite->query($sql); 273 274 return $ok; 275 } 276 277 /** 278 * Add an additional column to the existing data table 279 * 280 * @param int $index the new column index to add 281 * @return bool 282 */ 283 protected function addDataTableColumn($index) { 284 $tbl = 'data_' . $this->table; 285 $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 286 if(! $this->sqlite->query($sql)) { 287 return false; 288 } 289 return true; 290 } 291 292} 293