Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
138 / 138
100.00% covered (success)
100.00%
22 / 22
CRAP
100.00% covered (success)
100.00%
1 / 1
Select
100.00% covered (success)
100.00%
138 / 138
100.00% covered (success)
100.00%
22 / 22
58
100.00% covered (success)
100.00%
1 / 1
 select
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 buildSelectSQL
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resetSelect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 from
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 buildFromSQL
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resetFrom
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 join
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 leftJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rightJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 applyJoin
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 buildJoinSQL
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 resetJoins
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 groupBy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 buildGroupBySQL
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 resetGroupBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orderBy
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 buildOrderBySQL
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resetOrderBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sql
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 params
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reset
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 allTablesReferenced
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace 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 */
14class 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}