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