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