xref: /plugin/struct/meta/AccessTable.php (revision efe74305e33c04e0a85fabebfa21d58c1c78c3dd)
1f411d872SAndreas Gohr<?php
2f411d872SAndreas Gohr
3f411d872SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
4f411d872SAndreas Gohr
5c73fba38SAnna Dabrowska/**
6c73fba38SAnna Dabrowska * Class AccessTable
7c73fba38SAnna Dabrowska *
8c73fba38SAnna Dabrowska * Base class for data accessors
9c73fba38SAnna Dabrowska *
10c73fba38SAnna Dabrowska * @package dokuwiki\plugin\struct\meta
11c73fba38SAnna Dabrowska */
12f411d872SAndreas Gohrabstract class AccessTable {
13f411d872SAndreas Gohr
14*efe74305SAnna Dabrowska    const DEFAULT_REV = 0;
15*efe74305SAnna Dabrowska    const DEFAULT_LATEST = 1;
16*efe74305SAnna Dabrowska
17f411d872SAndreas Gohr    /** @var  Schema */
18f411d872SAndreas Gohr    protected $schema;
19f411d872SAndreas Gohr    protected $pid;
200ceefd5cSAnna Dabrowska    protected $rid;
21f411d872SAndreas Gohr    protected $labels = array();
22f411d872SAndreas Gohr    protected $ts     = 0;
23f411d872SAndreas Gohr    /** @var \helper_plugin_sqlite */
24f411d872SAndreas Gohr    protected $sqlite;
25f411d872SAndreas Gohr
2690421550SAndreas Gohr    // options on how to retrieve data
27f411d872SAndreas Gohr    protected $opt_skipempty = false;
28f411d872SAndreas Gohr
29f411d872SAndreas Gohr    /**
30b9d35ff2SAnna Dabrowska     * Factory method returning the appropriate data accessor (page, lookup or serial)
31f411d872SAndreas Gohr     *
32f411d872SAndreas Gohr     * @param Schema $schema schema to load
3386a40c1eSAnna Dabrowska     * @param string $pid Page id to access
34897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
3586a40c1eSAnna Dabrowska     * @param int $rid Row id, 0 for page type data, otherwise autoincrement
36b9d35ff2SAnna Dabrowska     * @return AccessTableData|AccessTableLookup|AccessTableSerial
37f411d872SAndreas Gohr     */
380ceefd5cSAnna Dabrowska    public static function bySchema(Schema $schema, $pid, $ts = 0, $rid = 0) {
39c73fba38SAnna Dabrowska        if (self::isTypeLookup($pid, $ts, $rid)) {
400ceefd5cSAnna Dabrowska            return new AccessTableLookup($schema, $pid, $ts, $rid);
41f411d872SAndreas Gohr        }
42c73fba38SAnna Dabrowska        if (self::isTypeSerial($pid, $ts, $rid)) {
43b9d35ff2SAnna Dabrowska            return new AccessTableSerial($schema, $pid, $ts, $rid);
44b9d35ff2SAnna Dabrowska        }
450ceefd5cSAnna Dabrowska        return new AccessTableData($schema, $pid, $ts, $rid);
46f411d872SAndreas Gohr    }
47f411d872SAndreas Gohr
48f411d872SAndreas Gohr    /**
49c73fba38SAnna Dabrowska     * Factory Method to access data
50f411d872SAndreas Gohr     *
51f411d872SAndreas Gohr     * @param string $tablename schema to load
5286a40c1eSAnna Dabrowska     * @param string $pid Page id to access
53897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
5486a40c1eSAnna Dabrowska     * @param int $rid Row id, 0 for page type data, otherwise autoincrement
55c73fba38SAnna Dabrowska     * @return AccessTableData|AccessTableLookup|AccessTableSerial
56f411d872SAndreas Gohr     */
570ceefd5cSAnna Dabrowska    public static function byTableName($tablename, $pid, $ts = 0, $rid = 0) {
58f411d872SAndreas Gohr        $schema = new Schema($tablename, $ts);
590ceefd5cSAnna Dabrowska        return self::bySchema($schema, $pid, $ts, $rid);
60f411d872SAndreas Gohr    }
61f411d872SAndreas Gohr
62f411d872SAndreas Gohr    /**
63f411d872SAndreas Gohr     * AccessTable constructor
64f411d872SAndreas Gohr     *
65897aef42SAndreas Gohr     * @param Schema $schema The schema valid at $ts
6686a40c1eSAnna Dabrowska     * @param string $pid Page id
67897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
680ceefd5cSAnna Dabrowska     * @param int $rid Row id: 0 for pages, autoincremented for other types
69f411d872SAndreas Gohr     */
700ceefd5cSAnna Dabrowska    public function __construct(Schema $schema, $pid, $ts = 0, $rid = 0) {
71f411d872SAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
72f411d872SAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
73f411d872SAndreas Gohr        $this->sqlite = $helper->getDB();
74f411d872SAndreas Gohr
75f411d872SAndreas Gohr        if(!$schema->getId()) {
76f411d872SAndreas Gohr            throw new StructException('Schema does not exist. Only data of existing schemas can be accessed');
77f411d872SAndreas Gohr        }
78f411d872SAndreas Gohr
79f411d872SAndreas Gohr        $this->schema = $schema;
80f411d872SAndreas Gohr        $this->pid = $pid;
810ceefd5cSAnna Dabrowska        $this->rid = $rid;
82897aef42SAndreas Gohr        $this->setTimestamp($ts);
83f411d872SAndreas Gohr        foreach($this->schema->getColumns() as $col) {
84f411d872SAndreas Gohr            $this->labels[$col->getColref()] = $col->getType()->getLabel();
85f411d872SAndreas Gohr        }
86f411d872SAndreas Gohr    }
87f411d872SAndreas Gohr
88f411d872SAndreas Gohr    /**
89f411d872SAndreas Gohr     * gives access to the schema
90f411d872SAndreas Gohr     *
91f411d872SAndreas Gohr     * @return Schema
92f411d872SAndreas Gohr     */
93f411d872SAndreas Gohr    public function getSchema() {
94f411d872SAndreas Gohr        return $this->schema;
95f411d872SAndreas Gohr    }
96f411d872SAndreas Gohr
97f411d872SAndreas Gohr    /**
98f107f479SAndreas Gohr     * The current pid
99f107f479SAndreas Gohr     *
10086a40c1eSAnna Dabrowska     * @return string
101f107f479SAndreas Gohr     */
102f107f479SAndreas Gohr    public function getPid() {
103f107f479SAndreas Gohr        return $this->pid;
104f107f479SAndreas Gohr    }
105f107f479SAndreas Gohr
106f107f479SAndreas Gohr    /**
1070ceefd5cSAnna Dabrowska     * The current rid
1080ceefd5cSAnna Dabrowska     *
10986a40c1eSAnna Dabrowska     * @return int
1100ceefd5cSAnna Dabrowska     */
1110ceefd5cSAnna Dabrowska    public function getRid() {
1120ceefd5cSAnna Dabrowska        return $this->rid;
1130ceefd5cSAnna Dabrowska    }
1140ceefd5cSAnna Dabrowska
1150ceefd5cSAnna Dabrowska    /**
116f411d872SAndreas Gohr     * Should remove the current data, by either deleting or ovewriting it
117f411d872SAndreas Gohr     *
118f411d872SAndreas Gohr     * @return bool if the delete succeeded
119f411d872SAndreas Gohr     */
120f411d872SAndreas Gohr    abstract public function clearData();
121f411d872SAndreas Gohr
122f411d872SAndreas Gohr    /**
123f411d872SAndreas Gohr     * Save the data to the database.
124f411d872SAndreas Gohr     *
125f411d872SAndreas Gohr     * We differentiate between single-value-column and multi-value-column by the value to the respective column-name,
126f411d872SAndreas Gohr     * i.e. depending on if that is a string or an array, respectively.
127f411d872SAndreas Gohr     *
128f411d872SAndreas Gohr     * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields
129f411d872SAndreas Gohr     * @return bool success of saving the data to the database
130f411d872SAndreas Gohr     */
131f411d872SAndreas Gohr    abstract public function saveData($data);
132f411d872SAndreas Gohr
133f411d872SAndreas Gohr    /**
134f411d872SAndreas Gohr     * Should empty or invisible (inpage) fields be returned?
135f411d872SAndreas Gohr     *
136f411d872SAndreas Gohr     * Defaults to false
137f411d872SAndreas Gohr     *
138f411d872SAndreas Gohr     * @param null|bool $set new value, null to read only
139f411d872SAndreas Gohr     * @return bool current value (after set)
140f411d872SAndreas Gohr     */
141f411d872SAndreas Gohr    public function optionSkipEmpty($set = null) {
142f411d872SAndreas Gohr        if(!is_null($set)) {
143f411d872SAndreas Gohr            $this->opt_skipempty = $set;
144f411d872SAndreas Gohr        }
145f411d872SAndreas Gohr        return $this->opt_skipempty;
146f411d872SAndreas Gohr    }
147f411d872SAndreas Gohr
148f411d872SAndreas Gohr    /**
149f411d872SAndreas Gohr     * Get the value of a single column
150f411d872SAndreas Gohr     *
151f411d872SAndreas Gohr     * @param Column $column
152f411d872SAndreas Gohr     * @return Value|null
153f411d872SAndreas Gohr     */
154f411d872SAndreas Gohr    public function getDataColumn($column) {
155f411d872SAndreas Gohr        $data = $this->getData();
156f411d872SAndreas Gohr        foreach($data as $value) {
157f411d872SAndreas Gohr            if($value->getColumn() == $column) {
158f411d872SAndreas Gohr                return $value;
159f411d872SAndreas Gohr            }
160f411d872SAndreas Gohr        }
161f411d872SAndreas Gohr        return null;
162f411d872SAndreas Gohr    }
163f411d872SAndreas Gohr
164f411d872SAndreas Gohr    /**
165f411d872SAndreas Gohr     * returns the data saved for the page
166f411d872SAndreas Gohr     *
167f411d872SAndreas Gohr     * @return Value[] a list of values saved for the current page
168f411d872SAndreas Gohr     */
169f411d872SAndreas Gohr    public function getData() {
170f411d872SAndreas Gohr        $data = $this->getDataFromDB();
171f411d872SAndreas Gohr        $data = $this->consolidateData($data, false);
172f411d872SAndreas Gohr        return $data;
173f411d872SAndreas Gohr    }
174f411d872SAndreas Gohr
175f411d872SAndreas Gohr    /**
176f411d872SAndreas Gohr     * returns the data saved for the page as associative array
177f411d872SAndreas Gohr     *
178f411d872SAndreas Gohr     * The array returned is in the same format as used in @see saveData()
179f411d872SAndreas Gohr     *
18090421550SAndreas Gohr     * It always returns raw Values!
18190421550SAndreas Gohr     *
182f411d872SAndreas Gohr     * @return array
183f411d872SAndreas Gohr     */
184f411d872SAndreas Gohr    public function getDataArray() {
185f411d872SAndreas Gohr        $data = $this->getDataFromDB();
186f411d872SAndreas Gohr        $data = $this->consolidateData($data, true);
187f411d872SAndreas Gohr        return $data;
188f411d872SAndreas Gohr    }
189f411d872SAndreas Gohr
190f411d872SAndreas Gohr    /**
191f411d872SAndreas Gohr     * Return the data in pseudo syntax
192f411d872SAndreas Gohr     */
193f411d872SAndreas Gohr    public function getDataPseudoSyntax() {
194f411d872SAndreas Gohr        $result = '';
195a0a1d14eSAndreas Gohr        $data = $this->getData();
196a0a1d14eSAndreas Gohr
197a0a1d14eSAndreas Gohr        foreach($data as $value) {
198a0a1d14eSAndreas Gohr            $key = $value->getColumn()->getFullQualifiedLabel();
199a0a1d14eSAndreas Gohr            $value = $value->getDisplayValue();
200f411d872SAndreas Gohr            if(is_array($value)) $value = join(', ', $value);
201f411d872SAndreas Gohr            $result .= sprintf("% -20s : %s\n", $key, $value);
202f411d872SAndreas Gohr        }
203f411d872SAndreas Gohr        return $result;
204f411d872SAndreas Gohr    }
205f411d872SAndreas Gohr
206f411d872SAndreas Gohr    /**
207f411d872SAndreas Gohr     * retrieve the data saved for the page from the database. Usually there is no need to call this function.
208f411d872SAndreas Gohr     * Call @see SchemaData::getData instead.
209f411d872SAndreas Gohr     */
210f411d872SAndreas Gohr    protected function getDataFromDB() {
211*efe74305SAnna Dabrowska        $idColumn = self::isTypePage($this->pid, $this->ts, $this->rid) ? 'pid' : 'rid';
212*efe74305SAnna Dabrowska        list($sql, $opt) = $this->buildGetDataSQL($idColumn);
213f411d872SAndreas Gohr
214f411d872SAndreas Gohr        $res = $this->sqlite->query($sql, $opt);
215f411d872SAndreas Gohr        $data = $this->sqlite->res2arr($res);
2169c00b26cSAndreas Gohr        $this->sqlite->res_close($res);
217f411d872SAndreas Gohr        return $data;
218f411d872SAndreas Gohr    }
219f411d872SAndreas Gohr
220f411d872SAndreas Gohr    /**
221f411d872SAndreas Gohr     * Creates a proper result array from the database data
222f411d872SAndreas Gohr     *
223f411d872SAndreas Gohr     * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB
224f411d872SAndreas Gohr     * @param bool $asarray return data as associative array (true) or as array of Values (false)
225f411d872SAndreas Gohr     * @return array|Value[]
226f411d872SAndreas Gohr     */
227f411d872SAndreas Gohr    protected function consolidateData($DBdata, $asarray = false) {
228f411d872SAndreas Gohr        $data = array();
229f411d872SAndreas Gohr
230f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
231f411d872SAndreas Gohr
232f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
233f411d872SAndreas Gohr
23490421550SAndreas Gohr            // if no data saved yet, return empty strings
235f411d872SAndreas Gohr            if($DBdata) {
236bab52340SAndreas Gohr                $val = $DBdata[0]['out' . $col->getColref()];
237f411d872SAndreas Gohr            } else {
238f411d872SAndreas Gohr                $val = '';
239f411d872SAndreas Gohr            }
240f411d872SAndreas Gohr
241f411d872SAndreas Gohr            // multi val data is concatenated
242f411d872SAndreas Gohr            if($col->isMulti()) {
243f411d872SAndreas Gohr                $val = explode($sep, $val);
244f411d872SAndreas Gohr                $val = array_filter($val);
245f411d872SAndreas Gohr            }
246f411d872SAndreas Gohr
24790421550SAndreas Gohr            $value = new Value($col, $val);
248f411d872SAndreas Gohr
24990421550SAndreas Gohr            if($this->opt_skipempty && $value->isEmpty()) continue;
25090421550SAndreas Gohr            if($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption?
25190421550SAndreas Gohr
25290421550SAndreas Gohr            // for arrays, we return the raw value only
253f411d872SAndreas Gohr            if($asarray) {
25490421550SAndreas Gohr                $data[$col->getLabel()] = $value->getRawValue();
255f411d872SAndreas Gohr            } else {
2566e54daafSMichael Große                $data[$col->getLabel()] = $value;
257f411d872SAndreas Gohr            }
258f411d872SAndreas Gohr        }
259f411d872SAndreas Gohr
260f411d872SAndreas Gohr        return $data;
261f411d872SAndreas Gohr    }
262f411d872SAndreas Gohr
263f411d872SAndreas Gohr    /**
264f411d872SAndreas Gohr     * Builds the SQL statement to select the data for this page and schema
265f411d872SAndreas Gohr     *
266f411d872SAndreas Gohr     * @return array Two fields: the SQL string and the parameters array
267f411d872SAndreas Gohr     */
2686fd73b4bSAnna Dabrowska    protected function buildGetDataSQL($idColumn = 'pid') {
269f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
270f411d872SAndreas Gohr        $stable = 'data_' . $this->schema->getTable();
271f411d872SAndreas Gohr        $mtable = 'multi_' . $this->schema->getTable();
272f411d872SAndreas Gohr
273f411d872SAndreas Gohr        $QB = new QueryBuilder();
274f411d872SAndreas Gohr        $QB->addTable($stable, 'DATA');
2756fd73b4bSAnna Dabrowska        $QB->addSelectColumn('DATA', $idColumn, strtoupper($idColumn));
2766fd73b4bSAnna Dabrowska        $QB->addGroupByStatement("DATA.$idColumn");
277f411d872SAndreas Gohr
278f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
279f411d872SAndreas Gohr
280f411d872SAndreas Gohr            $colref = $col->getColref();
281f411d872SAndreas Gohr            $colname = 'col' . $colref;
282bab52340SAndreas Gohr            $outname = 'out' . $colref;
283f411d872SAndreas Gohr
284f411d872SAndreas Gohr            if($col->getType()->isMulti()) {
285f411d872SAndreas Gohr                $tn = 'M' . $colref;
286f411d872SAndreas Gohr                $QB->addLeftJoin(
287f411d872SAndreas Gohr                    'DATA',
288f411d872SAndreas Gohr                    $mtable,
289f411d872SAndreas Gohr                    $tn,
2906fd73b4bSAnna Dabrowska                    "DATA.$idColumn = $tn.$idColumn AND DATA.rev = $tn.rev AND $tn.colref = $colref"
291f411d872SAndreas Gohr                );
292bab52340SAndreas Gohr                $col->getType()->select($QB, $tn, 'value', $outname);
293bab52340SAndreas Gohr                $sel = $QB->getSelectStatement($outname);
294bab52340SAndreas Gohr                $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname);
295f411d872SAndreas Gohr            } else {
296bab52340SAndreas Gohr                $col->getType()->select($QB, 'DATA', $colname, $outname);
297bab52340SAndreas Gohr                $QB->addGroupByStatement($outname);
298f411d872SAndreas Gohr            }
299f411d872SAndreas Gohr        }
300f411d872SAndreas Gohr
3016fd73b4bSAnna Dabrowska        $pl = $QB->addValue($this->{$idColumn});
3026fd73b4bSAnna Dabrowska        $QB->filters()->whereAnd("DATA.$idColumn = $pl");
303897aef42SAndreas Gohr        $pl = $QB->addValue($this->getLastRevisionTimestamp());
304f411d872SAndreas Gohr        $QB->filters()->whereAnd("DATA.rev = $pl");
305f411d872SAndreas Gohr
306f411d872SAndreas Gohr        return $QB->getSQL();
307f411d872SAndreas Gohr    }
308f411d872SAndreas Gohr
309f411d872SAndreas Gohr    /**
31013eddb0fSAndreas Gohr     * @param int $ts
31113eddb0fSAndreas Gohr     */
31213eddb0fSAndreas Gohr    public function setTimestamp($ts) {
313897aef42SAndreas Gohr        if($ts && $ts < $this->schema->getTimeStamp()) {
314897aef42SAndreas Gohr            throw new StructException('Given timestamp is not valid for current Schema');
315897aef42SAndreas Gohr        }
316897aef42SAndreas Gohr
31713eddb0fSAndreas Gohr        $this->ts = $ts;
31813eddb0fSAndreas Gohr    }
31913eddb0fSAndreas Gohr
32013eddb0fSAndreas Gohr    /**
32169f7ec8fSAnna Dabrowska     * Returns the timestamp from the current data
32269f7ec8fSAnna Dabrowska     * @return int
32369f7ec8fSAnna Dabrowska     */
32469f7ec8fSAnna Dabrowska    public function getTimestamp()
32569f7ec8fSAnna Dabrowska    {
32669f7ec8fSAnna Dabrowska        return $this->ts;
32769f7ec8fSAnna Dabrowska    }
32869f7ec8fSAnna Dabrowska
32969f7ec8fSAnna Dabrowska    /**
330897aef42SAndreas Gohr     * Return the last time an edit happened for this table for the currently set
331897aef42SAndreas Gohr     * time and pid. When the current timestamp is 0, the newest revision is
332897aef42SAndreas Gohr     * returned. Used in @see buildGetDataSQL()
333f411d872SAndreas Gohr     *
334897aef42SAndreas Gohr     * @return int
335f411d872SAndreas Gohr     */
336897aef42SAndreas Gohr    abstract protected function getLastRevisionTimestamp();
33787dc1344SAndreas Gohr
33887dc1344SAndreas Gohr    /**
33987dc1344SAndreas Gohr     * Check if the given data validates against the current types.
34087dc1344SAndreas Gohr     *
34187dc1344SAndreas Gohr     * @param array $data
34293ca6f4fSAndreas Gohr     * @return AccessDataValidator
34387dc1344SAndreas Gohr     */
34487dc1344SAndreas Gohr    public function getValidator($data) {
34593ca6f4fSAndreas Gohr        return new AccessDataValidator($this, $data);
34687dc1344SAndreas Gohr    }
347c73fba38SAnna Dabrowska
348c73fba38SAnna Dabrowska    /**
349c73fba38SAnna Dabrowska     * Returns true if data is of type "page"
350c73fba38SAnna Dabrowska     *
351c73fba38SAnna Dabrowska     * @param string $pid
352c73fba38SAnna Dabrowska     * @param int $rev
353c73fba38SAnna Dabrowska     * @param int $rid
354c73fba38SAnna Dabrowska     * @return bool
355c73fba38SAnna Dabrowska     */
356c73fba38SAnna Dabrowska    public static function isTypePage($pid, $rev, $rid)
357c73fba38SAnna Dabrowska    {
358c73fba38SAnna Dabrowska        return $rev > 0;
359c73fba38SAnna Dabrowska    }
360c73fba38SAnna Dabrowska
361c73fba38SAnna Dabrowska    /**
362c73fba38SAnna Dabrowska     * Returns true if data is of type "lookup"
363c73fba38SAnna Dabrowska     *
364c73fba38SAnna Dabrowska     * @param string $pid
365c73fba38SAnna Dabrowska     * @param int $rev
366c73fba38SAnna Dabrowska     * @param int $rid
367c73fba38SAnna Dabrowska     * @return bool
368c73fba38SAnna Dabrowska     */
369c73fba38SAnna Dabrowska    public static function isTypeLookup($pid, $rev, $rid)
370c73fba38SAnna Dabrowska    {
371c73fba38SAnna Dabrowska        return $pid === '';
372c73fba38SAnna Dabrowska    }
373c73fba38SAnna Dabrowska
374c73fba38SAnna Dabrowska    /**
375c73fba38SAnna Dabrowska     * Returns true if data is of type "serial"
376c73fba38SAnna Dabrowska     *
377c73fba38SAnna Dabrowska     * @param string $pid
378c73fba38SAnna Dabrowska     * @param int $rev
379c73fba38SAnna Dabrowska     * @param int $rid
380c73fba38SAnna Dabrowska     * @return bool
381c73fba38SAnna Dabrowska     */
382c73fba38SAnna Dabrowska    public static function isTypeSerial($pid, $rev, $rid)
383c73fba38SAnna Dabrowska    {
384c73fba38SAnna Dabrowska        return $pid !== '' && $rev === 0;
385c73fba38SAnna Dabrowska    }
386f411d872SAndreas Gohr}
387f411d872SAndreas Gohr
388f411d872SAndreas Gohr
389