Completed
Push — develop ( 111dce...b8d476 )
by Timothy
02:35
created

AbstractQueryBuilder::_where_string()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nc 7
nop 3
dl 0
loc 35
rs 8.439
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/**
3
 * Query
4
 *
5
 * SQL Query Builder / Database Abstraction Layer
6
 *
7
 * PHP version 7
8
 *
9
 * @package     Query
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2012 - 2016 Timothy J. Warren
12
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
 * @link        https://git.timshomepage.net/aviat4ion/Query
14
 */
15
16
namespace Query;
17
18
use PDOStatement;
19
20
/**
21
 * Abstract Class for internal implementation methods of the Query Builder
22
 * @package Query
23
 */
24
abstract class AbstractQueryBuilder {
25
26
	// --------------------------------------------------------------------------
27
	// ! Constants
28
	// --------------------------------------------------------------------------
29
30
	const KEY 	= 0;
31
	const VALUE = 1;
32
	const BOTH 	= 2;
33
34
	// --------------------------------------------------------------------------
35
	// ! SQL Clause Strings
36
	// --------------------------------------------------------------------------
37
38
	/**
39
	 * Compiled 'select' clause
40
	 * @var string
41
	 */
42
	protected $selectString = '';
43
44
	/**
45
	 * Compiled 'from' clause
46
	 * @var string
47
	 */
48
	protected $fromString = '';
49
50
	/**
51
	 * Compiled arguments for insert / update
52
	 * @var string
53
	 */
54
	protected $setString;
55
56
	/**
57
	 * Order by clause
58
	 * @var string
59
	 */
60
	protected $orderString;
61
62
	/**
63
	 * Group by clause
64
	 * @var string
65
	 */
66
	protected $groupString;
67
68
	// --------------------------------------------------------------------------
69
	// ! SQL Clause Arrays
70
	// --------------------------------------------------------------------------
71
72
	/**
73
	 * Keys for insert/update statement
74
	 * @var array
75
	 */
76
	protected $setArrayKeys = [];
77
78
	/**
79
	 * Key/val pairs for order by clause
80
	 * @var array
81
	 */
82
	protected $orderArray = [];
83
84
	/**
85
	 * Key/val pairs for group by clause
86
	 * @var array
87
	 */
88
	protected $groupArray = [];
89
90
	// --------------------------------------------------------------------------
91
	// ! Other Class vars
92
	// --------------------------------------------------------------------------
93
94
	/**
95
	 * Values to apply to prepared statements
96
	 * @var array
97
	 */
98
	protected $values = [];
99
100
	/**
101
	 * Values to apply to where clauses in prepared statements
102
	 * @var array
103
	 */
104
	protected $whereValues = [];
105
106
	/**
107
	 * Value for limit string
108
	 * @var string
109
	 */
110
	protected $limit;
111
112
	/**
113
	 * Value for offset in limit string
114
	 * @var integer
115
	 */
116
	protected $offset;
117
118
	/**
119
	 * Query component order mapping
120
	 * for complex select queries
121
	 *
122
	 * Format:
123
	 * array(
124
	 *		'type' => 'where',
125
	 *		'conjunction' => ' AND ',
126
	 *		'string' => 'k=?'
127
	 * )
128
	 *
129
	 * @var array
130
	 */
131
	protected $queryMap = [];
132
133
	/**
134
	 * Map for having clause
135
	 * @var array
136
	 */
137
	protected $havingMap;
138
139
	/**
140
	 * Convenience property for connection management
141
	 * @var string
142
	 */
143
	public $connName = "";
144
145
	/**
146
	 * List of queries executed
147
	 * @var array
148
	 */
149
	public $queries;
150
151
	/**
152
	 * Whether to do only an explain on the query
153
	 * @var boolean
154
	 */
155
	protected $explain;
156
157
	/**
158
	 * The current database driver
159
	 * @var \Query\Drivers\DriverInterface
160
	 */
161
	public $db;
162
163
	/**
164
	 * Query parser class instance
165
	 * @var QueryParser
166
	 */
167
	public $parser;
168
169
	/**
170
	 * Alias to driver util class
171
	 * @var \Query\Drivers\AbstractUtil
172
	 */
173
	public $util;
174
175
	/**
176
	 * Alias to driver sql class
177
	 * @var \Query\Drivers\SQLInterface
178
	 */
179
	public $sql;
180
181
	// --------------------------------------------------------------------------
182
	// Methods
183
	// --------------------------------------------------------------------------
184
185
	/**
186
	 * Set values in the class, with either an array or key value pair
187
	 *
188
	 * @param array $var
189
	 * @param mixed $key
190
	 * @param mixed $val
191
	 * @param int $valType
192
	 * @return array
193
	 */
194
	protected function _mixedSet(array &$var, $key, $val=NULL, int $valType=self::BOTH): array
195
	{
196
		$arg = (is_scalar($key) && is_scalar($val))
197
			? [$key => $val]
198
			: $key;
199
200
		foreach($arg as $k => $v)
201
		{
202
			if (in_array($valType, [self::KEY, self::VALUE]))
203
			{
204
				$var[] = ($valType === self::KEY)
205
					? $k
206
					: $v;
207
			}
208
			else
209
			{
210
				$var[$k] = $v;
211
			}
212
		}
213
214
		return $var;
215
	}
216
217
	/**
218
	 * Method to simplify select_ methods
219
	 *
220
	 * @param string $field
221
	 * @param string|bool $as
222
	 * @return string
223
	 */
224
	protected function _select(string $field, $as = FALSE): string
225
	{
226
		// Escape the identifiers
227
		$field = $this->db->quoteIdent($field);
228
229
		if ( ! is_string($as))
230
		{
231
			return $field;
232
		}
233
234
		$as = $this->db->quoteIdent($as);
235
		return "({$field}) AS {$as} ";
236
	}
237
238
	/**
239
	 * Helper function for returning sql strings
240
	 *
241
	 * @param string $type
242
	 * @param string $table
243
	 * @param bool $reset
244
	 * @return string
245
	 */
246
	protected function _getCompile(string $type, string $table, bool $reset): string
247
	{
248
		$sql = $this->_compile($type, $table);
249
250
		// Reset the query builder for the next query
251
		if ($reset)
252
		{
253
			$this->resetQuery();
254
		}
255
256
		return $sql;
257
	}
258
259
	/**
260
	 * Simplify 'like' methods
261
	 *
262
	 * @param string $field
263
	 * @param mixed $val
264
	 * @param string $pos
265
	 * @param string $like
266
	 * @param string $conj
267
	 * @return QueryBuilderInterface
268
	 */
269
	protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): QueryBuilderInterface
