xref: /plugin/struct/meta/AccessTable.php (revision c73fba3804eebb07c2b5d6de2e8c16480ab70ca4)
1f411d872SAndreas Gohr<?php
2f411d872SAndreas Gohr
3f411d872SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
4f411d872SAndreas Gohr
5*c73fba38SAnna Dabrowska/**
6*c73fba38SAnna Dabrowska * Class AccessTable
7*c73fba38SAnna Dabrowska *
8*c73fba38SAnna Dabrowska * Base class for data accessors
9*c73fba38SAnna Dabrowska *
10*c73fba38SAnna Dabrowska * @package dokuwiki\plugin\struct\meta
11*c73fba38SAnna Dabrowska */
12f411d872SAndreas Gohrabstract class AccessTable {
13f411d872SAndreas Gohr
14f411d872SAndreas Gohr    /** @var  Schema */
15f411d872SAndreas Gohr    protected $schema;
16f411d872SAndreas Gohr    protected $pid;
170ceefd5cSAnna Dabrowska    protected $rid;
18f411d872SAndreas Gohr    protected $labels = array();
19f411d872SAndreas Gohr    protected $ts     = 0;
20f411d872SAndreas Gohr    /** @var \helper_plugin_sqlite */
21f411d872SAndreas Gohr    protected $sqlite;
22f411d872SAndreas Gohr
2390421550SAndreas Gohr    // options on how to retrieve data
24f411d872SAndreas Gohr    protected $opt_skipempty = false;
25f411d872SAndreas Gohr
26f411d872SAndreas Gohr    /**
27b9d35ff2SAnna Dabrowska     * Factory method returning the appropriate data accessor (page, lookup or serial)
28f411d872SAndreas Gohr     *
29f411d872SAndreas Gohr     * @param Schema $schema schema to load
3086a40c1eSAnna Dabrowska     * @param string $pid Page id to access
31897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
3286a40c1eSAnna Dabrowska     * @param int $rid Row id, 0 for page type data, otherwise autoincrement
33b9d35ff2SAnna Dabrowska     * @return AccessTableData|AccessTableLookup|AccessTableSerial
34f411d872SAndreas Gohr     */
350ceefd5cSAnna Dabrowska    public static function bySchema(Schema $schema, $pid, $ts = 0, $rid = 0) {
36*c73fba38SAnna Dabrowska        if (self::isTypeLookup($pid, $ts, $rid)) {
370ceefd5cSAnna Dabrowska            return new AccessTableLookup($schema, $pid, $ts, $rid);
38f411d872SAndreas Gohr        }
39*c73fba38SAnna Dabrowska        if (self::isTypeSerial($pid, $ts, $rid)) {
40b9d35ff2SAnna Dabrowska            return new AccessTableSerial($schema, $pid, $ts, $rid);
41b9d35ff2SAnna Dabrowska        }
420ceefd5cSAnna Dabrowska        return new AccessTableData($schema, $pid, $ts, $rid);
43f411d872SAndreas Gohr    }
44f411d872SAndreas Gohr
45f411d872SAndreas Gohr    /**
46*c73fba38SAnna Dabrowska     * Factory Method to access data
47f411d872SAndreas Gohr     *
48f411d872SAndreas Gohr     * @param string $tablename schema to load
4986a40c1eSAnna Dabrowska     * @param string $pid Page id to access
50897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
5186a40c1eSAnna Dabrowska     * @param int $rid Row id, 0 for page type data, otherwise autoincrement
52*c73fba38SAnna Dabrowska     * @return AccessTableData|AccessTableLookup|AccessTableSerial
53f411d872SAndreas Gohr     */
540ceefd5cSAnna Dabrowska    public static function byTableName($tablename, $pid, $ts = 0, $rid = 0) {
55f411d872SAndreas Gohr        $schema = new Schema($tablename, $ts);
560ceefd5cSAnna Dabrowska        return self::bySchema($schema, $pid, $ts, $rid);
57f411d872SAndreas Gohr    }
58f411d872SAndreas Gohr
59f411d872SAndreas Gohr    /**
60f411d872SAndreas Gohr     * AccessTable constructor
61f411d872SAndreas Gohr     *
62897aef42SAndreas Gohr     * @param Schema $schema The schema valid at $ts
6386a40c1eSAnna Dabrowska     * @param string $pid Page id
64897aef42SAndreas Gohr     * @param int $ts Time at which the data should be read or written, 0 for now
650ceefd5cSAnna Dabrowska     * @param int $rid Row id: 0 for pages, autoincremented for other types
66f411d872SAndreas Gohr     */
670ceefd5cSAnna Dabrowska    public function __construct(Schema $schema, $pid, $ts = 0, $rid = 0) {
68f411d872SAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
69f411d872SAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
70f411d872SAndreas Gohr        $this->sqlite = $helper->getDB();
71f411d872SAndreas Gohr
72f411d872SAndreas Gohr        if(!$schema->getId()) {
73f411d872SAndreas Gohr            throw new StructException('Schema does not exist. Only data of existing schemas can be accessed');
74f411d872SAndreas Gohr        }
75f411d872SAndreas Gohr
76f411d872SAndreas Gohr        $this->schema = $schema;
77f411d872SAndreas Gohr        $this->pid = $pid;
780ceefd5cSAnna Dabrowska        $this->rid = $rid;
79897aef42SAndreas Gohr        $this->setTimestamp($ts);
80f411d872SAndreas Gohr        foreach($this->schema->getColumns() as $col) {
81f411d872SAndreas Gohr            $this->labels[$col->getColref()] = $col->getType()->getLabel();
82f411d872SAndreas Gohr        }
83f411d872SAndreas Gohr    }
84f411d872SAndreas Gohr
85f411d872SAndreas Gohr    /**
86f411d872SAndreas Gohr     * gives access to the schema
87f411d872SAndreas Gohr     *
88f411d872SAndreas Gohr     * @return Schema
89f411d872SAndreas Gohr     */
90f411d872SAndreas Gohr    public function getSchema() {
91f411d872SAndreas Gohr        return $this->schema;
92f411d872SAndreas Gohr    }
93f411d872SAndreas Gohr
94f411d872SAndreas Gohr    /**
95f107f479SAndreas Gohr     * The current pid
96f107f479SAndreas Gohr     *
9786a40c1eSAnna Dabrowska     * @return string
98f107f479SAndreas Gohr     */
99f107f479SAndreas Gohr    public function getPid() {
100f107f479SAndreas Gohr        return $this->pid;
101f107f479SAndreas Gohr    }
102f107f479SAndreas Gohr
103f107f479SAndreas Gohr    /**
1040ceefd5cSAnna Dabrowska     * The current rid
1050ceefd5cSAnna Dabrowska     *
10686a40c1eSAnna Dabrowska     * @return int
1070ceefd5cSAnna Dabrowska     */
1080ceefd5cSAnna Dabrowska    public function getRid() {
1090ceefd5cSAnna Dabrowska        return $this->rid;
1100ceefd5cSAnna Dabrowska    }
1110ceefd5cSAnna Dabrowska
1120ceefd5cSAnna Dabrowska    /**
113f411d872SAndreas Gohr     * Should remove the current data, by either deleting or ovewriting it
114f411d872SAndreas Gohr     *
115f411d872SAndreas Gohr     * @return bool if the delete succeeded
116f411d872SAndreas Gohr     */
117f411d872SAndreas Gohr    abstract public function clearData();
118f411d872SAndreas Gohr
119f411d872SAndreas Gohr    /**
120f411d872SAndreas Gohr     * Save the data to the database.
121f411d872SAndreas Gohr     *
122f411d872SAndreas Gohr     * We differentiate between single-value-column and multi-value-column by the value to the respective column-name,
123f411d872SAndreas Gohr     * i.e. depending on if that is a string or an array, respectively.
124f411d872SAndreas Gohr     *
125f411d872SAndreas Gohr     * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields
126f411d872SAndreas Gohr     * @return bool success of saving the data to the database
127f411d872SAndreas Gohr     */
128f411d872SAndreas Gohr    abstract public function saveData($data);
129f411d872SAndreas Gohr
130f411d872SAndreas Gohr    /**
131f411d872SAndreas Gohr     * Should empty or invisible (inpage) fields be returned?
132f411d872SAndreas Gohr     *
133f411d872SAndreas Gohr     * Defaults to false
134f411d872SAndreas Gohr     *
135f411d872SAndreas Gohr     * @param null|bool $set new value, null to read only
136f411d872SAndreas Gohr     * @return bool current value (after set)
137f411d872SAndreas Gohr     */
138f411d872SAndreas Gohr    public function optionSkipEmpty($set = null) {
139f411d872SAndreas Gohr        if(!is_null($set)) {
140f411d872SAndreas Gohr            $this->opt_skipempty = $set;
141f411d872SAndreas Gohr        }
142f411d872SAndreas Gohr        return $this->opt_skipempty;
143f411d872SAndreas Gohr    }
144f411d872SAndreas Gohr
145f411d872SAndreas Gohr    /**
146f411d872SAndreas Gohr     * Get the value of a single column
147f411d872SAndreas Gohr     *
148f411d872SAndreas Gohr     * @param Column $column
149f411d872SAndreas Gohr     * @return Value|null
150f411d872SAndreas Gohr     */
151f411d872SAndreas Gohr    public function getDataColumn($column) {
152f411d872SAndreas Gohr        $data = $this->getData();
153f411d872SAndreas Gohr        foreach($data as $value) {
154f411d872SAndreas Gohr            if($value->getColumn() == $column) {
155f411d872SAndreas Gohr                return $value;
156f411d872SAndreas Gohr            }
157f411d872SAndreas Gohr        }
158f411d872SAndreas Gohr        return null;
159f411d872SAndreas Gohr    }
160f411d872SAndreas Gohr
161f411d872SAndreas Gohr    /**
162f411d872SAndreas Gohr     * returns the data saved for the page
163f411d872SAndreas Gohr     *
164f411d872SAndreas Gohr     * @return Value[] a list of values saved for the current page
165f411d872SAndreas Gohr     */
166f411d872SAndreas Gohr    public function getData() {
167f411d872SAndreas Gohr        $data = $this->getDataFromDB();
168f411d872SAndreas Gohr        $data = $this->consolidateData($data, false);
169f411d872SAndreas Gohr        return $data;
170f411d872SAndreas Gohr    }
171f411d872SAndreas Gohr
172f411d872SAndreas Gohr    /**
173f411d872SAndreas Gohr     * returns the data saved for the page as associative array
174f411d872SAndreas Gohr     *
175f411d872SAndreas Gohr     * The array returned is in the same format as used in @see saveData()
176f411d872SAndreas Gohr     *
17790421550SAndreas Gohr     * It always returns raw Values!
17890421550SAndreas Gohr     *
179f411d872SAndreas Gohr     * @return array
180f411d872SAndreas Gohr     */
181f411d872SAndreas Gohr    public function getDataArray() {
182f411d872SAndreas Gohr        $data = $this->getDataFromDB();
183f411d872SAndreas Gohr        $data = $this->consolidateData($data, true);
184f411d872SAndreas Gohr        return $data;
185f411d872SAndreas Gohr    }
186f411d872SAndreas Gohr
187f411d872SAndreas Gohr    /**
188f411d872SAndreas Gohr     * Return the data in pseudo syntax
189f411d872SAndreas Gohr     */
190f411d872SAndreas Gohr    public function getDataPseudoSyntax() {
191f411d872SAndreas Gohr        $result = '';
192a0a1d14eSAndreas Gohr        $data = $this->getData();
193a0a1d14eSAndreas Gohr
194a0a1d14eSAndreas Gohr        foreach($data as $value) {
195a0a1d14eSAndreas Gohr            $key = $value->getColumn()->getFullQualifiedLabel();
196a0a1d14eSAndreas Gohr            $value = $value->getDisplayValue();
197f411d872SAndreas Gohr            if(is_array($value)) $value = join(', ', $value);
198f411d872SAndreas Gohr            $result .= sprintf("% -20s : %s\n", $key, $value);
199f411d872SAndreas Gohr        }
200f411d872SAndreas Gohr        return $result;
201f411d872SAndreas Gohr    }
202f411d872SAndreas Gohr
203f411d872SAndreas Gohr    /**
204f411d872SAndreas Gohr     * retrieve the data saved for the page from the database. Usually there is no need to call this function.
205f411d872SAndreas Gohr     * Call @see SchemaData::getData instead.
206f411d872SAndreas Gohr     */
207f411d872SAndreas Gohr    protected function getDataFromDB() {
208f411d872SAndreas Gohr        list($sql, $opt) = $this->buildGetDataSQL();
209f411d872SAndreas Gohr
210f411d872SAndreas Gohr        $res = $this->sqlite->query($sql, $opt);
211f411d872SAndreas Gohr        $data = $this->sqlite->res2arr($res);
2129c00b26cSAndreas Gohr        $this->sqlite->res_close($res);
213f411d872SAndreas Gohr        return $data;
214f411d872SAndreas Gohr    }
215f411d872SAndreas Gohr
216f411d872SAndreas Gohr    /**
217f411d872SAndreas Gohr     * Creates a proper result array from the database data
218f411d872SAndreas Gohr     *
219f411d872SAndreas Gohr     * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB
220f411d872SAndreas Gohr     * @param bool $asarray return data as associative array (true) or as array of Values (false)
221f411d872SAndreas Gohr     * @return array|Value[]
222f411d872SAndreas Gohr     */
223f411d872SAndreas Gohr    protected function consolidateData($DBdata, $asarray = false) {
224f411d872SAndreas Gohr        $data = array();
225f411d872SAndreas Gohr
226f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
227f411d872SAndreas Gohr
228f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
229f411d872SAndreas Gohr
23090421550SAndreas Gohr            // if no data saved yet, return empty strings
231f411d872SAndreas Gohr            if($DBdata) {
232bab52340SAndreas Gohr                $val = $DBdata[0]['out' . $col->getColref()];
233f411d872SAndreas Gohr            } else {
234f411d872SAndreas Gohr                $val = '';
235f411d872SAndreas Gohr            }
236f411d872SAndreas Gohr
237f411d872SAndreas Gohr            // multi val data is concatenated
238f411d872SAndreas Gohr            if($col->isMulti()) {
239f411d872SAndreas Gohr                $val = explode($sep, $val);
240f411d872SAndreas Gohr                $val = array_filter($val);
241f411d872SAndreas Gohr            }
242f411d872SAndreas Gohr
24390421550SAndreas Gohr            $value = new Value($col, $val);
244f411d872SAndreas Gohr
24590421550SAndreas Gohr            if($this->opt_skipempty && $value->isEmpty()) continue;
24690421550SAndreas Gohr            if($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption?
24790421550SAndreas Gohr
24890421550SAndreas Gohr            // for arrays, we return the raw value only
249f411d872SAndreas Gohr            if($asarray) {
25090421550SAndreas Gohr                $data[$col->getLabel()] = $value->getRawValue();
251f411d872SAndreas Gohr            } else {
2526e54daafSMichael Große                $data[$col->getLabel()] = $value;
253f411d872SAndreas Gohr            }
254f411d872SAndreas Gohr        }
255f411d872SAndreas Gohr
256f411d872SAndreas Gohr        return $data;
257f411d872SAndreas Gohr    }
258f411d872SAndreas Gohr
259f411d872SAndreas Gohr    /**
260f411d872SAndreas Gohr     * Builds the SQL statement to select the data for this page and schema
261f411d872SAndreas Gohr     *
262f411d872SAndreas Gohr     * @return array Two fields: the SQL string and the parameters array
263f411d872SAndreas Gohr     */
2646fd73b4bSAnna Dabrowska    protected function buildGetDataSQL($idColumn = 'pid') {
265f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
266f411d872SAndreas Gohr        $stable = 'data_' . $this->schema->getTable();
267f411d872SAndreas Gohr        $mtable = 'multi_' . $this->schema->getTable();
268f411d872SAndreas Gohr
269f411d872SAndreas Gohr        $QB = new QueryBuilder();
270f411d872SAndreas Gohr        $QB->addTable($stable, 'DATA');
2716fd73b4bSAnna Dabrowska        $QB->addSelectColumn('DATA', $idColumn, strtoupper($idColumn));
2726fd73b4bSAnna Dabrowska        $QB->addGroupByStatement("DATA.$idColumn");
273f411d872SAndreas Gohr
274f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
275f411d872SAndreas Gohr
276f411d872SAndreas Gohr            $colref = $col->getColref();
277f411d872SAndreas Gohr            $colname = 'col' . $colref;
278bab52340SAndreas Gohr            $outname = 'out' . $colref;
279f411d872SAndreas Gohr
280f411d872SAndreas Gohr            if($col->getType()->isMulti()) {
281f411d872SAndreas Gohr                $tn = 'M' . $colref;
282f411d872SAndreas Gohr                $QB->addLeftJoin(
283f411d872SAndreas Gohr                    'DATA',
284f411d872SAndreas Gohr                    $mtable,
285f411d872SAndreas Gohr                    $tn,
2866fd73b4bSAnna Dabrowska                    "DATA.$idColumn = $tn.$idColumn AND DATA.rev = $tn.rev AND $tn.colref = $colref"
287f411d872SAndreas Gohr                );
288bab52340SAndreas Gohr                $col->getType()->select($QB, $tn, 'value', $outname);
289bab52340SAndreas Gohr                $sel = $QB->getSelectStatement($outname);
290bab52340SAndreas Gohr                $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname);
291f411d872SAndreas Gohr            } else {
292bab52340SAndreas Gohr                $col->getType()->select($QB, 'DATA', $colname, $outname);
293bab52340SAndreas Gohr                $QB->addGroupByStatement($outname);
294f411d872SAndreas Gohr            }
295f411d872SAndreas Gohr        }
296f411d872SAndreas Gohr
2976fd73b4bSAnna Dabrowska        $pl = $QB->addValue($this->{$idColumn});
2986fd73b4bSAnna Dabrowska        $QB->filters()->whereAnd("DATA.$idColumn = $pl");
299897aef42SAndreas Gohr        $pl = $QB->addValue($this->getLastRevisionTimestamp());
300f411d872SAndreas Gohr        $QB->filters()->whereAnd("DATA.rev = $pl");
301f411d872SAndreas Gohr
302f411d872SAndreas Gohr        return $QB->getSQL();
303f411d872SAndreas Gohr    }
304f411d872SAndreas Gohr
305f411d872SAndreas Gohr    /**
30613eddb0fSAndreas Gohr     * @param int $ts
30713eddb0fSAndreas Gohr     */
30813eddb0fSAndreas Gohr    public function setTimestamp($ts) {
309897aef42SAndreas Gohr        if($ts && $ts < $this->schema->getTimeStamp()) {
310897aef42SAndreas Gohr            throw new StructException('Given timestamp is not valid for current Schema');
311897aef42SAndreas Gohr        }
312897aef42SAndreas Gohr
31313eddb0fSAndreas Gohr        $this->ts = $ts;
31413eddb0fSAndreas Gohr    }
31513eddb0fSAndreas Gohr
31613eddb0fSAndreas Gohr    /**
317897aef42SAndreas Gohr     * Return the last time an edit happened for this table for the currently set
318897aef42SAndreas Gohr     * time and pid. When the current timestamp is 0, the newest revision is
319897aef42SAndreas Gohr     * returned. Used in @see buildGetDataSQL()
320f411d872SAndreas Gohr     *
321897aef42SAndreas Gohr     * @return int
322f411d872SAndreas Gohr     */
323897aef42SAndreas Gohr    abstract protected function getLastRevisionTimestamp();
32487dc1344SAndreas Gohr
32587dc1344SAndreas Gohr    /**
32687dc1344SAndreas Gohr     * Check if the given data validates against the current types.
32787dc1344SAndreas Gohr     *
32887dc1344SAndreas Gohr     * @param array $data
32993ca6f4fSAndreas Gohr     * @return AccessDataValidator
33087dc1344SAndreas Gohr     */
33187dc1344SAndreas Gohr    public function getValidator($data) {
33293ca6f4fSAndreas Gohr        return new AccessDataValidator($this, $data);
33387dc1344SAndreas Gohr    }
334*c73fba38SAnna Dabrowska
335*c73fba38SAnna Dabrowska    /**
336*c73fba38SAnna Dabrowska     * Returns true if data is of type "page"
337*c73fba38SAnna Dabrowska     *
338*c73fba38SAnna Dabrowska     * @param string $pid
339*c73fba38SAnna Dabrowska     * @param int $rev
340*c73fba38SAnna Dabrowska     * @param int $rid
341*c73fba38SAnna Dabrowska     * @return bool
342*c73fba38SAnna Dabrowska     */
343*c73fba38SAnna Dabrowska    public static function isTypePage($pid, $rev, $rid)
344*c73fba38SAnna Dabrowska    {
345*c73fba38SAnna Dabrowska        return $rev > 0;
346*c73fba38SAnna Dabrowska    }
347*c73fba38SAnna Dabrowska
348*c73fba38SAnna Dabrowska    /**
349*c73fba38SAnna Dabrowska     * Returns true if data is of type "lookup"
350*c73fba38SAnna Dabrowska     *
351*c73fba38SAnna Dabrowska     * @param string $pid
352*c73fba38SAnna Dabrowska     * @param int $rev
353*c73fba38SAnna Dabrowska     * @param int $rid
354*c73fba38SAnna Dabrowska     * @return bool
355*c73fba38SAnna Dabrowska     */
356*c73fba38SAnna Dabrowska    public static function isTypeLookup($pid, $rev, $rid)
357*c73fba38SAnna Dabrowska    {
358*c73fba38SAnna Dabrowska        return $pid === '';
359*c73fba38SAnna Dabrowska    }
360*c73fba38SAnna Dabrowska
361*c73fba38SAnna Dabrowska    /**
362*c73fba38SAnna Dabrowska     * Returns true if data is of type "serial"
363*c73fba38SAnna Dabrowska     *
364*c73fba38SAnna Dabrowska     * @param string $pid
365*c73fba38SAnna Dabrowska     * @param int $rev
366*c73fba38SAnna Dabrowska     * @param int $rid
367*c73fba38SAnna Dabrowska     * @return bool
368*c73fba38SAnna Dabrowska     */
369*c73fba38SAnna Dabrowska    public static function isTypeSerial($pid, $rev, $rid)
370*c73fba38SAnna Dabrowska    {
371*c73fba38SAnna Dabrowska        return $pid !== '' && $rev === 0;
372*c73fba38SAnna Dabrowska    }
373f411d872SAndreas Gohr}
374f411d872SAndreas Gohr
375f411d872SAndreas Gohr
376