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