xref: /plugin/struct/meta/AccessTable.php (revision 6e54daafbe3e4c08bcecd34c03e56fcd7266745d)
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->getData();
170
171        foreach($data as $value) {
172            $key = $value->getColumn()->getFullQualifiedLabel();
173            $value = $value->getDisplayValue();
174            if(is_array($value)) $value = join(', ', $value);
175            $result .= sprintf("% -20s : %s\n", $key, $value);
176        }
177        return $result;
178    }
179
180    /**
181     * retrieve the data saved for the page from the database. Usually there is no need to call this function.
182     * Call @see SchemaData::getData instead.
183     */
184    protected function getDataFromDB() {
185        list($sql, $opt) = $this->buildGetDataSQL();
186
187        $res = $this->sqlite->query($sql, $opt);
188        $data = $this->sqlite->res2arr($res);
189        $this->sqlite->res_close($res);
190        return $data;
191    }
192
193    /**
194     * Creates a proper result array from the database data
195     *
196     * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB
197     * @param bool $asarray return data as associative array (true) or as array of Values (false)
198     * @return array|Value[]
199     */
200    protected function consolidateData($DBdata, $asarray = false) {
201        $data = array();
202
203        $sep = Search::CONCAT_SEPARATOR;
204
205        foreach($this->schema->getColumns(false) as $col) {
206
207            // if no data saved yet, return empty strings
208            if($DBdata) {
209                $val = $DBdata[0]['out' . $col->getColref()];
210            } else {
211                $val = '';
212            }
213
214            // multi val data is concatenated
215            if($col->isMulti()) {
216                $val = explode($sep, $val);
217                $val = array_filter($val);
218            }
219
220            $value = new Value($col, $val);
221
222            if($this->opt_skipempty && $value->isEmpty()) continue;
223            if($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption?
224
225            // for arrays, we return the raw value only
226            if($asarray) {
227                $data[$col->getLabel()] = $value->getRawValue();
228            } else {
229                $data[$col->getLabel()] = $value;
230            }
231        }
232
233        return $data;
234    }
235
236    /**
237     * Builds the SQL statement to select the data for this page and schema
238     *
239     * @return array Two fields: the SQL string and the parameters array
240     */
241    protected function buildGetDataSQL() {
242        $sep = Search::CONCAT_SEPARATOR;
243        $stable = 'data_' . $this->schema->getTable();
244        $mtable = 'multi_' . $this->schema->getTable();
245
246        $QB = new QueryBuilder();
247        $QB->addTable($stable, 'DATA');
248        $QB->addSelectColumn('DATA', 'pid', 'PID');
249        $QB->addGroupByStatement('DATA.pid');
250
251        foreach($this->schema->getColumns(false) as $col) {
252
253            $colref = $col->getColref();
254            $colname = 'col' . $colref;
255            $outname = 'out' . $colref;
256
257            if($col->getType()->isMulti()) {
258                $tn = 'M' . $colref;
259                $QB->addLeftJoin(
260                    'DATA',
261                    $mtable,
262                    $tn,
263                    "DATA.pid = $tn.pid AND DATA.rev = $tn.rev AND $tn.colref = $colref"
264                );
265                $col->getType()->select($QB, $tn, 'value', $outname);
266                $sel = $QB->getSelectStatement($outname);
267                $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname);
268            } else {
269                $col->getType()->select($QB, 'DATA', $colname, $outname);
270                $QB->addGroupByStatement($outname);
271            }
272        }
273
274        $pl = $QB->addValue($this->pid);
275        $QB->filters()->whereAnd("DATA.pid = $pl");
276        $pl = $QB->addValue($this->getLastRevisionTimestamp());
277        $QB->filters()->whereAnd("DATA.rev = $pl");
278
279        return $QB->getSQL();
280    }
281
282    /**
283     * @param int $ts
284     */
285    public function setTimestamp($ts) {
286        if($ts && $ts < $this->schema->getTimeStamp()) {
287            throw new StructException('Given timestamp is not valid for current Schema');
288        }
289
290        $this->ts = $ts;
291    }
292
293    /**
294     * Return the last time an edit happened for this table for the currently set
295     * time and pid. When the current timestamp is 0, the newest revision is
296     * returned. Used in @see buildGetDataSQL()
297     *
298     * @return int
299     */
300    abstract protected function getLastRevisionTimestamp();
301
302    /**
303     * Check if the given data validates against the current types.
304     *
305     * @param array $data
306     * @return AccessDataValidator
307     */
308    public function getValidator($data) {
309        return new AccessDataValidator($this, $data);
310    }
311}
312
313
314