xref: /plugin/struct/meta/AccessTable.php (revision e12908f1f620671d1d3f9e31afbd164ebc82a6b0)
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        $data = $this->getDataFromDB();
156        $data = $this->consolidateData($data, false);
157        return $data;
158    }
159
160    /**
161     * returns the data saved for the page as associative array
162     *
163     * The array returned is in the same format as used in @see saveData()
164     *
165     * @return array
166     */
167    public function getDataArray() {
168        $data = $this->getDataFromDB();
169        $data = $this->consolidateData($data, true);
170        return $data;
171    }
172
173    /**
174     * Return the data in pseudo syntax
175     */
176    public function getDataPseudoSyntax() {
177        $result = '';
178        $data = $this->getDataArray();
179        foreach($data as $key => $value) {
180            $key = $this->schema->getTable() . ".$key";
181            if(is_array($value)) $value = join(', ', $value);
182            $result .= sprintf("% -20s : %s\n", $key, $value);
183        }
184        return $result;
185    }
186
187    /**
188     * retrieve the data saved for the page from the database. Usually there is no need to call this function.
189     * Call @see SchemaData::getData instead.
190     */
191    protected function getDataFromDB() {
192        list($sql, $opt) = $this->buildGetDataSQL();
193
194        $res = $this->sqlite->query($sql, $opt);
195        $data = $this->sqlite->res2arr($res);
196
197        return $data;
198    }
199
200    /**
201     * Creates a proper result array from the database data
202     *
203     * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB
204     * @param bool $asarray return data as associative array (true) or as array of Values (false)
205     * @return array|Value[]
206     */
207    protected function consolidateData($DBdata, $asarray = false) {
208        $data = array();
209
210        $sep = Search::CONCAT_SEPARATOR;
211
212        foreach($this->schema->getColumns(false) as $col) {
213
214            // if no data saved, yet return empty strings
215            if($DBdata) {
216                $val = $DBdata[0]['col'.$col->getColref()];
217            } else {
218                $val = '';
219            }
220
221            // multi val data is concatenated
222            if($col->isMulti()) {
223                $val = explode($sep, $val);
224                if($this->opt_rawvalue) {
225                    $val = array_map(
226                        function ($val) use ($col) { // FIXME requires PHP 5.4+
227                            return $col->getType()->rawValue($val);
228                        },
229                        $val
230                    );
231                }
232                $val = array_filter($val);
233            } else {
234                if($this->opt_rawvalue) {
235                    $val = $col->getType()->rawValue($val);
236                }
237            }
238
239            if($this->opt_skipempty && ($val === '' || $val == array())) continue;
240            if($this->opt_skipempty && !$col->isVisibleInPage()) continue;
241
242            if($asarray) {
243                $data[$col->getLabel()] = $val;
244            } else {
245                $data[] = new Value($col, $val);
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() {
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', 'pid', 'PID');
265        $QB->addGroupByStatement('DATA.pid');
266
267        foreach($this->schema->getColumns(false) as $col) {
268
269            $colref = $col->getColref();
270            $colname = 'col'.$colref;
271
272            if($col->getType()->isMulti()) {
273                $tn = 'M' . $colref;
274                $QB->addLeftJoin(
275                    'DATA',
276                    $mtable,
277                    $tn,
278                    "DATA.pid = $tn.pid AND DATA.rev = $tn.rev AND $tn.colref = $colref"
279                );
280                $col->getType()->select($QB, $tn, 'value', $colname);
281                $sel = $QB->getSelectStatement($colname);
282                $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $colname);
283            } else {
284                $col->getType()->select($QB, 'DATA', $colname, $colname);
285                $QB->addGroupByStatement($colname);
286            }
287        }
288
289        $pl = $QB->addValue($this->pid);
290        $QB->filters()->whereAnd("DATA.pid = $pl");
291        $pl = $QB->addValue($this->getLastRevisionTimestamp());
292        $QB->filters()->whereAnd("DATA.rev = $pl");
293
294        return $QB->getSQL();
295    }
296
297    /**
298     * @param int $ts
299     */
300    public function setTimestamp($ts) {
301        if($ts && $ts < $this->schema->getTimeStamp()) {
302            throw new StructException('Given timestamp is not valid for current Schema');
303        }
304
305        $this->ts = $ts;
306    }
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
320