xref: /plugin/struct/meta/AccessTable.php (revision 13eddb0f545f8f41b29b77f59e797a50081d9821)
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     * @return AccessTableData|AccessTableLookup
25     */
26    public static function bySchema(Schema $schema, $pid) {
27        if($schema->isLookup()) {
28            return new AccessTableLookup($schema, $pid);
29        } else {
30            return new AccessTableData($schema, $pid);
31        }
32    }
33
34    /**
35     * Factory Method to access a data or lookup table
36     *
37     * @param string $tablename schema to load
38     * @param string|int $pid Page or row id to access
39     * @param int $ts from when is the schema to access?
40     * @return AccessTableData|AccessTableLookup
41     */
42    public static function byTableName($tablename, $pid, $ts = 0) {
43        $schema = new Schema($tablename, $ts);
44        return self::bySchema($schema, $pid);
45    }
46
47    /**
48     * AccessTable constructor
49     *
50     * @param Schema $schema
51     * @param string $pid
52     */
53    public function __construct(Schema $schema, $pid) {
54        /** @var \helper_plugin_struct_db $helper */
55        $helper = plugin_load('helper', 'struct_db');
56        $this->sqlite = $helper->getDB();
57        if(!$this->sqlite) {
58            throw new StructException('Sqlite plugin required');
59        }
60
61        if(!$schema->getId()) {
62            throw new StructException('Schema does not exist. Only data of existing schemas can be accessed');
63        }
64
65        $this->schema = $schema;
66        $this->pid = $pid;
67        $this->ts = $this->schema->getTimeStamp();
68        foreach($this->schema->getColumns() as $col) {
69            $this->labels[$col->getColref()] = $col->getType()->getLabel();
70        }
71    }
72
73    /**
74     * gives access to the schema
75     *
76     * @return Schema
77     */
78    public function getSchema() {
79        return $this->schema;
80    }
81
82    /**
83     * Should remove the current data, by either deleting or ovewriting it
84     *
85     * @return bool if the delete succeeded
86     */
87    abstract public function clearData();
88
89    /**
90     * Save the data to the database.
91     *
92     * We differentiate between single-value-column and multi-value-column by the value to the respective column-name,
93     * i.e. depending on if that is a string or an array, respectively.
94     *
95     * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields
96     * @return bool success of saving the data to the database
97     */
98    abstract public function saveData($data);
99
100    /**
101     * Should empty or invisible (inpage) fields be returned?
102     *
103     * Defaults to false
104     *
105     * @param null|bool $set new value, null to read only
106     * @return bool current value (after set)
107     */
108    public function optionSkipEmpty($set = null) {
109        if(!is_null($set)) {
110            $this->opt_skipempty = $set;
111        }
112        return $this->opt_skipempty;
113    }
114
115    /**
116     * Should the values be returned raw or are complex returns okay?
117     *
118     * Defaults to false = complex values okay
119     *
120     * @param null|bool $set new value, null to read only
121     * @return bool current value (after set)
122     */
123    public function optionRawValue($set = null) {
124        if(!is_null($set)) {
125            $this->opt_rawvalue = $set;
126        }
127        return $this->opt_rawvalue;
128    }
129
130
131    /**
132     * Get the value of a single column
133     *
134     * @param Column $column
135     * @return Value|null
136     */
137    public function getDataColumn($column) {
138        $data = $this->getData();
139        foreach($data as $value) {
140            if($value->getColumn() == $column) {
141                return $value;
142            }
143        }
144        return null;
145    }
146
147    /**
148     * returns the data saved for the page
149     *
150     * @return Value[] a list of values saved for the current page
151     */
152    public function getData() {
153        $this->setCorrectTimestamp($this->pid, $this->ts);
154        $data = $this->getDataFromDB();
155        $data = $this->consolidateData($data, false);
156        return $data;
157    }
158
159    /**
160     * returns the data saved for the page as associative array
161     *
162     * The array returned is in the same format as used in @see saveData()
163     *
164     * @return array
165     */
166    public function getDataArray() {
167        $this->setCorrectTimestamp($this->pid, $this->ts);
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->ts);
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        $this->ts = $ts;
302    }
303
304
305    /**
306     * Set $this->ts to an existing timestamp, which is either current timestamp if it exists
307     * or the next oldest timestamp that exists. If not timestamp is provided it is the newest timestamp that exists.
308     *
309     * @param          $page
310     * @param int|null $ts
311     * @fixme clear up description
312     */
313    abstract protected function setCorrectTimestamp($page, $ts = null);
314}
315
316
317