Completed
Push — develop ( 2db7ad...3eb4d8 )
by Timothy
10:51
created

src/Query/AbstractQueryBuilder.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Query
4
 *
5
 * SQL Query Builder / Database Abstraction Layer
6
 *
7
 * PHP version 5.4
8
 *
9
 * @package     Query
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2012 - 2015 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
/**
19
 * Abstract Class for internal implementation methods of the Query Builder
20
 * @package Query
21
 */
22
abstract class AbstractQueryBuilder {
23
24
	// --------------------------------------------------------------------------
25
	// ! Constants
26
	// --------------------------------------------------------------------------
27
28
	const KEY 	= 0;
29
	const VALUE = 1;
30
	const BOTH 	= 2;
31
32
33
	// --------------------------------------------------------------------------
34
	// ! SQL Clause Strings
35
	// --------------------------------------------------------------------------
36
37
	/**
38
	 * Compiled 'select' clause
39
	 * @var string
40
	 */
41
	protected $select_string = '';
42
43
	/**
44
	 * Compiled 'from' clause
45
	 * @var string
46
	 */
47
	protected $from_string;
48
49
	/**
50
	 * Compiled arguments for insert / update
51
	 * @var string
52
	 */
53
	protected $set_string;
54
55
	/**
56
	 * Order by clause
57
	 * @var string
58
	 */
59
	protected $order_string;
60
61
	/**
62
	 * Group by clause
63
	 * @var string
64
	 */
65
	protected $group_string;
66
67
	// --------------------------------------------------------------------------
68
	// ! SQL Clause Arrays
69
	// --------------------------------------------------------------------------
70
71
	/**
72
	 * Keys for insert/update statement
73
	 * @var array
74
	 */
75
	protected $set_array_keys = [];
76
77
	/**
78
	 * Key/val pairs for order by clause
79
	 * @var array
80
	 */
81
	protected $order_array = [];
82
83
	/**
84
	 * Key/val pairs for group by clause
85
	 * @var array
86
	 */
87
	protected $group_array = [];
88
89
	// --------------------------------------------------------------------------
90
	// ! Other Class vars
91
	// --------------------------------------------------------------------------
92
93
	/**
94
	 * Values to apply to prepared statements
95
	 * @var array
96
	 */
97
	protected $values = [];
98
99
	/**
100
	 * Values to apply to where clauses in prepared statements
101
	 * @var array
102
	 */
103
	protected $where_values = [];
104
105
	/**
106
	 * Value for limit string
107
	 * @var string
108
	 */
109
	protected $limit;
110
111
	/**
112
	 * Value for offset in limit string
113
	 * @var integer
114
	 */
115
	protected $offset;
116
117
	/**
118
	 * Query component order mapping
119
	 * for complex select queries
120
	 *
121
	 * Format:
122
	 * array(
123
	 *		'type' => 'where',
124
	 *		'conjunction' => ' AND ',
125
	 *		'string' => 'k=?'
126
	 * )
127
	 *
128
	 * @var array
129
	 */
130
	protected $query_map = [];
131
132
	/**
133
	 * Map for having clause
134
	 * @var array
135
	 */
136
	protected $having_map;
137
138
	/**
139
	 * Convenience property for connection management
140
	 * @var string
141
	 */
142
	public $conn_name = "";
143
144
	/**
145
	 * List of queries executed
146
	 * @var array
147
	 */
148
	public $queries;
149
150
	/**
151
	 * Whether to do only an explain on the query
152
	 * @var boolean
153
	 */
154
	protected $explain;
155
156
	/**
157
	 * The current database driver
158
	 * @var \Query\Drivers\DriverInterface
159
	 */
160
	public $db;
161
162
	/**
163
	 * Query parser class instance
164
	 * @var QueryParser
165
	 */
166
	public $parser;
167
168
	/**
169
	 * Alias to driver util class
170
	 * @var \Query\Drivers\AbstractUtil
171
	 */
172
	public $util;
173
174
	/**
175
	 * Alias to driver sql class
176
	 * @var \Query\Drivers\SQLInterface
177
	 */
178
	public $sql;
179
180
	// --------------------------------------------------------------------------
181
	// Methods
182
	// --------------------------------------------------------------------------
183
184
	/**
185
	 * Set values in the class, with either an array or key value pair
186
	 *
187
	 * @param array $var
188
	 * @param mixed $key
189
	 * @param mixed $val
190
	 * @param int $val_type
191
	 * @return array
192
	 */
193
	protected function _mixed_set(&$var, $key, $val=NULL, $val_type=self::BOTH)
194
	{
195
		$arg = (is_scalar($key) && is_scalar($val))
196
			? [$key => $val]
197
			: $key;
198
199
		foreach($arg as $k => $v)
200
		{
201
			if (in_array($val_type, [self::KEY, self::VALUE]))
202
			{
203
				$var[] = ($val_type === self::KEY)
204
					? $k
205
					: $v;
206
			}
207
			else
208
			{
209
				$var[$k] = $v;
210
			}
211
		}
212
213
		return $var;
214
	}
215
216
	// --------------------------------------------------------------------------
217
218
	/**
219
	 * Method to simplify select_ methods
220
	 *
221
	 * @param string $field
222
	 * @param string|bool $as
223
	 * @return string
224
	 */
225
	protected function _select($field, $as = FALSE)
226
	{
227
		// Escape the identifiers
228
		$field = $this->db->quote_ident($field);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->db->quote_ident($field); of type string|array adds the type array to the return on line 232 which is incompatible with the return type documented by Query\AbstractQueryBuilder::_select of type string.
Loading history...
229
230
		if ( ! is_string($as))
231
		{
232
			return $field;
233
		}
234
235
236
		$as = $this->db->quote_ident($as);
237
		return "({$field}) AS {$as} ";
238
	}
239
240
	// --------------------------------------------------------------------------
241
242
	/**
243
	 * Helper function for returning sql strings
244
	 *
245
	 * @param string $type
246
	 * @param string $table
247
	 * @param bool $reset
248
	 * @return string
249
	 */
250
	protected function _get_compile($type, $table, $reset)
251
	{
252
		$sql = $this->_compile($type, $table);
253
254
		// Reset the query builder for the next query
255
		if ($reset)
256
		{
257
			$this->reset_query();
258
		}
259
260
		return $sql;
261
	}
262
263
	// --------------------------------------------------------------------------
264
265
	/**
266
	 * Simplify 'like' methods
267
	 *
268
	 * @param string $field
269
	 * @param mixed $val
270
	 * @param string $pos
271
	 * @param string $like
272
	 * @param string $conj
273
	 * @return QueryBuilder
274
	 */
275
	protected function _like($field, $val, $pos, $like='LIKE', $conj='AND')
276
	{
277
		$field = $this->db->quote_ident($field);
278
279
		// Add the like string into the order map
280
		$like = $field. " {$like} ?";
281
282
		if ($pos == 'before')
283
		{
284
			$val = "%{$val}";
285
		}
286
		elseif ($pos == 'after')
287
		{
288
			$val = "{$val}%";
289
		}
290
		else
291
		{
292
			$val = "%{$val}%";
293
		}
294
295
		$conj = (empty($this->query_map)) ? ' WHERE ' : " {$conj} ";
296
		$this->_append_map($conj, $like, 'like');
297
298
		// Add to the values array
299
		$this->where_values[] = $val;
300
301
		return $this;
302
	}
303
304
	// --------------------------------------------------------------------------
305
306
	/**
307
	 * Simplify building having clauses
308
	 *
309
	 * @param mixed $key
310
	 * @param mixed $val
311
	 * @param string $conj
312
	 * @return QueryBuilder
313
	 */
314
	protected function _having($key, $val=[], $conj='AND')
315
	{
316
		$where = $this->_where($key, $val);
317
318
		// Create key/value placeholders
319
		foreach($where as $f => $val)
320
		{
321
			// Split each key by spaces, in case there
322
			// is an operator such as >, <, !=, etc.
323
			$f_array = explode(' ', trim($f));
324
325
			$item = $this->db->quote_ident($f_array[0]);
326
327
			// Simple key value, or an operator
328
			$item .= (count($f_array) === 1) ? '=?' : " {$f_array[1]} ?";
329
330
			// Put in the having map
331
			$this->having_map[] = [
332
				'conjunction' => ( ! empty($this->having_map)) ? " {$conj} " : ' HAVING ',
333
				'string' => $item
334
			];
335
		}
336
337
		return $this;
338
	}
339
340
	// --------------------------------------------------------------------------
341
342
	/**
343
	 * Do all the redundant stuff for where/having type methods
344
	 *
345
	 * @param mixed $key
346
	 * @param mixed $val
347
	 * @return array
348
	 */
349
	protected function _where($key, $val=[])
350
	{
351
		$where = [];
352
		$this->_mixed_set($where, $key, $val, self::BOTH);
353
		$this->_mixed_set($this->where_values, $key, $val, self::VALUE);
354
		return $where;
355
	}
356
357
	// --------------------------------------------------------------------------
358
359
	/**
360
	 * Simplify generating where string
361
	 *
362
	 * @param mixed $key
363
	 * @param mixed $val
364
	 * @param string $defaultConj
365
	 * @return QueryBuilder
366
	 */
367
	protected function _where_string($key, $val=[], $defaultConj='AND')
368
	{
369
		// Create key/value placeholders
370
		foreach($this->_where($key, $val) as $f => $val)
371
		{
372
			// Split each key by spaces, in case there
373
			// is an operator such as >, <, !=, etc.
374
			$f_array = explode(' ', trim($f));
375
376
			$item = $this->db->quote_ident($f_array[0]);
377
378
			// Simple key value, or an operator
379
			$item .= (count($f_array) === 1) ? '=?' : " {$f_array[1]} ?";
380
			$last_item = end($this->query_map);
381
382
			// Determine the correct conjunction
383
			$conjunctionList = array_column($this->query_map, 'conjunction');
384
			if (empty($this->query_map) || ( ! regex_in_array($conjunctionList, "/^ ?\n?WHERE/i")))
385
			{
386
				$conj = "\nWHERE ";
387
			}
388
			elseif ($last_item['type'] === 'group_start')
389
			{
390
				$conj = '';
391
			}
392
			else
393
			{
394
				$conj = " {$defaultConj} ";
395
			}
396
397
			$this->_append_map($conj, $item, 'where');
398
		}
399
400
		return $this;
401
	}
402
403
	// --------------------------------------------------------------------------
404
405
	/**
406
	 * Simplify where_in methods
407
	 *
408
	 * @param mixed $key
409
	 * @param mixed $val
410
	 * @param string $in - The (not) in fragment
411
	 * @param string $conj - The where in conjunction
412
	 * @return QueryBuilder
413
	 */
414
	protected function _where_in($key, $val=[], $in='IN', $conj='AND')
415
	{
416
		$key = $this->db->quote_ident($key);
417
		$params = array_fill(0, count($val), '?');
418
419
		foreach($val as $v)
420
		{
421
			$this->where_values[] = $v;
422
		}
423
424
		$conjunction = ( ! empty($this->query_map)) ? " {$conj} " : ' WHERE ';
425
		$str = $key . " {$in} (".implode(',', $params).') ';
426
427
		$this->_append_map($conjunction, $str, 'where_in');
428
429
		return $this;
430
	}
431
432
	// --------------------------------------------------------------------------
433
434
	/**
435
	 * Executes the compiled query
436
	 *
437
	 * @param string $type
438
	 * @param string $table
439
	 * @param string $sql
440
	 * @param array|null $vals
441
	 * @param boolean $reset
442
	 * @return \PDOStatement
443
	 */
444
	protected function _run($type, $table, $sql=NULL, $vals=NULL, $reset=TRUE)
445
	{
446
		if (is_null($sql))
447
		{
448
			$sql = $this->_compile($type, $table);
449
		}
450
451
		if (is_null($vals))
452
		{
453
			$vals = array_merge($this->values, (array) $this->where_values);
454
		}
455
456
		$start_time = microtime(TRUE);
457
458
		$res = (empty($vals))
459
			? $this->db->query($sql)
0 ignored issues
show
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...
460
			: $this->db->prepare_execute($sql, $vals);
461
462
		$end_time = microtime(TRUE);
463
		$total_time = number_format($end_time - $start_time, 5);
464
465
		// Add this query to the list of executed queries
466
		$this->_append_query($vals, $sql, $total_time);
467
468
		// Reset class state for next query
469
		if ($reset)
470
		{
471
			$this->reset_query();
472
		}
473
474
		return $res;
475
	}
476
477
	// --------------------------------------------------------------------------
478
479
	/**
480
	 * Add an additional set of mapping pairs to a internal map
481
	 *
482
	 * @param string $conjunction
483
	 * @param string $string
484
	 * @param string $type
485
	 * @return void
486
	 */
487
	protected function _append_map($conjunction = '', $string = '', $type = '')
488
	{
489
		array_push($this->query_map, [
490
			'type' => $type,
491
			'conjunction' => $conjunction,
492
			'string' => $string
493
		]);
494
	}
495
496
	// --------------------------------------------------------------------------
497
498
	/**
499
	 * Convert the prepared statement into readable sql
500
	 *
501
	 * @param array $vals
502
	 * @param string $sql
503
	 * @param int $total_time
504
	 * @return void
505
	 */
506
	protected function _append_query($vals, $sql, $total_time)
507
	{
508
		$evals = (is_array($vals)) ? $vals : [];
509
		$esql = str_replace('?', "%s", $sql);
510
511
		// Quote string values
512
		foreach($evals as &$v)
513
		{
514
			$v = ( ! is_numeric($v)) ? htmlentities($this->db->quote($v), ENT_NOQUOTES, 'utf-8')  : $v;
515
		}
516
517
		// Add the query onto the array of values to pass
518
		// as arguments to sprintf
519
		array_unshift($evals, $esql);
520
521
		// Add the interpreted query to the list of executed queries
522
		$this->queries[] = [
523
			'time' => $total_time,
524
			'sql' => call_user_func_array('sprintf', $evals),
525
		];
526
527
		$this->queries['total_time'] += (int) $total_time;
528
529
		// Set the last query to get rowcounts properly
530
		$this->db->set_last_query($sql);
531
	}
532
533
	// --------------------------------------------------------------------------
534
535
	/**
536
	 * Sub-method for generating sql strings
537
	 *
538
	 * @param string $type
539
	 * @param string $table
540
	 * @return string
541
	 */
542
	protected function _compile_type($type='', $table='')
543
	{
544
		switch($type)
545
		{
546
			case "insert":
547
				$param_count = count($this->set_array_keys);
548
				$params = array_fill(0, $param_count, '?');
549
				$sql = "INSERT INTO {$table} ("
550
					. implode(',', $this->set_array_keys)
551
					. ")\nVALUES (".implode(',', $params).')';
552
			break;
553
554
			case "update":
555
				$sql = "UPDATE {$table}\nSET {$this->set_string}";
556
			break;
557
558
			case "replace":
559
				// @TODO implement
560
				$sql = "";
561
			break;
562
563
			case "delete":
564
				$sql = "DELETE FROM {$table}";
565
			break;
566
567
			// Get queries
568
			default:
569
				$sql = "SELECT * \nFROM {$this->from_string}";
570
571
				// Set the select string
572
				if ( ! empty($this->select_string))
573
				{
574
					// Replace the star with the selected fields
575
					$sql = str_replace('*', $this->select_string, $sql);
576
				}
577
			break;
578
		}
579
580
		return $sql;
581
	}
582
583
	// --------------------------------------------------------------------------
584
585
	/**
586
	 * String together the sql statements for sending to the db
587
	 *
588
	 * @param string $type
589
	 * @param string $table
590
	 * @return string
591
	 */
592
	protected function _compile($type='', $table='')
593
	{
594
		// Get the base clause for the query
595
		$sql = $this->_compile_type($type, $this->db->quote_table($table));
596
597
		$clauses = [
598
			'query_map',
599
			'group_string',
600
			'order_string',
601
			'having_map',
602
		];
603
604
		// Set each type of subclause
605
		foreach($clauses as $clause)
606
		{
607
			$param = $this->$clause;
608
			if (is_array($param))
609
			{
610
				foreach($param as $q)
611
				{
612
					$sql .= $q['conjunction'] . $q['string'];
613
				}
614
			}
615
			else
616
			{
617
				$sql .= $param;
618
			}
619
		}
620
621
		// Set the limit via the class variables
622
		if (is_numeric($this->limit))
623
		{
624
			$sql = $this->sql->limit($sql, $this->limit, $this->offset);
625
		}
626
627
		// See if the query plan, rather than the
628
		// query data should be returned
629
		if ($this->explain === TRUE)
630
		{
631
			$sql = $this->sql->explain($sql);
632
		}
633
634
		return $sql;
635
	}
636
}
637
638
// End of abstract_QueryBuilder.php