xref: /plugin/struct/meta/QueryBuilder.php (revision b69eb99aac6baac10c7c3698339b391fb015dada)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5/**
6 * Class QueryBuilder
7 * @package dokuwiki\plugin\struct\meta
8 */
9class QueryBuilder {
10
11    /** @var array placeholder -> values */
12    protected $values = array();
13    /** @var array (alias -> statement */
14    protected $select = array();
15    /** @var array (alias -> statement) */
16    protected $from = array();
17    /** @var array (alias -> "table"|"join") keeps how tables were added, as table or join*/
18    protected $type = array();
19    /** @var QueryBuilderWhere */
20    protected $where;
21    /** @var  string[] */
22    protected $orderby = array();
23    /** @var  string[] */
24    protected $groupby = array();
25
26    /**
27     * QueryBuilder constructor.
28     */
29    public function __construct() {
30        $this->where = new QueryBuilderWhere();
31    }
32
33    /**
34     * Adds a column to select
35     *
36     * If the alias already exists, the current statement for that alias will be overwritten.
37     *
38     * @param string $tablealias The table to select from
39     * @param string $column The column to select
40     * @param string $alias Under whichname to slect the column. blank for column name
41     */
42    public function addSelectColumn($tablealias, $column, $alias = '') {
43        if($alias === '') $alias = $column;
44        if(!isset($this->from[$tablealias])) throw new StructException('Table Alias does not exist');
45        $this->select[$alias] = "$tablealias.$column AS $alias";
46    }
47
48    /**
49     * Add a new select statement (the column part of it)
50     *
51     * Basically the same as @see addSelectColumn but accepts any statement. This is useful to
52     * select things like fixed strings or more complex function calls, but the correctness will not
53     * be checked.
54     *
55     * If the alias already exists, the current statement for that alias will be overwritten.
56     *
57     * @param string $statement
58     * @param string $alias
59     */
60    public function addSelectStatement($statement, $alias) {
61        $this->select[$alias] = "$statement AS $alias";
62    }
63
64    /**
65     * Adds the the table to the FROM statement part
66     *
67     * @param string $table the table to add
68     * @param string $alias alias for the table, blank for table name
69     */
70    public function addTable($table, $alias = '') {
71        if($alias === '') $alias = $table;
72        if(isset($this->from[$alias])) throw new StructException('Table Alias exists');
73        $this->from[$alias] = "$table AS $alias";
74        $this->type[$alias] = 'table';
75    }
76
77    /**
78     * Adds a LEFT JOIN clause to the FROM statement part, sorted at the correct spot
79     *
80     * @param string $leftalias the alias of the left table you're joining on, has to exist already
81     * @param string $righttable the right table to be joined
82     * @param string $rightalias an alias for the right table, blank for table name
83     * @param string $onclause the ON clause condition the join is based on
84     */
85    public function addLeftJoin($leftalias, $righttable, $rightalias, $onclause) {
86        if($rightalias === '') $rightalias = $righttable;
87        if(!isset($this->from[$leftalias])) throw new StructException('Table Alias does not exist');
88        if(isset($this->from[$rightalias])) throw new StructException('Table Alias already exists');
89
90        $pos = array_search($leftalias, array_keys($this->from));
91        $statement = "LEFT OUTER JOIN $righttable AS $rightalias ON $onclause";
92        $this->from = $this->array_insert($this->from, array($rightalias => $statement), $pos + 1);
93        $this->type[$rightalias] = 'join';
94    }
95
96    /**
97     * Returns the current WHERE filters and allows to set new ones
98     *
99     * @return QueryBuilderWhere
100     */
101    public function filters() {
102        return $this->where;
103    }
104
105    /**
106     * Add an ORDER BY clause
107     *
108     * @param string $sort a single sorting condition
109     */
110    public function addOrderBy($sort) {
111        $this->orderby[] = $sort;
112    }
113
114    /**
115     * Add an GROUP BY clause
116     *
117     * @param string $tablealias
118     * @param string $column
119     */
120    public function addGroupByColumn($tablealias, $column) {
121        if(!isset($this->from[$tablealias])) throw new StructException('Table Alias does not exist');
122        $this->groupby[] = "$tablealias.$column";
123    }
124
125    /**
126     * Add an GROUP BY clause
127     *
128     * Like @see addGroupByColumn but accepts an arbitrary statement
129     *
130     * @param string $statement a single grouping clause
131     */
132    public function addGroupByStatement($statement) {
133        $this->groupby[] = $statement;
134    }
135
136    /**
137     * Adds a value to the statement
138     *
139     * This function returns the name of the placeholder you have to use in your statement, so whenever
140     * you need to use a user value in a statement, call this first, then add the statement through the
141     * other functions using the returned placeholder.
142     *
143     * @param mixed $value
144     * @return string
145     */
146    public function addValue($value) {
147        static $count = 0;
148        $count++;
149
150        $placeholder = ":!!val$count!!:"; // sqlite plugin does not support named parameters, yet so we have simulate it
151        $this->values[$placeholder] = $value;
152        return $placeholder;
153    }
154
155    /**
156     * Creates a new table alias that has not been used before
157     *
158     * @return string
159     */
160    public function generateTableAlias() {
161        static $count = 0;
162        $count++;
163        return "T$count";
164    }
165
166    /**
167     * Returns the complete SQL statement and the values to apply
168     *
169     * @return array ($sql, $vals)
170     */
171    public function getSQL() {
172        // FROM needs commas only for tables, not joins
173        $from = '';
174        foreach($this->from as $alias => $statement) {
175            if($this->type[$alias] == 'table' && $from) {
176                $from .= ",\n";
177            } else {
178                $from .= "\n";
179            }
180
181            $from .= $statement;
182        }
183
184        $sql =
185            ' SELECT ' . join(",\n", $this->select) . "\n" .
186            '   FROM ' . $from . "\n" .
187            '  WHERE ' . $this->where->toSQL() . "\n";
188
189        if($this->groupby) {
190            $sql .=
191                'GROUP BY ' . join(",\n", $this->groupby) . "\n";
192        }
193
194        if($this->orderby) {
195            $sql .=
196                'ORDER BY ' . join(",\n", $this->orderby) . "\n";
197        }
198
199        return $this->fixPlaceholders($sql);
200    }
201
202    /**
203     * Replaces the named placeholders with ? placeholders
204     *
205     * Until the sqlite plugin can use named placeholder properly
206     *
207     * @param string $sql
208     * @return array
209     */
210    protected function fixPlaceholders($sql) {
211        $vals = array();
212
213        while(preg_match('/(:!!val\d+!!:)/', $sql, $m)) {
214            $pl = $m[1];
215
216            if(!isset($this->values[$pl])) throw new StructException('Placeholder not found');
217
218            $sql = preg_replace("/$pl/", '?', $sql, 1);
219            $vals[] = $this->values[$pl];
220        }
221
222        return array($sql, $vals);
223    }
224
225    /**
226     * Insert an array into another array at a given position in an associative array
227     *
228     * @param array $array The initial array
229     * @param array $pairs The array to insert
230     * @param string $key_pos The position at which to insert
231     * @link https://gist.github.com/scribu/588429 simplified
232     * @return array
233     */
234    protected function array_insert($array, $pairs, $key_pos) {
235        $result = array_slice($array, 0, $key_pos);
236        $result = array_merge($result, $pairs);
237        $result = array_merge($result, array_slice($array, $key_pos));
238        return $result;
239    }
240}
241
242