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