1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of Yolk - Gamer Network's PHP Framework. |
4
|
|
|
* |
5
|
|
|
* Copyright (c) 2015 Gamer Network Ltd. |
6
|
|
|
* |
7
|
|
|
* Distributed under the MIT License, a copy of which is available in the |
8
|
|
|
* LICENSE file that was bundled with this package, or online at: |
9
|
|
|
* https://github.com/gamernetwork/yolk-database |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace yolk\database\query; |
13
|
|
|
|
14
|
|
|
use yolk\contracts\database\DatabaseConnection; |
15
|
|
|
use yolk\contracts\database\Query; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Generic. |
19
|
|
|
*/ |
20
|
|
|
abstract class BaseQuery implements Query { |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Database connection the query is associated with. |
24
|
|
|
* @var DatabaseConnection |
25
|
|
|
*/ |
26
|
|
|
protected $db; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Array of join clauses. |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $joins; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Array of where clauses. |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
protected $where; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Array of order by clauses. |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
protected $order; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Query offset. |
48
|
|
|
* @var integer |
49
|
|
|
*/ |
50
|
|
|
protected $offset; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Query limit. |
54
|
|
|
* @var integer|null |
55
|
|
|
*/ |
56
|
|
|
protected $limit; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Array of query parameters |
60
|
|
|
* @var array |
61
|
|
|
*/ |
62
|
|
|
protected $params; |
63
|
|
|
|
64
|
|
|
public function __construct( DatabaseConnection $db ) { |
65
|
|
|
$this->db = $db; |
66
|
|
|
$this->joins = []; |
67
|
|
|
$this->where = []; |
68
|
|
|
$this->order = []; |
69
|
|
|
$this->offset = 0; |
70
|
|
|
$this->limit = null; |
71
|
|
|
$this->params = []; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
public function __toString() { |
75
|
|
|
return implode("\n", $this->compile()); |
76
|
|
|
return d(implode("\n", $this->compile()), $this->params); |
|
|
|
|
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function innerJoin( $table, array $on ) { |
80
|
|
|
$this->joins[] = ['INNER', $table, $on]; |
81
|
|
|
return $this; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
public function leftJoin( $table, array $on ) { |
85
|
|
|
$this->joins[] = ['LEFT', $table, $on]; |
86
|
|
|
return $this; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
public function joinRaw( $sql, $parameters = [] ) { |
90
|
|
|
$this->joins[] = $sql; |
91
|
|
|
$this->params = array_merge($this->params, $parameters); |
92
|
|
|
return $this; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
public function where( $column, $operator, $value = null ) { |
96
|
|
|
|
97
|
|
|
// shortcut for equals |
98
|
|
|
if( func_num_args() == 2 ) { |
99
|
|
|
$value = $operator; |
100
|
|
|
$operator = '='; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
$operator = trim(strtoupper($operator)); |
104
|
|
|
|
105
|
|
|
// can't bind IN values as parameters so we escape them and embed them directly |
106
|
|
|
if( in_array($operator, ['IN', 'NOT IN']) && is_array($value) ) { |
107
|
|
|
$value = $this->makeInClause($value); |
108
|
|
|
} |
109
|
|
|
// do parameter binding |
110
|
|
|
else { |
111
|
|
|
$value = $this->bindParam( |
112
|
|
|
$this->getParameterName($column, $operator), |
113
|
|
|
$value |
114
|
|
|
); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$this->where[] = [$this->quoteIdentifier($column), $operator, $value]; |
118
|
|
|
|
119
|
|
|
return $this; |
120
|
|
|
|
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
public function whereArray( array $where ) { |
124
|
|
|
|
125
|
|
|
foreach( $where as $k => $v ) { |
126
|
|
|
$operator = is_array($v) ? 'IN' : '='; |
127
|
|
|
$this->where($k, $operator, $v); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return $this; |
131
|
|
|
|
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
public function whereRaw( $sql, $parameters = [] ) { |
135
|
|
|
$this->where[] = $sql; |
136
|
|
|
$this->params = array_merge($this->params, $parameters); |
137
|
|
|
return $this; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
public function orderBy( $column, $ascending = true ) { |
141
|
|
|
$column = $this->quoteIdentifier($column); |
142
|
|
|
$this->order[$column] = (bool) $ascending ? 'ASC' : 'DESC'; |
143
|
|
|
return $this; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
public function offset( $offset ) { |
147
|
|
|
$this->offset = max(0, (int) $offset); |
148
|
|
|
return $this; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
public function limit( $limit ) { |
152
|
|
|
$this->limit = max(1, (int) $limit); |
153
|
|
|
return $this; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
public function getParameters() { |
157
|
|
|
return $this->params; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
View Code Duplication |
public function setParameters( array $params, $replace = false ) { |
|
|
|
|
161
|
|
|
if( $replace ) |
162
|
|
|
$this->params = $params; |
163
|
|
|
else |
164
|
|
|
$this->params = array_merge($this->params, $params); |
165
|
|
|
return $this; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Generate a SQL string as an array. |
170
|
|
|
* @return array |
171
|
|
|
*/ |
172
|
|
|
abstract protected function compile(); |
173
|
|
|
|
174
|
|
|
protected function compileJoins() { |
175
|
|
|
|
176
|
|
|
$sql = []; |
177
|
|
|
|
178
|
|
|
foreach( $this->joins as $join ) { |
179
|
|
|
if( is_array($join) ) { |
180
|
|
|
list($type, $table, $on) = $join; |
181
|
|
|
$join = sprintf("%s JOIN %s\nON %s", $type, $this->quoteIdentifier($table), $this->compileOn($on)); |
182
|
|
|
} |
183
|
|
|
$sql[] = $join; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
return $sql; |
187
|
|
|
|
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
protected function compileOn( array $on ) { |
191
|
|
|
|
192
|
|
|
$sql = []; |
193
|
|
|
|
194
|
|
|
foreach( $on as $column => $value ) { |
195
|
|
|
// if it's not a number or a quoted sring it much be an identifier, so quote it |
196
|
|
|
if( !is_numeric($value) && !preg_match('/^\'.*\'$/', $value) ) |
197
|
|
|
$value = $this->quoteIdentifier($value); |
198
|
|
|
$sql[] = sprintf("%s = %s", $this->quoteIdentifier($column), $value); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
return implode("\nAND ", $sql); |
202
|
|
|
|
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
protected function compileWhere() { |
206
|
|
|
|
207
|
|
|
$sql = []; |
208
|
|
|
|
209
|
|
|
foreach( $this->where as $i => $clause ) { |
210
|
|
|
if( is_array($clause) ) |
211
|
|
|
$clause = implode(' ', $clause); |
212
|
|
|
$sql[] = ($i ? 'AND ' : 'WHERE '). $clause; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
return $sql; |
216
|
|
|
|
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
protected function compileOrderBy() { |
220
|
|
|
|
221
|
|
|
$sql = []; |
222
|
|
|
|
223
|
|
|
if( $this->order ) { |
|
|
|
|
224
|
|
|
$order = 'ORDER BY '; |
225
|
|
|
foreach( $this->order as $column => $dir ) { |
226
|
|
|
$order .= $column. ' '. $dir. ', '; |
227
|
|
|
} |
228
|
|
|
$sql[] = trim($order, ', '); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
return $sql; |
232
|
|
|
|
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
protected function compileOffsetLimit() { |
236
|
|
|
|
237
|
|
|
$sql = []; |
238
|
|
|
|
239
|
|
|
$limit = $this->limit; |
240
|
|
|
$offset = $this->offset; |
241
|
|
|
|
242
|
|
|
if( $limit || $offset ) { |
|
|
|
|
243
|
|
|
|
244
|
|
|
if( !$limit ) |
|
|
|
|
245
|
|
|
$limit = PHP_INT_MAX; |
246
|
|
|
|
247
|
|
|
$sql[] = sprintf( |
248
|
|
|
"LIMIT %s OFFSET %s", |
249
|
|
|
$this->bindParam('_limit', $limit), |
250
|
|
|
$this->bindParam('_offset', $offset) |
251
|
|
|
); |
252
|
|
|
|
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $sql; |
256
|
|
|
|
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
protected function quoteIdentifier( $spec ) { |
260
|
|
|
|
261
|
|
|
// don't quote things that are functions/expressions |
262
|
|
|
if( strpos($spec, '(') !== false ) |
263
|
|
|
return $spec; |
264
|
|
|
|
265
|
|
|
foreach( [' AS ', ' ', '.'] as $sep) { |
266
|
|
|
if( $pos = strripos($spec, $sep) ) { |
267
|
|
|
return |
268
|
|
|
$this->quoteIdentifier(substr($spec, 0, $pos)). |
269
|
|
|
$sep. |
270
|
|
|
$this->db->quoteIdentifier(substr($spec, $pos + strlen($sep))); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
return $this->db->quoteIdentifier($spec); |
275
|
|
|
|
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Join an array of values to form a string suitable for use in a SQL IN clause. |
280
|
|
|
* The numeric parameter determines whether values are escaped and quoted; |
281
|
|
|
* a null value (the default) will cause the function to auto-detect whether |
282
|
|
|
* values should be escaped and quoted. |
283
|
|
|
* |
284
|
|
|
* @param array $values |
285
|
|
|
* @param null|boolean $numeric |
286
|
|
|
* @return string |
287
|
|
|
*/ |
288
|
|
|
protected function makeInClause( array $values, $numeric = null ) { |
289
|
|
|
|
290
|
|
|
// if numeric flag wasn't specified then detected it |
291
|
|
|
// by checking all items in the array are numeric |
292
|
|
|
if( $numeric === null ) { |
293
|
|
|
$numeric = count(array_filter($values, 'is_numeric')) == count($values); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// not numeric so we need to escape all the values |
297
|
|
|
if( !$numeric ) { |
298
|
|
|
$values = array_map([$this->db, 'quote'], $values); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return sprintf('(%s)', implode(', ', $values)); |
302
|
|
|
|
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
protected function getParameterName( $column, $operator ) { |
306
|
|
|
|
307
|
|
|
$suffixes = [ |
308
|
|
|
'=' => 'eq', |
309
|
|
|
'!=' => 'neq', |
310
|
|
|
'<>' => 'neq', |
311
|
|
|
'<' => 'max', |
312
|
|
|
'<=' => 'max', |
313
|
|
|
'>' => 'min', |
314
|
|
|
'>=' => 'min', |
315
|
|
|
'LIKE' => 'like', |
316
|
|
|
'NOT LIKE' => 'notlike', |
317
|
|
|
]; |
318
|
|
|
|
319
|
|
|
$name = $column; |
320
|
|
|
|
321
|
|
|
// strip the table identifier |
322
|
|
|
if( $pos = strpos($name, '.') ) |
323
|
|
|
$name = substr($name, $pos + 1); |
324
|
|
|
|
325
|
|
|
if( isset($suffixes[$operator]) ) |
326
|
|
|
$name .= '_'. $suffixes[$operator]; |
327
|
|
|
|
328
|
|
|
return $name; |
329
|
|
|
|
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Add a parameter and return the placeholder to be inserted into the query string. |
334
|
|
|
* @param string $name |
335
|
|
|
* @param mixed $value |
336
|
|
|
* @return string |
337
|
|
|
*/ |
338
|
|
|
protected function bindParam( $name, $value ) { |
339
|
|
|
|
340
|
|
|
if( isset($this->params[$name]) ) |
341
|
|
|
throw new \LogicException("Parameter: {$name} has already been defined"); |
342
|
|
|
|
343
|
|
|
$this->params[$name] = $value; |
344
|
|
|
|
345
|
|
|
return ":{$name}"; |
346
|
|
|
|
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
// EOF |
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.