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