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