Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
138 / 138 |
|
100.00% |
22 / 22 |
CRAP | |
100.00% |
1 / 1 |
Select | |
100.00% |
138 / 138 |
|
100.00% |
22 / 22 |
58 | |
100.00% |
1 / 1 |
select | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
buildSelectSQL | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
resetSelect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
from | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
buildFromSQL | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
resetFrom | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
join | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
leftJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rightJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
applyJoin | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
buildJoinSQL | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
resetJoins | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
groupBy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
buildGroupBySQL | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
resetGroupBy | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
orderBy | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
buildOrderBySQL | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
resetOrderBy | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
sql | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
params | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reset | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
allTablesReferenced | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace BO\Zmsdb\Query\Builder; |
4 | |
5 | /** |
6 | * Select |
7 | * |
8 | * Runs a SELECT SQL query against the database. |
9 | * |
10 | * @package BO\Zmsdb\Query\Builder |
11 | * @author Alex Gisby<alex@solution10.com> |
12 | * @license MIT |
13 | */ |
14 | class Select extends Query |
15 | { |
16 | use Where; |
17 | use Having; |
18 | use Paginate; |
19 | |
20 | /** |
21 | * @var string The base part of the query |
22 | */ |
23 | protected $queryBase = 'SELECT'; |
24 | |
25 | /** |
26 | * @var array |
27 | */ |
28 | protected $selectColumns = []; |
29 | |
30 | /** |
31 | * @var array |
32 | */ |
33 | protected $fromTables = []; |
34 | |
35 | /** |
36 | * @var array |
37 | */ |
38 | protected $joins = []; |
39 | |
40 | /** |
41 | * @var array |
42 | */ |
43 | protected $groupBy = []; |
44 | |
45 | /** |
46 | * @var array |
47 | */ |
48 | protected $orderBy = []; |
49 | |
50 | /* |
51 | * ------------------ SELECT --------------------- |
52 | */ |
53 | |
54 | /** |
55 | * Set/Get the Select columns. |
56 | * |
57 | * To get, pass no arguments. |
58 | * |
59 | * To set: |
60 | * - pass either a pair of string for column, alias |
61 | * - pass an array of columns |
62 | * - pass an array of [alias => column] pairs |
63 | * |
64 | * Columns can be ExpressionInterface instances. |
65 | * |
66 | * @param string|array|null $columns |
67 | * @param string|null $alias Null to get or no alias. Alias is ignored if $columns is an array. |
68 | * @return $this|array |
69 | */ |
70 | public function select($columns = null, $alias = null) |
71 | { |
72 | if ($columns === null) { |
73 | return $this->selectColumns; |
74 | } |
75 | |
76 | if (!is_array($columns)) { |
77 | $this->selectColumns[] = [ |
78 | 'column' => $columns, |
79 | 'alias' => $alias, |
80 | ]; |
81 | } else { |
82 | foreach ($columns as $k => $v) { |
83 | $this->selectColumns[] = [ |
84 | 'column' => $v, |
85 | 'alias' => (is_numeric($k) || is_null($k)) ? null : $k, |
86 | ]; |
87 | } |
88 | } |
89 | |
90 | return $this; |
91 | } |
92 | |
93 | /** |
94 | * Builds the SQL for the SELECT component of the query |
95 | * |
96 | * @return string |
97 | */ |
98 | public function buildSelectSQL() |
99 | { |
100 | if (empty($this->selectColumns)) { |
101 | return ''; |
102 | } |
103 | |
104 | $parts = []; |
105 | foreach ($this->selectColumns as $c) { |
106 | $ret = $this->dialect->quoteField($c['column']); |
107 | $ret .= ($c['alias'] != null) ? ' AS ' . $this->dialect->quoteField($c['alias']) : ''; |
108 | $parts[] = $ret; |
109 | } |
110 | return $this->queryBase . ' ' . implode(', ', $parts); |
111 | } |
112 | |
113 | /** |
114 | * Resets the SELECT portion of this query to empty. |
115 | * |
116 | * @return $this |
117 | */ |
118 | public function resetSelect() |
119 | { |
120 | $this->selectColumns = []; |
121 | return $this; |
122 | } |
123 | |
124 | /* |
125 | * ----------------- FROM -------------------- |
126 | */ |
127 | |
128 | /** |
129 | * Get/Set Table to pull from. |
130 | * |
131 | * @param string|null $table String to set, NULL to return |
132 | * @param string|null $alias |
133 | * @return $this|string|array |
134 | */ |
135 | public function from($table = null, $alias = null) |
136 | { |
137 | if ($table === null) { |
138 | return $this->fromTables; |
139 | } |
140 | $this->fromTables[] = [ |
141 | 'table' => $table, |
142 | 'alias' => $alias |
143 | ]; |
144 | return $this; |
145 | } |
146 | |
147 | /** |
148 | * Builds the SQL for the FROM component of the query |
149 | * |
150 | * @return string |
151 | */ |
152 | public function buildFromSQL() |
153 | { |
154 | if (empty($this->fromTables)) { |
155 | return ''; |
156 | } |
157 | |
158 | $parts = []; |
159 | foreach ($this->fromTables as $f) { |
160 | $part = $this->dialect->quoteTable($f['table']); |
161 | $part .= ($f['alias'] !== null) ? ' ' . $this->dialect->quoteTable($f['alias']) : ''; |
162 | $parts[] = $part; |
163 | } |
164 | return 'FROM ' . implode(', ', $parts); |
165 | } |
166 | |
167 | /** |
168 | * Resets the FROM portion of this query to empty. |
169 | * |
170 | * @return $this |
171 | */ |
172 | public function resetFrom() |
173 | { |
174 | $this->fromTables = []; |
175 | return $this; |
176 | } |
177 | |
178 | /* |
179 | * ------------------ JOIN ----------------------- |
180 | */ |
181 | |
182 | /** |
183 | * Sets/Gets an INNER JOIN. |
184 | * |
185 | * $query->join('comments', 'users.id', '=', 'comment.user_id'); |
186 | * |
187 | * @param string|null $right Name of the table to join |
188 | * @param string|null $leftField Left part of the ON |
189 | * @param string|null $operator Operator for the ON |
190 | * @param string|null $rightField Right part of the ON |
191 | * @return $this|array $this on set, array on get |
192 | * @throws \InvalidArgumentException On unknown $type |
193 | */ |
194 | public function join($right = null, $leftField = null, $operator = null, $rightField = null) |
195 | { |
196 | return $this->applyJoin('INNER', $right, $leftField, $operator, $rightField); |
197 | } |
198 | |
199 | /** |
200 | * Sets/Gets a LEFT JOIN. |
201 | * |
202 | * $query->leftJoin('comments', 'users.id', '=', 'comment.user_id'); |
203 | * |
204 | * @param string|null $right Name of the table we're joining |
205 | * @param string|null $leftField Left part of the ON |
206 | * @param string|null $operator Operator for the ON |
207 | * @param string|null $rightField Right part of the ON |
208 | * @return $this|array $this on set, array on get |
209 | * @throws \InvalidArgumentException On unknown $type |
210 | */ |
211 | public function leftJoin($right = null, $leftField = null, $operator = null, $rightField = null) |
212 | { |
213 | return $this->applyJoin('LEFT', $right, $leftField, $operator, $rightField); |
214 | } |
215 | |
216 | /** |
217 | * Sets/Gets a LEFT JOIN. |
218 | * |
219 | * $query->leftJoin('comments', 'users.id', '=', 'comment.user_id'); |
220 | * |
221 | * @param string|null $right Name of the table we're joining |
222 | * @param string|null $leftField Left part of the ON |
223 | * @param string|null $operator Operator for the ON |
224 | * @param string|null $rightField Right part of the ON |
225 | * @return $this|array $this on set, array on get |
226 | * @throws \InvalidArgumentException On unknown $type |
227 | */ |
228 | public function rightJoin($right = null, $leftField = null, $operator = null, $rightField = null) |
229 | { |
230 | return $this->applyJoin('RIGHT', $right, $leftField, $operator, $rightField); |
231 | } |
232 | |
233 | /** |
234 | * Adds in any kind of join. Used internally, you should use join(), leftJoin() and rightJoin() |
235 | * |
236 | * @param string $type The type of join to add |
237 | * @param string|null $right Name of the right table |
238 | * @param string|null $leftField Left part of the ON |
239 | * @param string|null $operator Operator for the ON |
240 | * @param string|null $rightField Right part of the ON |
241 | * @return $this|array $this on set, array of joins for given type on get |
242 | */ |
243 | protected function applyJoin($type, $right = null, $leftField = null, $operator = null, $rightField = null) |
244 | { |
245 | // Get: |
246 | if ($right === null) { |
247 | return (array_key_exists($type, $this->joins)) ? $this->joins[$type] : []; |
248 | } |
249 | |
250 | // Set: |
251 | $this->joins[$type][] = [ |
252 | 'right' => $right, |
253 | 'leftField' => $leftField, |
254 | 'operator' => $operator, |
255 | 'rightField' => $rightField |
256 | ]; |
257 | |
258 | return $this; |
259 | } |
260 | |
261 | /** |
262 | * Builds the SQL for a JOIN statement |
263 | * |
264 | * @return string |
265 | */ |
266 | public function buildJoinSQL() |
267 | { |
268 | if (empty($this->joins)) { |
269 | return ''; |
270 | } |
271 | |
272 | $joins = []; |
273 | foreach ($this->joins as $type => $typeJoins) { |
274 | foreach ($typeJoins as $j) { |
275 | $join = ($type != 'INNER') ? $type . ' ' : ''; |
276 | $join .= 'JOIN '; |
277 | $join .= $this->dialect->quoteTable($j['right']) . ' ON '; |
278 | $join .= $this->dialect->quoteField($j['leftField']); |
279 | $join .= ' ' . $j['operator'] . ' '; |
280 | $join .= $this->dialect->quoteField($j['rightField']); |
281 | $joins[] = $join; |
282 | } |
283 | } |
284 | |
285 | return trim(implode("\n", $joins)); |
286 | } |
287 | |
288 | /** |
289 | * Resets the JOIN portion of this query to empty. |
290 | * |
291 | * @return $this |
292 | */ |
293 | public function resetJoins() |
294 | { |
295 | $this->joins = []; |
296 | return $this; |
297 | } |
298 | |
299 | /* |
300 | * ------------------ GROUP BY ------------------- |
301 | */ |
302 | |
303 | /** |
304 | * Set/Get the group by clause |
305 | * |
306 | * @param string|array|null $clause String or array to set, null to get |
307 | * @return $this|string|array |
308 | */ |
309 | public function groupBy($clause = null) |
310 | { |
311 | if ($clause === null) { |
312 | return $this->groupBy; |
313 | } |
314 | |
315 | $clause = (is_array($clause)) ? $clause : [$clause]; |
316 | $this->groupBy = array_merge($this->groupBy, $clause); |
317 | |
318 | return $this; |
319 | } |
320 | |
321 | /** |
322 | * Builds the SQL for a GROUP BY statement |
323 | * |
324 | * @return string |
325 | */ |
326 | public function buildGroupBySQL() |
327 | { |
328 | if (empty($this->groupBy)) { |
329 | return ''; |
330 | } |
331 | |
332 | $parts = []; |
333 | foreach ($this->groupBy as $p) { |
334 | $parts[] = $this->dialect->quoteField($p); |
335 | } |
336 | |
337 | return 'GROUP BY ' . implode(', ', $parts); |
338 | } |
339 | |
340 | /** |
341 | * Resets the GROUP BY portion of this query to empty. |
342 | * |
343 | * @return $this |
344 | */ |
345 | public function resetGroupBy() |
346 | { |
347 | $this->groupBy = []; |
348 | return $this; |
349 | } |
350 | |
351 | /* |
352 | * ---------------- ORDER BY ---------------------- |
353 | */ |
354 | |
355 | /** |
356 | * Set/Get the ORDER BY component of the query. |
357 | * |
358 | * @param string|array|null $field Field name or an array of field => direction. Null to get |
359 | * @param string|null $direction ASC by default |
360 | * @return $this|array |
361 | */ |
362 | public function orderBy($field = null, $direction = 'ASC') |
363 | { |
364 | if ($field === null) { |
365 | return $this->orderBy; |
366 | } |
367 | |
368 | if (is_array($field)) { |
369 | foreach ($field as $f => $d) { |
370 | $this->orderBy[] = [ |
371 | 'field' => $f, |
372 | 'direction' => $d, |
373 | ]; |
374 | } |
375 | } else { |
376 | $this->orderBy[] = [ |
377 | 'field' => $field, |
378 | 'direction' => ($field instanceof ExpressionInterface) ? null : $direction |
379 | ]; |
380 | } |
381 | |
382 | return $this; |
383 | } |
384 | |
385 | /** |
386 | * Builds the SQL for the ORDER BY part of the query |
387 | * |
388 | * @return string |
389 | */ |
390 | public function buildOrderBySQL() |
391 | { |
392 | if (empty($this->orderBy)) { |
393 | return ''; |
394 | } |
395 | |
396 | $parts = []; |
397 | foreach ($this->orderBy as $order) { |
398 | $part = $this->dialect->quoteField($order['field']); |
399 | $part .= ($order['direction'] != null) ? ' ' . $order['direction'] : ''; |
400 | $parts[] = $part; |
401 | } |
402 | |
403 | return 'ORDER BY ' . implode(', ', $parts); |
404 | } |
405 | |
406 | /** |
407 | * Resets the ORDER BY portion of this query to empty. |
408 | * |
409 | * @return $this |
410 | */ |
411 | public function resetOrderBy() |
412 | { |
413 | $this->orderBy = []; |
414 | return $this; |
415 | } |
416 | |
417 | /* |
418 | * --------------- Generating SQL -------------------- |
419 | */ |
420 | |
421 | /** |
422 | * Generates the full SQL statement for this query with all the composite parts. |
423 | * Note: there is no guarantee that this will be valid SQL! Obviously the parts |
424 | * you've given will come out good, but if you forget to add a FROM or something, |
425 | * this class won't automatically guess one for you! |
426 | * |
427 | * @return string |
428 | */ |
429 | public function sql() |
430 | { |
431 | $candidateParts = [ |
432 | $this->buildSelectSQL(), |
433 | $this->buildFromSQL(), |
434 | $this->buildJoinSQL(), |
435 | $this->buildWhereSQL($this->dialect), |
436 | $this->buildGroupBySQL(), |
437 | $this->buildHavingSQL($this->dialect), |
438 | $this->buildOrderBySQL(), |
439 | $this->buildPaginateSQL() |
440 | ]; |
441 | |
442 | $realParts = []; |
443 | foreach ($candidateParts as $p) { |
444 | if ($p != '') { |
445 | $realParts[] = $p; |
446 | } |
447 | } |
448 | |
449 | return implode(" ", $realParts); |
450 | } |
451 | |
452 | /** |
453 | * Returns all the parameters, in the correct order, to pass into PDO. |
454 | * |
455 | * @return array |
456 | */ |
457 | public function params() |
458 | { |
459 | return array_merge($this->getWhereParams(), $this->getHavingParams()); |
460 | } |
461 | |
462 | /** |
463 | * Resets the entire query by calling each sections resetXXX() function. |
464 | * |
465 | * @return $this |
466 | */ |
467 | public function reset() |
468 | { |
469 | return $this |
470 | ->resetSelect() |
471 | ->resetFrom() |
472 | ->resetJoins() |
473 | ->resetWhere() |
474 | ->resetGroupBy() |
475 | ->resetHaving() |
476 | ->resetOrderBy() |
477 | ->resetLimit() |
478 | ->resetOffset(); |
479 | } |
480 | |
481 | /* |
482 | * ------------------- All Tables ------------------------ |
483 | */ |
484 | |
485 | /** |
486 | * Returns all the tables that this query makes mention of, in FROMs and JOINs |
487 | * |
488 | * @return array |
489 | */ |
490 | public function allTablesReferenced() |
491 | { |
492 | $tables = []; |
493 | |
494 | // Deal with FROMs: |
495 | if ($this->fromTables) { |
496 | foreach ($this->fromTables as $tbl) { |
497 | $tables[] = $tbl['table']; |
498 | } |
499 | } |
500 | |
501 | // And now JOINs: |
502 | if ($this->joins) { |
503 | foreach (array_keys($this->joins) as $type) { |
504 | foreach ($this->joins[$type] as $join) { |
505 | $tables[] = $join['right']; |
506 | } |
507 | } |
508 | } |
509 | |
510 | return array_unique($tables); |
511 | } |
512 | } |