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_sqlite|null */ 40 protected $sqlite; 41 42 /** 43 * SchemaBuilder constructor. 44 * 45 * @param string $table The table's name 46 * @param array $data The defining of the table (basically what get's posted in the schema editor form) 47 * @see Schema::AdminEditor() 48 */ 49 public function __construct($table, $data) { 50 $this->table = $table; 51 $this->data = $data; 52 $this->oldschema = new Schema($table); 53 54 /** @var \helper_plugin_struct_db $helper */ 55 $helper = plugin_load('helper', 'struct_db'); 56 $this->sqlite = $helper->getDB(); 57 } 58 59 /** 60 * Create the new schema 61 * 62 * @return bool|int the new schema id on success 63 */ 64 public function build() { 65 $this->sqlite->query('BEGIN TRANSACTION'); 66 $ok = true; 67 // create the data table if new schema 68 if(!$this->oldschema->getId()) { 69 $ok = $this->newDataTable(); 70 } 71 72 // create a new schema 73 $ok = $ok && $this->newSchema(); 74 75 // update column info 76 $ok = $ok && $this->updateColumns(); 77 $ok = $ok && $this->addColumns(); 78 79 if (!$ok) { 80 $this->sqlite->query('ROLLBACK TRANSACTION'); 81 return false; 82 } 83 $this->sqlite->query('COMMIT TRANSACTION'); 84 85 return $this->newschemaid; 86 } 87 88 /** 89 * Creates a new schema 90 * 91 * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!? 92 */ 93 protected function newSchema() { 94 $sql = "INSERT INTO schemas (tbl, ts) VALUES (?, ?)"; 95 $this->sqlite->query($sql, $this->table, time()); 96 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 97 $this->newschemaid = $this->sqlite->res2single($res); 98 $this->sqlite->res_close($res); 99 if(!$this->newschemaid) return false; 100 return true; 101 } 102 103 /** 104 * Updates all the existing column infos and adds them to the new schema 105 */ 106 protected function updateColumns() { 107 foreach($this->oldschema->getColumns() as $column) { 108 $oldEntry = $column->getType()->getAsEntry(); 109 $oldTid = $column->getTid(); 110 $newEntry = $oldEntry; 111 $newTid = $oldTid; 112 $enabled = true; 113 $sort = $column->getSort(); 114 if(isset($this->data['cols'][$column->getColref()])){ 115 // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 116 $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 117 $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 118 $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 119 $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 120 $sort = $this->data['cols'][$column->getColref()]['sort']; 121 122 // when the type definition has changed, we create a new one 123 if(array_diff_assoc($oldEntry, $newEntry)) { 124 $ok = $this->sqlite->storeEntry('types', $newEntry); 125 if(!$ok) return false; 126 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 127 if(!$res) return false; 128 $newTid = $this->sqlite->res2single($res); 129 $this->sqlite->res_close($res); 130 } 131 } else { 132 $enabled = false; // no longer there FIXME this assumes we remove the entry from the form completely. We might not want to do that 133 } 134 135 // add this type to the schema columns 136 $schemaEntry = array( 137 'sid' => $this->newschemaid, 138 'colref' => $column->getColref(), 139 'enabled' => $enabled, 140 'tid' => $newTid, 141 'sort' => $sort 142 ); 143 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 144 if(!$ok) return false; 145 } 146 return true; 147 } 148 149 /** 150 * Adds new columns to the new schema 151 * 152 * @return bool 153 */ 154 protected function addColumns() { 155 if(!isset($this->data['new'])) return true; 156 157 $colref = count($this->oldschema->getColumns())+1; 158 159 foreach($this->data['new'] as $column) { 160 // todo this duplicates the hardcoding as in the function above 161 $newEntry = array(); 162 $newEntry['config'] = $column['config']; 163 $newEntry['label'] = $column['label']; 164 $newEntry['ismulti'] = $column['ismulti']; 165 $newEntry['class'] = $column['class']; 166 $sort = $column['sort']; 167 $enabled = true; 168 169 // only save if the column got a name 170 if(!$newEntry['label']) continue; 171 172 // add new column to the data table 173 if(!$this->addDataTableColumn($colref)) { 174 return false; 175 } 176 177 // save the type 178 $ok = $this->sqlite->storeEntry('types', $newEntry); 179 if(!$ok) return false; 180 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 181 if(!$res) return false; 182 $newTid = $this->sqlite->res2single($res); 183 $this->sqlite->res_close($res); 184 185 186 // add this type to the schema columns 187 $schemaEntry = array( 188 'sid' => $this->newschemaid, 189 'colref' => $colref, 190 'enabled' => $enabled, 191 'tid' => $newTid, 192 'sort' => $sort 193 ); 194 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 195 if(!$ok) return false; 196 $colref++; 197 } 198 199 return true; 200 } 201 202 /** 203 * Create a completely new data table with columns yet 204 * 205 * @todo how do we want to handle indexes? 206 * @return bool 207 */ 208 protected function newDataTable() { 209 $tbl = 'data_' . $this->table; 210 211 $sql = "CREATE TABLE $tbl ( 212 pid NOT NULL, 213 rev INTEGER NOT NULL, 214 latest BOOLEAN NOT NULL DEFAULT 0, 215 PRIMARY KEY(pid, rev) 216 )"; 217 218 return (bool) $this->sqlite->query($sql); 219 } 220 221 /** 222 * Add an additional column to the existing data table 223 * 224 * @param int $index the new column index to add 225 * @return bool 226 */ 227 protected function addDataTableColumn($index) { 228 $tbl = 'data_' . $this->table; 229 $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 230 if(! $this->sqlite->query($sql)) { 231 return false; 232 } 233 return true; 234 } 235 236} 237