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