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