1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5abstract class AccessTable { 6 7 /** @var Schema */ 8 protected $schema; 9 protected $pid; 10 protected $labels = array(); 11 protected $ts = 0; 12 /** @var \helper_plugin_sqlite */ 13 protected $sqlite; 14 15 // options on how to retieve data 16 protected $opt_skipempty = false; 17 protected $opt_rawvalue = 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 * @return AccessTableData|AccessTableLookup 25 */ 26 public static function bySchema(Schema $schema, $pid) { 27 if($schema->isLookup()) { 28 return new AccessTableLookup($schema, $pid); 29 } else { 30 return new AccessTableData($schema, $pid); 31 } 32 } 33 34 /** 35 * Factory Method to access a data or lookup table 36 * 37 * @param string $tablename schema to load 38 * @param string|int $pid Page or row id to access 39 * @param int $ts from when is the schema to access? 40 * @return AccessTableData|AccessTableLookup 41 */ 42 public static function byTableName($tablename, $pid, $ts = 0) { 43 $schema = new Schema($tablename, $ts); 44 return self::bySchema($schema, $pid); 45 } 46 47 /** 48 * AccessTable constructor 49 * 50 * @param Schema $schema 51 * @param string $pid 52 */ 53 public function __construct(Schema $schema, $pid) { 54 /** @var \helper_plugin_struct_db $helper */ 55 $helper = plugin_load('helper', 'struct_db'); 56 $this->sqlite = $helper->getDB(); 57 if(!$this->sqlite) { 58 throw new StructException('Sqlite plugin required'); 59 } 60 61 if(!$schema->getId()) { 62 throw new StructException('Schema does not exist. Only data of existing schemas can be accessed'); 63 } 64 65 $this->schema = $schema; 66 $this->pid = $pid; 67 $this->ts = $this->schema->getTimeStamp(); 68 foreach($this->schema->getColumns() as $col) { 69 $this->labels[$col->getColref()] = $col->getType()->getLabel(); 70 } 71 } 72 73 /** 74 * gives access to the schema 75 * 76 * @return Schema 77 */ 78 public function getSchema() { 79 return $this->schema; 80 } 81 82 /** 83 * Should remove the current data, by either deleting or ovewriting it 84 * 85 * @return bool if the delete succeeded 86 */ 87 abstract public function clearData(); 88 89 /** 90 * Save the data to the database. 91 * 92 * We differentiate between single-value-column and multi-value-column by the value to the respective column-name, 93 * i.e. depending on if that is a string or an array, respectively. 94 * 95 * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields 96 * @return bool success of saving the data to the database 97 */ 98 abstract public function saveData($data); 99 100 /** 101 * Should empty or invisible (inpage) fields be returned? 102 * 103 * Defaults to false 104 * 105 * @param null|bool $set new value, null to read only 106 * @return bool current value (after set) 107 */ 108 public function optionSkipEmpty($set = null) { 109 if(!is_null($set)) { 110 $this->opt_skipempty = $set; 111 } 112 return $this->opt_skipempty; 113 } 114 115 /** 116 * Should the values be returned raw or are complex returns okay? 117 * 118 * Defaults to false = complex values okay 119 * 120 * @param null|bool $set new value, null to read only 121 * @return bool current value (after set) 122 */ 123 public function optionRawValue($set = null) { 124 if(!is_null($set)) { 125 $this->opt_rawvalue = $set; 126 } 127 return $this->opt_rawvalue; 128 } 129 130 131 /** 132 * Get the value of a single column 133 * 134 * @param Column $column 135 * @return Value|null 136 */ 137 public function getDataColumn($column) { 138 $data = $this->getData(); 139 foreach($data as $value) { 140 if($value->getColumn() == $column) { 141 return $value; 142 } 143 } 144 return null; 145 } 146 147 /** 148 * returns the data saved for the page 149 * 150 * @return Value[] a list of values saved for the current page 151 */ 152 public function getData() { 153 $this->setCorrectTimestamp($this->pid, $this->ts); 154 $data = $this->getDataFromDB(); 155 $data = $this->consolidateData($data, false); 156 return $data; 157 } 158 159 /** 160 * returns the data saved for the page as associative array 161 * 162 * The array returned is in the same format as used in @see saveData() 163 * 164 * @return array 165 */ 166 public function getDataArray() { 167 $this->setCorrectTimestamp($this->pid, $this->ts); 168 $data = $this->getDataFromDB(); 169 $data = $this->consolidateData($data, true); 170 return $data; 171 } 172 173 /** 174 * Return the data in pseudo syntax 175 */ 176 public function getDataPseudoSyntax() { 177 $result = ''; 178 $data = $this->getDataArray(); 179 foreach($data as $key => $value) { 180 $key = $this->schema->getTable() . ".$key"; 181 if(is_array($value)) $value = join(', ', $value); 182 $result .= sprintf("% -20s : %s\n", $key, $value); 183 } 184 return $result; 185 } 186 187 /** 188 * retrieve the data saved for the page from the database. Usually there is no need to call this function. 189 * Call @see SchemaData::getData instead. 190 */ 191 protected function getDataFromDB() { 192 list($sql, $opt) = $this->buildGetDataSQL(); 193 194 $res = $this->sqlite->query($sql, $opt); 195 $data = $this->sqlite->res2arr($res); 196 197 return $data; 198 } 199 200 /** 201 * Creates a proper result array from the database data 202 * 203 * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB 204 * @param bool $asarray return data as associative array (true) or as array of Values (false) 205 * @return array|Value[] 206 */ 207 protected function consolidateData($DBdata, $asarray = false) { 208 $data = array(); 209 210 $sep = Search::CONCAT_SEPARATOR; 211 212 foreach($this->schema->getColumns(false) as $col) { 213 214 // if no data saved, yet return empty strings 215 if($DBdata) { 216 $val = $DBdata[0]['col'.$col->getColref()]; 217 } else { 218 $val = ''; 219 } 220 221 // multi val data is concatenated 222 if($col->isMulti()) { 223 $val = explode($sep, $val); 224 if($this->opt_rawvalue) { 225 $val = array_map( 226 function ($val) use ($col) { // FIXME requires PHP 5.4+ 227 return $col->getType()->rawValue($val); 228 }, 229 $val 230 ); 231 } 232 $val = array_filter($val); 233 } else { 234 if($this->opt_rawvalue) { 235 $val = $col->getType()->rawValue($val); 236 } 237 } 238 239 if($this->opt_skipempty && ($val === '' || $val == array())) continue; 240 if($this->opt_skipempty && !$col->isVisibleInPage()) continue; 241 242 if($asarray) { 243 $data[$col->getLabel()] = $val; 244 } else { 245 $data[] = new Value($col, $val); 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 272 if($col->getType()->isMulti()) { 273 $tn = 'M' . $colref; 274 $QB->addLeftJoin( 275 'DATA', 276 $mtable, 277 $tn, 278 "DATA.pid = $tn.pid AND DATA.rev = $tn.rev AND $tn.colref = $colref" 279 ); 280 $col->getType()->select($QB, $tn, 'value', $colname); 281 $sel = $QB->getSelectStatement($colname); 282 $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $colname); 283 } else { 284 $col->getType()->select($QB, 'DATA', $colname, $colname); 285 $QB->addGroupByStatement($colname); 286 } 287 } 288 289 $pl = $QB->addValue($this->pid); 290 $QB->filters()->whereAnd("DATA.pid = $pl"); 291 $pl = $QB->addValue($this->ts); 292 $QB->filters()->whereAnd("DATA.rev = $pl"); 293 294 return $QB->getSQL(); 295 } 296 297 /** 298 * @param int $ts 299 */ 300 public function setTimestamp($ts) { 301 $this->ts = $ts; 302 } 303 304 305 /** 306 * Set $this->ts to an existing timestamp, which is either current timestamp if it exists 307 * or the next oldest timestamp that exists. If not timestamp is provided it is the newest timestamp that exists. 308 * 309 * @param $page 310 * @param int|null $ts 311 * @fixme clear up description 312 */ 313 abstract protected function setCorrectTimestamp($page, $ts = null); 314} 315 316 317