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