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