270
	{
271
		$field = $this->db->quoteIdent($field);
272
273
		// Add the like string into the order map
274
		$like = $field. " {$like} ?";
275
276
		if ($pos == 'before')
277
		{
278
			$val = "%{$val}";
279
		}
280
		elseif ($pos == 'after')
281
		{
282
			$val = "{$val}%";
283
		}
284
		else
285
		{
286
			$val = "%{$val}%";
287
		}
288
289
		$conj = (empty($this->queryMap)) ? ' WHERE ' : " {$conj} ";
290
		$this->_appendMap($conj, $like, 'like');
291
292
		// Add to the values array
293
		$this->whereValues[] = $val;
294
295
		return $this;
296
	}
297
298
	/**
299
	 * Simplify building having clauses
300
	 *
301
	 * @param mixed $key
302
	 * @param mixed $val
303
	 * @param string $conj
304
	 * @return QueryBuilderInterface
305
	 */
306
	protected function _having($key, $val=[], string $conj='AND'): QueryBuilderInterface
307
	{
308
		$where = $this->_where($key, $val);
309
310
		// Create key/value placeholders
311
		foreach($where as $f => $val)
312
		{
313
			// Split each key by spaces, in case there
314
			// is an operator such as >, <, !=, etc.
315
			$fArray = explode(' ', trim($f));
316
317
			$item = $this->db->quoteIdent($fArray[0]);
318
319
			// Simple key value, or an operator
320
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
321
322
			// Put in the having map
323
			$this->havingMap[] = [
324
				'conjunction' => ( ! empty($this->havingMap)) ? " {$conj} " : ' HAVING ',
325
				'string' => $item
326
			];
327
		}
328
329
		return $this;
330
	}
