xref: /plugin/struct/meta/AccessTable.php (revision 13eddb0f545f8f41b29b77f59e797a50081d9821)
1f411d872SAndreas Gohr<?php
2f411d872SAndreas Gohr
3f411d872SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
4f411d872SAndreas Gohr
5f411d872SAndreas Gohrabstract class AccessTable {
6f411d872SAndreas Gohr
7f411d872SAndreas Gohr    /** @var  Schema */
8f411d872SAndreas Gohr    protected $schema;
9f411d872SAndreas Gohr    protected $pid;
10f411d872SAndreas Gohr    protected $labels = array();
11f411d872SAndreas Gohr    protected $ts     = 0;
12f411d872SAndreas Gohr    /** @var \helper_plugin_sqlite */
13f411d872SAndreas Gohr    protected $sqlite;
14f411d872SAndreas Gohr
15f411d872SAndreas Gohr    // options on how to retieve data
16f411d872SAndreas Gohr    protected $opt_skipempty = false;
17f411d872SAndreas Gohr    protected $opt_rawvalue  = false;
18f411d872SAndreas Gohr
19f411d872SAndreas Gohr    /**
20f411d872SAndreas Gohr     * Factory Method to access a data or lookup table
21f411d872SAndreas Gohr     *
22f411d872SAndreas Gohr     * @param Schema $schema schema to load
23f411d872SAndreas Gohr     * @param string|int $pid Page or row id to access
2494c9aa4cSAndreas Gohr     * @return AccessTableData|AccessTableLookup
25f411d872SAndreas Gohr     */
26f411d872SAndreas Gohr    public static function bySchema(Schema $schema, $pid) {
27f411d872SAndreas Gohr        if($schema->isLookup()) {
2894c9aa4cSAndreas Gohr            return new AccessTableLookup($schema, $pid);
29f411d872SAndreas Gohr        } else {
3094c9aa4cSAndreas Gohr            return new AccessTableData($schema, $pid);
31f411d872SAndreas Gohr        }
32f411d872SAndreas Gohr    }
33f411d872SAndreas Gohr
34f411d872SAndreas Gohr    /**
35f411d872SAndreas Gohr     * Factory Method to access a data or lookup table
36f411d872SAndreas Gohr     *
37f411d872SAndreas Gohr     * @param string $tablename schema to load
38f411d872SAndreas Gohr     * @param string|int $pid Page or row id to access
39f411d872SAndreas Gohr     * @param int $ts from when is the schema to access?
4094c9aa4cSAndreas Gohr     * @return AccessTableData|AccessTableLookup
41f411d872SAndreas Gohr     */
42f411d872SAndreas Gohr    public static function byTableName($tablename, $pid, $ts = 0) {
43f411d872SAndreas Gohr        $schema = new Schema($tablename, $ts);
44f411d872SAndreas Gohr        return self::bySchema($schema, $pid);
45f411d872SAndreas Gohr    }
46f411d872SAndreas Gohr
47f411d872SAndreas Gohr    /**
48f411d872SAndreas Gohr     * AccessTable constructor
49f411d872SAndreas Gohr     *
50f411d872SAndreas Gohr     * @param Schema $schema
51f411d872SAndreas Gohr     * @param string $pid
52f411d872SAndreas Gohr     */
53f411d872SAndreas Gohr    public function __construct(Schema $schema, $pid) {
54f411d872SAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
55f411d872SAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
56f411d872SAndreas Gohr        $this->sqlite = $helper->getDB();
57f411d872SAndreas Gohr        if(!$this->sqlite) {
58f411d872SAndreas Gohr            throw new StructException('Sqlite plugin required');
59f411d872SAndreas Gohr        }
60f411d872SAndreas Gohr
61f411d872SAndreas Gohr        if(!$schema->getId()) {
62f411d872SAndreas Gohr            throw new StructException('Schema does not exist. Only data of existing schemas can be accessed');
63f411d872SAndreas Gohr        }
64f411d872SAndreas Gohr
65f411d872SAndreas Gohr        $this->schema = $schema;
66f411d872SAndreas Gohr        $this->pid = $pid;
67f411d872SAndreas Gohr        $this->ts = $this->schema->getTimeStamp();
68f411d872SAndreas Gohr        foreach($this->schema->getColumns() as $col) {
69f411d872SAndreas Gohr            $this->labels[$col->getColref()] = $col->getType()->getLabel();
70f411d872SAndreas Gohr        }
71f411d872SAndreas Gohr    }
72f411d872SAndreas Gohr
73f411d872SAndreas Gohr    /**
74f411d872SAndreas Gohr     * gives access to the schema
75f411d872SAndreas Gohr     *
76f411d872SAndreas Gohr     * @return Schema
77f411d872SAndreas Gohr     */
78f411d872SAndreas Gohr    public function getSchema() {
79f411d872SAndreas Gohr        return $this->schema;
80f411d872SAndreas Gohr    }
81f411d872SAndreas Gohr
82f411d872SAndreas Gohr    /**
83f411d872SAndreas Gohr     * Should remove the current data, by either deleting or ovewriting it
84f411d872SAndreas Gohr     *
85f411d872SAndreas Gohr     * @return bool if the delete succeeded
86f411d872SAndreas Gohr     */
87f411d872SAndreas Gohr    abstract public function clearData();
88f411d872SAndreas Gohr
89f411d872SAndreas Gohr    /**
90f411d872SAndreas Gohr     * Save the data to the database.
91f411d872SAndreas Gohr     *
92f411d872SAndreas Gohr     * We differentiate between single-value-column and multi-value-column by the value to the respective column-name,
93f411d872SAndreas Gohr     * i.e. depending on if that is a string or an array, respectively.
94f411d872SAndreas Gohr     *
95f411d872SAndreas Gohr     * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields
96f411d872SAndreas Gohr     * @return bool success of saving the data to the database
97f411d872SAndreas Gohr     */
98f411d872SAndreas Gohr    abstract public function saveData($data);
99f411d872SAndreas Gohr
100f411d872SAndreas Gohr    /**
101f411d872SAndreas Gohr     * Should empty or invisible (inpage) fields be returned?
102f411d872SAndreas Gohr     *
103f411d872SAndreas Gohr     * Defaults to false
104f411d872SAndreas Gohr     *
105f411d872SAndreas Gohr     * @param null|bool $set new value, null to read only
106f411d872SAndreas Gohr     * @return bool current value (after set)
107f411d872SAndreas Gohr     */
108f411d872SAndreas Gohr    public function optionSkipEmpty($set = null) {
109f411d872SAndreas Gohr        if(!is_null($set)) {
110f411d872SAndreas Gohr            $this->opt_skipempty = $set;
111f411d872SAndreas Gohr        }
112f411d872SAndreas Gohr        return $this->opt_skipempty;
113f411d872SAndreas Gohr    }
114f411d872SAndreas Gohr
115f411d872SAndreas Gohr    /**
116f411d872SAndreas Gohr     * Should the values be returned raw or are complex returns okay?
117f411d872SAndreas Gohr     *
118f411d872SAndreas Gohr     * Defaults to false = complex values okay
119f411d872SAndreas Gohr     *
120f411d872SAndreas Gohr     * @param null|bool $set new value, null to read only
121f411d872SAndreas Gohr     * @return bool current value (after set)
122f411d872SAndreas Gohr     */
123f411d872SAndreas Gohr    public function optionRawValue($set = null) {
124f411d872SAndreas Gohr        if(!is_null($set)) {
125f411d872SAndreas Gohr            $this->opt_rawvalue = $set;
126f411d872SAndreas Gohr        }
127f411d872SAndreas Gohr        return $this->opt_rawvalue;
128f411d872SAndreas Gohr    }
129f411d872SAndreas Gohr
130f411d872SAndreas Gohr
131f411d872SAndreas Gohr    /**
132f411d872SAndreas Gohr     * Get the value of a single column
133f411d872SAndreas Gohr     *
134f411d872SAndreas Gohr     * @param Column $column
135f411d872SAndreas Gohr     * @return Value|null
136f411d872SAndreas Gohr     */
137f411d872SAndreas Gohr    public function getDataColumn($column) {
138f411d872SAndreas Gohr        $data = $this->getData();
139f411d872SAndreas Gohr        foreach($data as $value) {
140f411d872SAndreas Gohr            if($value->getColumn() == $column) {
141f411d872SAndreas Gohr                return $value;
142f411d872SAndreas Gohr            }
143f411d872SAndreas Gohr        }
144f411d872SAndreas Gohr        return null;
145f411d872SAndreas Gohr    }
146f411d872SAndreas Gohr
147f411d872SAndreas Gohr    /**
148f411d872SAndreas Gohr     * returns the data saved for the page
149f411d872SAndreas Gohr     *
150f411d872SAndreas Gohr     * @return Value[] a list of values saved for the current page
151f411d872SAndreas Gohr     */
152f411d872SAndreas Gohr    public function getData() {
153f411d872SAndreas Gohr        $this->setCorrectTimestamp($this->pid, $this->ts);
154f411d872SAndreas Gohr        $data = $this->getDataFromDB();
155f411d872SAndreas Gohr        $data = $this->consolidateData($data, false);
156f411d872SAndreas Gohr        return $data;
157f411d872SAndreas Gohr    }
158f411d872SAndreas Gohr
159f411d872SAndreas Gohr    /**
160f411d872SAndreas Gohr     * returns the data saved for the page as associative array
161f411d872SAndreas Gohr     *
162f411d872SAndreas Gohr     * The array returned is in the same format as used in @see saveData()
163f411d872SAndreas Gohr     *
164f411d872SAndreas Gohr     * @return array
165f411d872SAndreas Gohr     */
166f411d872SAndreas Gohr    public function getDataArray() {
167f411d872SAndreas Gohr        $this->setCorrectTimestamp($this->pid, $this->ts);
168f411d872SAndreas Gohr        $data = $this->getDataFromDB();
169f411d872SAndreas Gohr        $data = $this->consolidateData($data, true);
170f411d872SAndreas Gohr        return $data;
171f411d872SAndreas Gohr    }
172f411d872SAndreas Gohr
173f411d872SAndreas Gohr    /**
174f411d872SAndreas Gohr     * Return the data in pseudo syntax
175f411d872SAndreas Gohr     */
176f411d872SAndreas Gohr    public function getDataPseudoSyntax() {
177f411d872SAndreas Gohr        $result = '';
178f411d872SAndreas Gohr        $data = $this->getDataArray();
179f411d872SAndreas Gohr        foreach($data as $key => $value) {
180f411d872SAndreas Gohr            $key = $this->schema->getTable() . ".$key";
181f411d872SAndreas Gohr            if(is_array($value)) $value = join(', ', $value);
182f411d872SAndreas Gohr            $result .= sprintf("% -20s : %s\n", $key, $value);
183f411d872SAndreas Gohr        }
184f411d872SAndreas Gohr        return $result;
185f411d872SAndreas Gohr    }
186f411d872SAndreas Gohr
187f411d872SAndreas Gohr    /**
188f411d872SAndreas Gohr     * retrieve the data saved for the page from the database. Usually there is no need to call this function.
189f411d872SAndreas Gohr     * Call @see SchemaData::getData instead.
190f411d872SAndreas Gohr     */
191f411d872SAndreas Gohr    protected function getDataFromDB() {
192f411d872SAndreas Gohr        list($sql, $opt) = $this->buildGetDataSQL();
193f411d872SAndreas Gohr
194f411d872SAndreas Gohr        $res = $this->sqlite->query($sql, $opt);
195f411d872SAndreas Gohr        $data = $this->sqlite->res2arr($res);
196f411d872SAndreas Gohr
197f411d872SAndreas Gohr        return $data;
198f411d872SAndreas Gohr    }
199f411d872SAndreas Gohr
200f411d872SAndreas Gohr    /**
201f411d872SAndreas Gohr     * Creates a proper result array from the database data
202f411d872SAndreas Gohr     *
203f411d872SAndreas Gohr     * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB
204f411d872SAndreas Gohr     * @param bool $asarray return data as associative array (true) or as array of Values (false)
205f411d872SAndreas Gohr     * @return array|Value[]
206f411d872SAndreas Gohr     */
207f411d872SAndreas Gohr    protected function consolidateData($DBdata, $asarray = false) {
208f411d872SAndreas Gohr        $data = array();
209f411d872SAndreas Gohr
210f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
211f411d872SAndreas Gohr
212f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
213f411d872SAndreas Gohr
214f411d872SAndreas Gohr            // if no data saved, yet return empty strings
215f411d872SAndreas Gohr            if($DBdata) {
216f411d872SAndreas Gohr                $val = $DBdata[0]['col'.$col->getColref()];
217f411d872SAndreas Gohr            } else {
218f411d872SAndreas Gohr                $val = '';
219f411d872SAndreas Gohr            }
220f411d872SAndreas Gohr
221f411d872SAndreas Gohr            // multi val data is concatenated
222f411d872SAndreas Gohr            if($col->isMulti()) {
223f411d872SAndreas Gohr                $val = explode($sep, $val);
224f411d872SAndreas Gohr                if($this->opt_rawvalue) {
225f411d872SAndreas Gohr                    $val = array_map(
226f411d872SAndreas Gohr                        function ($val) use ($col) { // FIXME requires PHP 5.4+
227f411d872SAndreas Gohr                            return $col->getType()->rawValue($val);
228f411d872SAndreas Gohr                        },
229f411d872SAndreas Gohr                        $val
230f411d872SAndreas Gohr                    );
231f411d872SAndreas Gohr                }
232f411d872SAndreas Gohr                $val = array_filter($val);
233f411d872SAndreas Gohr            } else {
234f411d872SAndreas Gohr                if($this->opt_rawvalue) {
235f411d872SAndreas Gohr                    $val = $col->getType()->rawValue($val);
236f411d872SAndreas Gohr                }
237f411d872SAndreas Gohr            }
238f411d872SAndreas Gohr
239f411d872SAndreas Gohr            if($this->opt_skipempty && ($val === '' || $val == array())) continue;
240f411d872SAndreas Gohr            if($this->opt_skipempty && !$col->isVisibleInPage()) continue;
241f411d872SAndreas Gohr
242f411d872SAndreas Gohr            if($asarray) {
243f411d872SAndreas Gohr                $data[$col->getLabel()] = $val;
244f411d872SAndreas Gohr            } else {
245f411d872SAndreas Gohr                $data[] = new Value($col, $val);
246f411d872SAndreas Gohr            }
247f411d872SAndreas Gohr        }
248f411d872SAndreas Gohr
249f411d872SAndreas Gohr        return $data;
250f411d872SAndreas Gohr    }
251f411d872SAndreas Gohr
252f411d872SAndreas Gohr    /**
253f411d872SAndreas Gohr     * Builds the SQL statement to select the data for this page and schema
254f411d872SAndreas Gohr     *
255f411d872SAndreas Gohr     * @return array Two fields: the SQL string and the parameters array
256f411d872SAndreas Gohr     */
257f411d872SAndreas Gohr    protected function buildGetDataSQL() {
258f411d872SAndreas Gohr        $sep = Search::CONCAT_SEPARATOR;
259f411d872SAndreas Gohr        $stable = 'data_' . $this->schema->getTable();
260f411d872SAndreas Gohr        $mtable = 'multi_' . $this->schema->getTable();
261f411d872SAndreas Gohr
262f411d872SAndreas Gohr        $QB = new QueryBuilder();
263f411d872SAndreas Gohr        $QB->addTable($stable, 'DATA');
264f411d872SAndreas Gohr        $QB->addSelectColumn('DATA', 'pid', 'PID');
265f411d872SAndreas Gohr        $QB->addGroupByStatement('DATA.pid');
266f411d872SAndreas Gohr
267f411d872SAndreas Gohr        foreach($this->schema->getColumns(false) as $col) {
268f411d872SAndreas Gohr
269f411d872SAndreas Gohr            $colref = $col->getColref();
270f411d872SAndreas Gohr            $colname = 'col'.$colref;
271f411d872SAndreas Gohr
272f411d872SAndreas Gohr            if($col->getType()->isMulti()) {
273f411d872SAndreas Gohr                $tn = 'M' . $colref;
274f411d872SAndreas Gohr                $QB->addLeftJoin(
275f411d872SAndreas Gohr                    'DATA',
276f411d872SAndreas Gohr                    $mtable,
277f411d872SAndreas Gohr                    $tn,
278f411d872SAndreas Gohr                    "DATA.pid = $tn.pid AND DATA.rev = $tn.rev AND $tn.colref = $colref"
279f411d872SAndreas Gohr                );
280f411d872SAndreas Gohr                $col->getType()->select($QB, $tn, 'value', $colname);
281f411d872SAndreas Gohr                $sel = $QB->getSelectStatement($colname);
282f411d872SAndreas Gohr                $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $colname);
283f411d872SAndreas Gohr            } else {
284f411d872SAndreas Gohr                $col->getType()->select($QB, 'DATA', $colname, $colname);
285f411d872SAndreas Gohr                $QB->addGroupByStatement($colname);
286f411d872SAndreas Gohr            }
287f411d872SAndreas Gohr        }
288f411d872SAndreas Gohr
289f411d872SAndreas Gohr        $pl = $QB->addValue($this->pid);
290f411d872SAndreas Gohr        $QB->filters()->whereAnd("DATA.pid = $pl");
291f411d872SAndreas Gohr        $pl = $QB->addValue($this->ts);
292f411d872SAndreas Gohr        $QB->filters()->whereAnd("DATA.rev = $pl");
293f411d872SAndreas Gohr
294f411d872SAndreas Gohr        return $QB->getSQL();
295f411d872SAndreas Gohr    }
296f411d872SAndreas Gohr
297f411d872SAndreas Gohr    /**
298*13eddb0fSAndreas Gohr     * @param int $ts
299*13eddb0fSAndreas Gohr     */
300*13eddb0fSAndreas Gohr    public function setTimestamp($ts) {
301*13eddb0fSAndreas Gohr        $this->ts = $ts;
302*13eddb0fSAndreas Gohr    }
303*13eddb0fSAndreas Gohr
304*13eddb0fSAndreas Gohr
305*13eddb0fSAndreas Gohr    /**
306f411d872SAndreas Gohr     * Set $this->ts to an existing timestamp, which is either current timestamp if it exists
307f411d872SAndreas Gohr     * or the next oldest timestamp that exists. If not timestamp is provided it is the newest timestamp that exists.
308f411d872SAndreas Gohr     *
309f411d872SAndreas Gohr     * @param          $page
310f411d872SAndreas Gohr     * @param int|null $ts
311f411d872SAndreas Gohr     * @fixme clear up description
312f411d872SAndreas Gohr     */
313f411d872SAndreas Gohr    abstract protected function setCorrectTimestamp($page, $ts = null);
314f411d872SAndreas Gohr}
315f411d872SAndreas Gohr
316f411d872SAndreas Gohr
317