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