331
332
	/**
333
	 * Do all the redundant stuff for where/having type methods
334
	 *
335
	 * @param mixed $key
336
	 * @param mixed $val
337
	 * @return array
338
	 */
339
	protected function _where($key, $val=[]): array
340
	{
341
		$where = [];
342
		$this->_mixedSet($where, $key, $val, self::BOTH);
343
		$this->_mixedSet($this->whereValues, $key, $val, self::VALUE);
344
		return $where;
345
	}
346
347
	/**
348
	 * Simplify generating where string
349
	 *
350
	 * @param mixed $key
351
	 * @param mixed $val
352
	 * @param string $defaultConj
353
	 * @return QueryBuilderInterface
354
	 */
355
	protected function _whereString($key, $val=[], string $defaultConj='AND'): QueryBuilderInterface
356
	{
357
		// Create key/value placeholders
358
		foreach($this->_where($key, $val) as $f => $val)
359
		{
360
			// Split each key by spaces, in case there
361
			// is an operator such as >, <, !=, etc.
362
			$fArray = explode(' ', trim($f));
363
364
			$item = $this->db->quoteIdent($fArray[0]);
365
366
			// Simple key value, or an operator
367
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
368
			$lastItem = end($this->queryMap);
369
370
			// Determine the correct conjunction
371
			$conjunctionList = array_column($this->queryMap, 'conjunction');
372
			if (empty($this->queryMap) || ( ! regex_in_array($conjunctionList, "/^ ?\n?WHERE/i")))
373
			{
374
				$conj = "\nWHERE ";
375
			}
376
			elseif ($lastItem['type'] === 'group_start')
377
			{
378
				$conj = '';
379
			}
380
			else
381
			{
382
				$conj = " {$defaultConj} ";
383
			}
384
385
			$this->_appendMap($conj, $item, 'where');
386
		}
387
388
		return $this;
389
	}
390
391
	/**
392
	 * Simplify where_in methods
393
	 *
394
	 * @param mixed $key
395
	 * @param mixed $val
396
	 * @param string $in - The (not) in fragment
397
	 * @param string $conj - The where in conjunction
398
	 * @return QueryBuilderInterface
399
	 */
400
	protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): QueryBuilderInterface
401
	{
402
		$key = $this->db->quoteIdent($key);
403
		$params = array_fill(0, count($val), '?');
404
405
		foreach($val as $v)
406
		{
407
			$this->whereValues[] = $v;
408
		}
409
410
		$conjunction = ( ! empty($this->queryMap)) ? " {$conj} " : ' WHERE ';
411
		$str = $key . " {$in} (".implode(',', $params).') ';
412
413
		$this->_appendMap($conjunction, $str, 'where_in');
414
415
		return $this;
416
	}
417
418
	/**
419
	 * Executes the compiled query
420
	 *
421
	 * @param string $type
422
	 * @param string $table
423
	 * @param string $sql
424
	 * @param array|null $vals
425
	 * @param boolean $reset
426
	 * @return PDOStatement
427
	 */
428
	protected function _run(string $type, string $table, $sql=NULL, $vals=NULL, bool $reset=TRUE): PDOStatement
429
	{
430
		if (is_null($sql))
431
		{
432
			$sql = $this->_compile($type, $table);
433
		}
434
435
		if (is_null($vals))
436
		{
437
			$vals = array_merge($this->values, (array) $this->whereValues);
438
		}
439
440
		$startTime = microtime(TRUE);
441
442
		$res = (empty($vals))
443
			? $this->db->query($sql)
0 ignored issues
show
Unused Code introduced by
The call to DriverInterface::query() has too many arguments starting with $sql.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
444
			: $this->db->prepareExecute($sql, $vals);
445
446
		$endTime = microtime(TRUE);
447
		$totalTime = number_format($endTime - $startTime, 5);
448
449
		// Add this query to the list of executed queries
450
		$this->_appendQuery($vals, $sql, (int) $totalTime);
451
452
		// Reset class state for next query
453
		if ($reset)
454
		{
455
			$this->resetQuery();
456
		}
457
458
		return $res;
459
	}
460
461
	/**
462
	 * Add an additional set of mapping pairs to a internal map
463
	 *
464
	 * @param string $conjunction
465
	 * @param string $string
466
	 * @param string $type
467
	 * @return void
468
	 */
