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