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