1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5abstract class AccessTable { 6 7 /** @var Schema */ 8 protected $schema; 9 protected $pid; 10 protected $rid; 11 protected $labels = array(); 12 protected $ts = 0; 13 /** @var \helper_plugin_sqlite */ 14 protected $sqlite; 15 16 // options on how to retrieve data 17 protected $opt_skipempty = false; 18 19 /** 20 * Factory method returning the appropriate data accessor (page, lookup or serial) 21 * 22 * @param Schema $schema schema to load 23 * @param string|int $pid Page or row id to access 24 * @param int $ts Time at which the data should be read or written, 0 for now 25 * @param int $rid 26 * @return AccessTableData|AccessTableLookup|AccessTableSerial 27 */ 28 public static function bySchema(Schema $schema, $pid, $ts = 0, $rid = 0) { 29 if (!$pid && $ts === 0) { 30 return new AccessTableLookup($schema, $pid, $ts, $rid); 31 } 32 if ($pid && $ts === 0) { 33 return new AccessTableSerial($schema, $pid, $ts, $rid); 34 } 35 return new AccessTableData($schema, $pid, $ts, $rid); 36 } 37 38 /** 39 * Factory Method to access a data or lookup table 40 * 41 * @param string $tablename schema to load 42 * @param string|int $pid Page or row id to access 43 * @param int $ts Time at which the data should be read or written, 0 for now 44 * @param int $rid 45 * @return AccessTableData|AccessTableLookup 46 */ 47 public static function byTableName($tablename, $pid, $ts = 0, $rid = 0) { 48 $schema = new Schema($tablename, $ts); 49 return self::bySchema($schema, $pid, $ts, $rid); 50 } 51 52 /** 53 * AccessTable constructor 54 * 55 * @param Schema $schema The schema valid at $ts 56 * @param string|int $pid 57 * @param int $ts Time at which the data should be read or written, 0 for now 58 * @param int $rid Row id: 0 for pages, autoincremented for other types 59 */ 60 public function __construct(Schema $schema, $pid, $ts = 0, $rid = 0) { 61 /** @var \helper_plugin_struct_db $helper */ 62 $helper = plugin_load('helper', 'struct_db'); 63 $this->sqlite = $helper->getDB(); 64 65 if(!$schema->getId()) { 66 throw new StructException('Schema does not exist. Only data of existing schemas can be accessed'); 67 } 68 69 $this->schema = $schema; 70 $this->pid = $pid; 71 $this->rid = $rid; 72 $this->setTimestamp($ts); 73 foreach($this->schema->getColumns() as $col) { 74 $this->labels[$col->getColref()] = $col->getType()->getLabel(); 75 } 76 } 77 78 /** 79 * gives access to the schema 80 * 81 * @return Schema 82 */ 83 public function getSchema() { 84 return $this->schema; 85 } 86 87 /** 88 * The current pid 89 * 90 * @return int|string 91 */ 92 public function getPid() { 93 return $this->pid; 94 } 95 96 /** 97 * The current rid 98 * 99 * @return int|string 100 */ 101 public function getRid() { 102 return $this->rid; 103 } 104 105 /** 106 * Should remove the current data, by either deleting or ovewriting it 107 * 108 * @return bool if the delete succeeded 109 */ 110 abstract public function clearData(); 111 112 /** 113 * Save the data to the database. 114 * 115 * We differentiate between single-value-column and multi-value-column by the value to the respective column-name, 116 * i.e. depending on if that is a string or an array, respectively. 117 * 118 * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields 119 * @return bool success of saving the data to the database 120 */ 121 abstract public function saveData($data); 122 123 /** 124 * Should empty or invisible (inpage) fields be returned? 125 * 126 * Defaults to false 127 * 128 * @param null|bool $set new value, null to read only 129 * @return bool current value (after set) 130 */ 131 public function optionSkipEmpty($set = null) { 132 if(!is_null($set)) { 133 $this->opt_skipempty = $set; 134 } 135 return $this->opt_skipempty; 136 } 137 138 /** 139 * Get the value of a single column 140 * 141 * @param Column $column 142 * @return Value|null 143 */ 144 public function getDataColumn($column) { 145 $data = $this->getData(); 146 foreach($data as $value) { 147 if($value->getColumn() == $column) { 148 return $value; 149 } 150 } 151 return null; 152 } 153 154 /** 155 * returns the data saved for the page 156 * 157 * @return Value[] a list of values saved for the current page 158 */ 159 public function getData() { 160 $data = $this->getDataFromDB(); 161 $data = $this->consolidateData($data, false); 162 return $data; 163 } 164 165 /** 166 * returns the data saved for the page as associative array 167 * 168 * The array returned is in the same format as used in @see saveData() 169 * 170 * It always returns raw Values! 171 * 172 * @return array 173 */ 174 public function getDataArray() { 175 $data = $this->getDataFromDB(); 176 $data = $this->consolidateData($data, true); 177 return $data; 178 } 179 180 /** 181 * Return the data in pseudo syntax 182 */ 183 public function getDataPseudoSyntax() { 184 $result = ''; 185 $data = $this->getData(); 186 187 foreach($data as $value) { 188 $key = $value->getColumn()->getFullQualifiedLabel(); 189 $value = $value->getDisplayValue(); 190 if(is_array($value)) $value = join(', ', $value); 191 $result .= sprintf("% -20s : %s\n", $key, $value); 192 } 193 return $result; 194 } 195 196 /** 197 * retrieve the data saved for the page from the database. Usually there is no need to call this function. 198 * Call @see SchemaData::getData instead. 199 */ 200 protected function getDataFromDB() { 201 list($sql, $opt) = $this->buildGetDataSQL(); 202 203 $res = $this->sqlite->query($sql, $opt); 204 $data = $this->sqlite->res2arr($res); 205 $this->sqlite->res_close($res); 206 return $data; 207 } 208 209 /** 210 * Creates a proper result array from the database data 211 * 212 * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB 213 * @param bool $asarray return data as associative array (true) or as array of Values (false) 214 * @return array|Value[] 215 */ 216 protected function consolidateData($DBdata, $asarray = false) { 217 $data = array(); 218 219 $sep = Search::CONCAT_SEPARATOR; 220 221 foreach($this->schema->getColumns(false) as $col) { 222 223 // if no data saved yet, return empty strings 224 if($DBdata) { 225 $val = $DBdata[0]['out' . $col->getColref()]; 226 } else { 227 $val = ''; 228 } 229 230 // multi val data is concatenated 231 if($col->isMulti()) { 232 $val = explode($sep, $val); 233 $val = array_filter($val); 234 } 235 236 $value = new Value($col, $val); 237 238 if($this->opt_skipempty && $value->isEmpty()) continue; 239 if($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption? 240 241 // for arrays, we return the raw value only 242 if($asarray) { 243 $data[$col->getLabel()] = $value->getRawValue(); 244 } else { 245 $data[$col->getLabel()] = $value; 246 } 247 } 248 249 return $data; 250 } 251 252 /** 253 * Builds the SQL statement to select the data for this page and schema 254 * 255 * @return array Two fields: the SQL string and the parameters array 256 */ 257 protected function buildGetDataSQL() { 258 $sep = Search::CONCAT_SEPARATOR; 259 $stable = 'data_' . $this->schema->getTable(); 260 $mtable = 'multi_' . $this->schema->getTable(); 261 262 $QB = new QueryBuilder(); 263 $QB->addTable($stable, 'DATA'); 264 $QB->addSelectColumn('DATA', 'pid', 'PID'); 265 $QB->addGroupByStatement('DATA.pid'); 266 267 foreach($this->schema->getColumns(false) as $col) { 268 269 $colref = $col->getColref(); 270 $colname = 'col' . $colref; 271 $outname = 'out' . $colref; 272 273 if($col->getType()->isMulti()) { 274 $tn = 'M' . $colref; 275 $QB->addLeftJoin( 276 'DATA', 277 $mtable, 278 $tn, 279 "DATA.pid = $tn.pid AND DATA.rev = $tn.rev AND $tn.colref = $colref" 280 ); 281 $col->getType()->select($QB, $tn, 'value', $outname); 282 $sel = $QB->getSelectStatement($outname); 283 $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname); 284 } else { 285 $col->getType()->select($QB, 'DATA', $colname, $outname); 286 $QB->addGroupByStatement($outname); 287 } 288 } 289 290 $pl = $QB->addValue($this->pid); 291 $QB->filters()->whereAnd("DATA.pid = $pl"); 292 $pl = $QB->addValue($this->getLastRevisionTimestamp()); 293 $QB->filters()->whereAnd("DATA.rev = $pl"); 294 295 return $QB->getSQL(); 296 } 297 298 /** 299 * @param int $ts 300 */ 301 public function setTimestamp($ts) { 302 if($ts && $ts < $this->schema->getTimeStamp()) { 303 throw new StructException('Given timestamp is not valid for current Schema'); 304 } 305 306 $this->ts = $ts; 307 } 308 309 /** 310 * Return the last time an edit happened for this table for the currently set 311 * time and pid. When the current timestamp is 0, the newest revision is 312 * returned. Used in @see buildGetDataSQL() 313 * 314 * @return int 315 */ 316 abstract protected function getLastRevisionTimestamp(); 317 318 /** 319 * Check if the given data validates against the current types. 320 * 321 * @param array $data 322 * @return AccessDataValidator 323 */ 324 public function getValidator($data) { 325 return new AccessDataValidator($this, $data); 326 } 327} 328 329 330