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