469
	protected function _appendMap(string $conjunction = '', string $string = '', string $type = '')
470
	{
471
		array_push($this->queryMap, [
472
			'type' => $type,
473
			'conjunction' => $conjunction,
474
			'string' => $string
475
		]);
476
	}
477
478
	/**
479
	 * Convert the prepared statement into readable sql
480
	 *
481
	 * @param array $vals
482
	 * @param string $sql
483
	 * @param int $totalTime
484
	 * @return void
485
	 */
486
	protected function _appendQuery($vals, string $sql, int $totalTime)
487
	{
488
		$evals = (is_array($vals)) ? $vals : [];
489
		$esql = str_replace('?', "%s", $sql);
490
491
		// Quote string values
492
		foreach($evals as &$v)
493
		{
494
			$v = ( ! is_numeric($v)) ? htmlentities($this->db->quote($v), ENT_NOQUOTES, 'utf-8')  : $v;
495
		}
496
497
		// Add the query onto the array of values to pass
498
		// as arguments to sprintf
499
		array_unshift($evals, $esql);
500
501
		// Add the interpreted query to the list of executed queries
502
		$this->queries[] = [
503
			'time' => $totalTime,
504
			'sql' => call_user_func_array('sprintf', $evals),
505
		];
506
507
		$this->queries['total_time'] += $totalTime;
508
509
		// Set the last query to get rowcounts properly
510
		$this->db->setLastQuery($sql);
511
	}
512
513
	/**
514
	 * Sub-method for generating sql strings
515
	 *
516
	 * @param string $type
517
	 * @param string $table
518
	 * @return string
519
	 */
520
	protected function _compileType(string $type='', string $table=''): string
521
	{
522
		switch($type)
523
		{
524
			case "insert":
525
				$paramCount = count($this->setArrayKeys);
526
				$params = array_fill(0, $paramCount, '?');
527
				$sql = "INSERT INTO {$table} ("
528
					. implode(',', $this->setArrayKeys)
529
					. ")\nVALUES (".implode(',', $params).')';
530
			break;
531
532
			case "update":
533
				$sql = "UPDATE {$table}\nSET {$this->setString}";
534
			break;
535
536
			case "replace":
537
				// @TODO implement
538
				$sql = "";
539
			break;
540
541
			case "delete":
542
				$sql = "DELETE FROM {$table}";
543
			break;
544
545
			// Get queries
546
			default:
547
				$sql = "SELECT * \nFROM {$this->fromString}";
548
549
				// Set the select string
550
				if ( ! empty($this->selectString))
551
				{
552
					// Replace the star with the selected fields
553
					$sql = str_replace('*', $this->selectString, $sql);
554
				}
555
			break;
556
		}
557
558
		return $sql;
559
	}
560
561
	/**
562
	 * String together the sql statements for sending to the db
563
	 *
564
	 * @param string $type
565
	 * @param string $table
566
	 * @return string
567
	 */
568
	protected function _compile(string $type='', string $table=''): string
569
	{
570
		// Get the base clause for the query
571
		$sql = $this->_compileType($type, $this->db->quoteTable($table));
0 ignored issues
show
Bug introduced by
It seems like $this->db->quoteTable($table) targeting Query\Drivers\DriverInterface::quoteTable() can also be of type array; however, Query\AbstractQueryBuilder::_compileType() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
572
573
		$clauses = [
574
			'queryMap',
575
			'groupString',
576
			'orderString',
577
			'havingMap',
578
		];
579
580
		// Set each type of subclause
581
		foreach($clauses as $clause)
582
		{
583
			$param = $this->$clause;
584
			if (is_array($param))
585
			{
586
				foreach($param as $q)
587
				{
588
					$sql .= $q['conjunction'] . $q['string'];
589
				}
590
			}
591
			else
592
			{
593
				$sql .= $param;
594
			}
595
		}
596
597
		// Set the limit via the class variables
598
		if (is_numeric($this->limit))
599
		{
600
			$sql = $this->sql->limit($sql, $this->limit, $this->offset);
601
		}
602
603
		// See if the query plan, rather than the
604
		// query data should be returned
605
		if ($this->explain === TRUE)
606
		{
607
			$sql = $this->sql->explain($sql);
608
		}
609
610
		return $sql;
611
	}
612
}