Completed
Push — develop ( 91eb81...8401cc )
by Timothy
01:47
created

QueryBuilder::setDriver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 4
rs 10
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.1
8
 *
9
 * @package     Query
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2012 - 2018 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
namespace Query;
16
17
use BadMethodCallException;
18
use PDOStatement;
19
use Query\Drivers\{
20
	AbstractUtil,
21
	DriverInterface,
22
	SQLInterface
23
};
24
25
/**
26
 * Convenience class for creating sql queries
27
 */
28
class QueryBuilder implements QueryBuilderInterface {
29
30
	// --------------------------------------------------------------------------
31
	// ! Constants
32
	// --------------------------------------------------------------------------
33
34
	const KEY 	= 0;
35
	const VALUE = 1;
36
	const BOTH 	= 2;
37
38
	// --------------------------------------------------------------------------
39
	// ! SQL Clause Strings
40
	// --------------------------------------------------------------------------
41
42
	/**
43
	 * Compiled 'select' clause
44
	 * @var string
45
	 */
46
	protected $selectString = '';
47
48
	/**
49
	 * Compiled 'from' clause
50
	 * @var string
51
	 */
52
	protected $fromString = '';
53
54
	/**
55
	 * Compiled arguments for insert / update
56
	 * @var string
57
	 */
58
	protected $setString;
59
60
	/**
61
	 * Order by clause
62
	 * @var string
63
	 */
64
	protected $orderString;
65
66
	/**
67
	 * Group by clause
68
	 * @var string
69
	 */
70
	protected $groupString;
71
72
	// --------------------------------------------------------------------------
73
	// ! SQL Clause Arrays
74
	// --------------------------------------------------------------------------
75
76
	/**
77
	 * Keys for insert/update statement
78
	 * @var array
79
	 */
80
	protected $setArrayKeys = [];
81
82
	/**
83
	 * Key/val pairs for order by clause
84
	 * @var array
85
	 */
86
	protected $orderArray = [];
87
88
	/**
89
	 * Key/val pairs for group by clause
90
	 * @var array
91
	 */
92
	protected $groupArray = [];
93
94
	// --------------------------------------------------------------------------
95
	// ! Other Class vars
96
	// --------------------------------------------------------------------------
97
98
	/**
99
	 * Values to apply to prepared statements
100
	 * @var array
101
	 */
102
	protected $values = [];
103
104
	/**
105
	 * Values to apply to where clauses in prepared statements
106
	 * @var array
107
	 */
108
	protected $whereValues = [];
109
110
	/**
111
	 * Value for limit string
112
	 * @var string
113
	 */
114
	protected $limit;
115
116
	/**
117
	 * Value for offset in limit string
118
	 * @var integer
119
	 */
120
	protected $offset;
121
122
	/**
123
	 * Query component order mapping
124
	 * for complex select queries
125
	 *
126
	 * Format:
127
	 * array(
128
	 *		'type' => 'where',
129
	 *		'conjunction' => ' AND ',
130
	 *		'string' => 'k=?'
131
	 * )
132
	 *
133
	 * @var array
134
	 */
135
	protected $queryMap = [];
136
137
	/**
138
	 * Map for having clause
139
	 * @var array
140
	 */
141
	protected $havingMap;
142
143
	/**
144
	 * Convenience property for connection management
145
	 * @var string
146
	 */
147
	public $connName = '';
148
149
	/**
150
	 * List of queries executed
151
	 * @var array
152
	 */
153
	public $queries;
154
155
	/**
156
	 * Whether to do only an explain on the query
157
	 * @var boolean
158
	 */
159
	protected $explain;
160
161
	/**
162
	 * The current database driver
163
	 * @var DriverInterface
164
	 */
165
	public $driver;
166
167
	/**
168
	 * Query parser class instance
169
	 * @var QueryParser
170
	 */
171
	protected $parser;
172
173
	/**
174
	 * Alias to driver util class
175
	 * @var AbstractUtil
176
	 */
177
	protected $util;
178
179
	/**
180
	 * Alias to driver sql class
181
	 * @var SQLInterface
182
	 */
183
	protected $sql;
184
185
	/**
186
	 * String class values to be reset
187
	 *
188
	 * @var array
189
	 */
190
	private $stringVars = [
191
		'selectString',
192
		'fromString',
193
		'setString',
194
		'orderString',
195
		'groupString',
196
		'limit',
197
		'offset',
198
		'explain',
199
	];
200
201
	/**
202
	 * Array class variables to be reset
203
	 *
204
	 * @var array
205
	 */
206
	private $arrayVars = [
207
		'setArrayKeys',
208
		'orderArray',
209
		'groupArray',
210
		'values',
211
		'whereValues',
212
		'queryMap',
213
		'havingMap'
214
	];
215
216
	// --------------------------------------------------------------------------
217
	// ! Methods
218
	// --------------------------------------------------------------------------
219
220
	/**
221
	 * Constructor
222
	 *
223
	 * @param DriverInterface $driver
224
	 * @param QueryParser $parser
225
	 */
226
	public function __construct(DriverInterface $driver, QueryParser $parser)
227
	{
228
		// Inject driver and parser
229
		$this->driver = $driver;
0 ignored issues
show
Documentation Bug introduced by
It seems like $driver of type object<Query\Drivers\DriverInterface> is incompatible with the declared type object<DriverInterface> of property $driver.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
230
		$this->parser = $parser;
231
232
		$this->queries['total_time'] = 0;
233
234
		// Alias driver sql and util classes
235
		$this->sql = $this->driver->getSql();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->driver->getSql() of type object<Query\Drivers\SQLInterface> is incompatible with the declared type object<SQLInterface> of property $sql.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
236
		$this->util = $this->driver->getUtil();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->driver->getUtil() of type object<Query\Drivers\AbstractUtil> is incompatible with the declared type object<AbstractUtil> of property $util.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
237
	}
238
239
	/**
240
	 * Destructor
241
	 * @codeCoverageIgnore
242
	 */
243
	public function __destruct()
244
	{
245
		$this->driver = NULL;
246
	}
247
248
	/**
249
	 * Calls a function further down the inheritance chain
250
	 *
251
	 * @param string $name
252
	 * @param array $params
253
	 * @return mixed
254
	 * @throws BadMethodCallException
255
	 */
256
	public function __call(string $name, array $params)
257
	{
258
		// Alias snake_case method calls
259
		$camelName = \to_camel_case($name);
260
261
		foreach([$this, $this->driver] as $object)
262
		{
263
			foreach([$name, $camelName] as $methodName)
264
			{
265
				if (method_exists($object, $methodName))
266
				{
267
					return \call_user_func_array([$object, $methodName], $params);
268
				}
269
			}
270
271
		}
272
273
		throw new BadMethodCallException('Method does not exist');
274
	}
275
276
	// --------------------------------------------------------------------------
277
	// ! Select Queries
278
	// --------------------------------------------------------------------------
279
280
	/**
281
	 * Specifies rows to select in a query
282
	 *
283
	 * @param string $fields
284
	 * @return QueryBuilderInterface
285
	 */
286
	public function select(string $fields): QueryBuilderInterface
287
	{
288
		// Split fields by comma
289
		$fieldsArray = explode(',', $fields);
290
		$fieldsArray = array_map('mb_trim', $fieldsArray);
291
292
		// Split on 'As'
293
		foreach ($fieldsArray as $key => $field)
294
		{
295
			if (stripos($field, 'as') !== FALSE)
296
			{
297
				$fieldsArray[$key] = preg_split('` as `i', $field);
298
				$fieldsArray[$key] = array_map('mb_trim', $fieldsArray[$key]);
299
			}
300
		}
301
302
		// Quote the identifiers
303
		$safeArray = $this->driver->quoteIdent($fieldsArray);
304
305
		unset($fieldsArray);
306
307
		// Join the strings back together
308
		for($i = 0, $c = count($safeArray); $i < $c; $i++)
309
		{
310
			if (\is_array($safeArray[$i]))
311
			{
312
				$safeArray[$i] = implode(' AS ', $safeArray[$i]);
313
			}
314
		}
315
316
		$this->selectString .= implode(', ', $safeArray);
317
318
		unset($safeArray);
319
320
		return $this;
321
	}
322
323
	/**
324
	 * Selects the maximum value of a field from a query
325
	 *
326
	 * @param string $field
327
	 * @param string|bool $as
328
	 * @return QueryBuilderInterface
329
	 */
330
	public function selectMax(string $field, $as=FALSE): QueryBuilderInterface
331
	{
332
		// Create the select string
333
		$this->selectString .= ' MAX'.$this->_select($field, $as);
334
		return $this;
335
	}
336
337
	/**
338
	 * Selects the minimum value of a field from a query
339
	 *
340
	 * @param string $field
341
	 * @param string|bool $as
342
	 * @return QueryBuilderInterface
343
	 */
344
	public function selectMin(string $field, $as=FALSE): QueryBuilderInterface
345
	{
346
		// Create the select string
347
		$this->selectString .= ' MIN'.$this->_select($field, $as);
348
		return $this;
349
	}
350
351
	/**
352
	 * Selects the average value of a field from a query
353
	 *
354
	 * @param string $field
355
	 * @param string|bool $as
356
	 * @return QueryBuilderInterface
357
	 */
358
	public function selectAvg(string $field, $as=FALSE): QueryBuilderInterface
359
	{
360
		// Create the select string
361
		$this->selectString .= ' AVG'.$this->_select($field, $as);
362
		return $this;
363
	}
364
365
	/**
366
	 * Selects the sum of a field from a query
367
	 *
368
	 * @param string $field
369
	 * @param string|bool $as
370
	 * @return QueryBuilderInterface
371
	 */
372
	public function selectSum(string $field, $as=FALSE): QueryBuilderInterface
373
	{
374
		// Create the select string
375
		$this->selectString .= ' SUM'.$this->_select($field, $as);
376
		return $this;
377
	}
378
379
	/**
380
	 * Adds the 'distinct' keyword to a query
381
	 *
382
	 * @return QueryBuilderInterface
383
	 */
384
	public function distinct(): QueryBuilderInterface
385
	{
386
		// Prepend the keyword to the select string
387
		$this->selectString = ' DISTINCT '.$this->selectString;
388
		return $this;
389
	}
390
391
	/**
392
	 * Tell the database to give you the query plan instead of result set
393
	 *
394
	 * @return QueryBuilderInterface
395
	 */
396
	public function explain(): QueryBuilderInterface
397
	{
398
		$this->explain = TRUE;
399
		return $this;
400
	}
401
402
	/**
403
	 * Specify the database table to select from
404
	 *
405
	 * @param string $tblname
406
	 * @return QueryBuilderInterface
407
	 */
408
	public function from($tblname): QueryBuilderInterface
409
	{
410
		// Split identifiers on spaces
411
		$identArray = explode(' ', \mb_trim($tblname));
412
		$identArray = array_map('\\mb_trim', $identArray);
413
414
		// Quote the identifiers
415
		$identArray[0] = $this->driver->quoteTable($identArray[0]);
416
		$identArray = $this->driver->quoteIdent($identArray);
417
418
		// Paste it back together
419
		$this->fromString = implode(' ', $identArray);
420
421
		return $this;
422
	}
423
424
	// --------------------------------------------------------------------------
425
	// ! 'Like' methods
426
	// --------------------------------------------------------------------------
427
428
	/**
429
	 * Creates a Like clause in the sql statement
430
	 *
431
	 * @param string $field
432
	 * @param mixed $val
433
	 * @param string $pos
434
	 * @return QueryBuilderInterface
435
	 */
436
	public function like($field, $val, $pos='both'): QueryBuilderInterface
437
	{
438
		return $this->_like($field, $val, $pos, 'LIKE', 'AND');
439
	}
440
441
	/**
442
	 * Generates an OR Like clause
443
	 *
444
	 * @param string $field
445
	 * @param mixed $val
446
	 * @param string $pos
447
	 * @return QueryBuilderInterface
448
	 */
449
	public function orLike($field, $val, $pos='both'): QueryBuilderInterface
450
	{
451
		return $this->_like($field, $val, $pos, 'LIKE', 'OR');
452
	}
453
454
	/**
455
	 * Generates a NOT LIKE clause
456
	 *
457
	 * @param string $field
458
	 * @param mixed $val
459
	 * @param string $pos
460
	 * @return QueryBuilderInterface
461
	 */
462
	public function notLike($field, $val, $pos='both'): QueryBuilderInterface
463
	{
464
		return $this->_like($field, $val, $pos, 'NOT LIKE', 'AND');
465
	}
466
467
	/**
468
	 * Generates a OR NOT LIKE clause
469
	 *
470
	 * @param string $field
471
	 * @param mixed $val
472
	 * @param string $pos
473
	 * @return QueryBuilderInterface
474
	 */
475
	public function orNotLike($field, $val, $pos='both'): QueryBuilderInterface
476
	{
477
		return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR');
478
	}
479
480
	// --------------------------------------------------------------------------
481
	// ! Having methods
482
	// --------------------------------------------------------------------------
483
484
	/**
485
	 * Generates a 'Having' clause
486
	 *
487
	 * @param mixed $key
488
	 * @param mixed $val
489
	 * @return QueryBuilderInterface
490
	 */
491
	public function having($key, $val=[]): QueryBuilderInterface
492
	{
493
		return $this->_having($key, $val, 'AND');
494
	}
495
496
	/**
497
	 * Generates a 'Having' clause prefixed with 'OR'
498
	 *
499
	 * @param mixed $key
500
	 * @param mixed $val
501
	 * @return QueryBuilderInterface
502
	 */
503
	public function orHaving($key, $val=[]): QueryBuilderInterface
504
	{
505
		return $this->_having($key, $val, 'OR');
506
	}
507
508
	// --------------------------------------------------------------------------
509
	// ! 'Where' methods
510
	// --------------------------------------------------------------------------
511
512
	/**
513
	 * Specify condition(s) in the where clause of a query
514
	 * Note: this function works with key / value, or a
515
	 * passed array with key / value pairs
516
	 *
517
	 * @param mixed $key
518
	 * @param mixed $val
519
	 * @param mixed $escape
520
	 * @return QueryBuilderInterface
521
	 */
522
	public function where($key, $val=[], $escape=NULL): QueryBuilderInterface
523
	{
524
		return $this->_whereString($key, $val, 'AND');
525
	}
526
527
	/**
528
	 * Where clause prefixed with "OR"
529
	 *
530
	 * @param string $key
531
	 * @param mixed $val
532
	 * @return QueryBuilderInterface
533
	 */
534
	public function orWhere($key, $val=[]): QueryBuilderInterface
535
	{
536
		return $this->_whereString($key, $val, 'OR');
537
	}
538
539
	/**
540
	 * Where clause with 'IN' statement
541
	 *
542
	 * @param mixed $field
543
	 * @param mixed $val
544
	 * @return QueryBuilderInterface
545
	 */
546
	public function whereIn($field, $val=[]): QueryBuilderInterface
547
	{
548
		return $this->_whereIn($field, $val);
549
	}
550
551
	/**
552
	 * Where in statement prefixed with "or"
553
	 *
554
	 * @param string $field
555
	 * @param mixed $val
556
	 * @return QueryBuilderInterface
557
	 */
558
	public function orWhereIn($field, $val=[]): QueryBuilderInterface
559
	{
560
		return $this->_whereIn($field, $val, 'IN', 'OR');
561
	}
562
563
	/**
564
	 * WHERE NOT IN (FOO) clause
565
	 *
566
	 * @param string $field
567
	 * @param mixed $val
568
	 * @return QueryBuilderInterface
569
	 */
570
	public function whereNotIn($field, $val=[]): QueryBuilderInterface
571
	{
572
		return $this->_whereIn($field, $val, 'NOT IN', 'AND');
573
	}
574
575
	/**
576
	 * OR WHERE NOT IN (FOO) clause
577
	 *
578
	 * @param string $field
579
	 * @param mixed $val
580
	 * @return QueryBuilderInterface
581
	 */
582
	public function orWhereNotIn($field, $val=[]): QueryBuilderInterface
583
	{
584
		return $this->_whereIn($field, $val, 'NOT IN', 'OR');
585
	}
586
587
	// --------------------------------------------------------------------------
588
	// ! Other Query Modifier methods
589
	// --------------------------------------------------------------------------
590
591
	/**
592
	 * Sets values for inserts / updates / deletes
593
	 *
594
	 * @param mixed $key
595
	 * @param mixed $val
596
	 * @return QueryBuilderInterface
597
	 */
598
	public function set($key, $val = NULL): QueryBuilderInterface
599
	{
600
		$this->_mixedSet($this->setArrayKeys, $key, $val, self::KEY);
601
		$this->_mixedSet($this->values, $key, $val, self::VALUE);
602
603
		// Use the keys of the array to make the insert/update string
604
		// Escape the field names
605
		$this->setArrayKeys = array_map([$this->driver, '_quote'], $this->setArrayKeys);
606
607
		// Generate the "set" string
608
		$this->setString = implode('=?,', $this->setArrayKeys);
609
		$this->setString .= '=?';
610
611
		return $this;
612
	}
613
614
	/**
615
	 * Creates a join phrase in a compiled query
616
	 *
617
	 * @param string $table
618
	 * @param string $condition
619
	 * @param string $type
620
	 * @return QueryBuilderInterface
621
	 */
622
	public function join($table, $condition, $type=''): QueryBuilderInterface
623
	{
624
		// Prefix and quote table name
625
		$table = explode(' ', mb_trim($table));
626
		$table[0] = $this->driver->quoteTable($table[0]);
627
		$table = $this->driver->quoteIdent($table);
628
		$table = implode(' ', $table);
629
630
		// Parse out the join condition
631
		$parsedCondition = $this->parser->compileJoin($condition);
632
		$condition = $table . ' ON ' . $parsedCondition;
633
634
		$this->_appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, 'join');
635
636
		return $this;
637
	}
638
639
	/**
640
	 * Group the results by the selected field(s)
641
	 *
642
	 * @param mixed $field
643
	 * @return QueryBuilderInterface
644
	 */
645
	public function groupBy($field): QueryBuilderInterface
646
	{
647
		if ( ! is_scalar($field))
648
		{
649
			$newGroupArray = array_map([$this->driver, 'quoteIdent'], $field);
650
			$this->groupArray = array_merge($this->groupArray, $newGroupArray);
651
		}
652
		else
653
		{
654
			$this->groupArray[] = $this->driver->quoteIdent($field);
655
		}
656
657
		$this->groupString = ' GROUP BY ' . implode(',', $this->groupArray);
658
659
		return $this;
660
	}
661
662
	/**
663
	 * Order the results by the selected field(s)
664
	 *
665
	 * @param string $field
666
	 * @param string $type
667
	 * @return QueryBuilderInterface
668
	 */
669
	public function orderBy($field, $type=''): QueryBuilderInterface
670
	{
671
		// When ordering by random, do an ascending order if the driver
672
		// doesn't support random ordering
673
		if (stripos($type, 'rand') !== FALSE)
674
		{
675
			$rand = $this->sql->random();
676
			$type = $rand ?? 'ASC';
677
		}
678
679
		// Set fields for later manipulation
680
		$field = $this->driver->quoteIdent($field);
681
		$this->orderArray[$field] = $type;
682
683
		$orderClauses = [];
684
685
		// Flatten key/val pairs into an array of space-separated pairs
686
		foreach($this->orderArray as $k => $v)
687
		{
688
			$orderClauses[] = $k . ' ' . strtoupper($v);
689
		}
690
691
		// Set the final string
692
		$this->orderString = ( ! isset($rand))
693
			? "\nORDER BY ".implode(', ', $orderClauses)
694
			: "\nORDER BY".$rand;
695
696
		return $this;
697
	}
698
699
	/**
700
	 * Set a limit on the current sql statement
701
	 *
702
	 * @param int $limit
703
	 * @param int|bool $offset
704
	 * @return QueryBuilderInterface
705
	 */
706
	public function limit($limit, $offset=FALSE): QueryBuilderInterface
707
	{
708
		$this->limit = (int) $limit;
0 ignored issues
show
Documentation Bug introduced by
The property $limit was declared of type string, but (int) $limit is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
709
		$this->offset = $offset;
0 ignored issues
show
Documentation Bug introduced by
It seems like $offset can also be of type boolean. However, the property $offset is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
710
711
		return $this;
712
	}
713
714
	// --------------------------------------------------------------------------
715
	// ! Query Grouping Methods
716
	// --------------------------------------------------------------------------
717
718
	/**
719
	 * Adds a paren to the current query for query grouping
720
	 *
721
	 * @return QueryBuilderInterface
722
	 */
723 View Code Duplication
	public function groupStart(): QueryBuilderInterface
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
724
	{
725
		$conj = empty($this->queryMap) ? ' WHERE ' : ' ';
726
727
		$this->_appendMap($conj, '(', 'group_start');
728
729
		return $this;
730
	}
731
732
	/**
733
	 * Adds a paren to the current query for query grouping,
734
	 * prefixed with 'NOT'
735
	 *
736
	 * @return QueryBuilderInterface
737
	 */
738 View Code Duplication
	public function notGroupStart(): QueryBuilderInterface
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
739
	{
740
		$conj = empty($this->queryMap) ? ' WHERE ' : ' AND ';
741
742
		$this->_appendMap($conj, ' NOT (', 'group_start');
743
744
		return $this;
745
	}
746
747
	/**
748
	 * Adds a paren to the current query for query grouping,
749
	 * prefixed with 'OR'
750
	 *
751
	 * @return QueryBuilderInterface
752
	 */
753
	public function orGroupStart(): QueryBuilderInterface
754
	{
755
		$this->_appendMap('', ' OR (', 'group_start');
756
757
		return $this;
758
	}
759
760
	/**
761
	 * Adds a paren to the current query for query grouping,
762
	 * prefixed with 'OR NOT'
763
	 *
764
	 * @return QueryBuilderInterface
765
	 */
766
	public function orNotGroupStart(): QueryBuilderInterface
767
	{
768
		$this->_appendMap('', ' OR NOT (', 'group_start');
769
770
		return $this;
771
	}
772
773
	/**
774
	 * Ends a query group
775
	 *
776
	 * @return QueryBuilderInterface
777
	 */
778
	public function groupEnd(): QueryBuilderInterface
779
	{
780
		$this->_appendMap('', ')', 'group_end');
781
782
		return $this;
783
	}
784
785
	// --------------------------------------------------------------------------
786
	// ! Query execution methods
787
	// --------------------------------------------------------------------------
788
789
	/**
790
	 * Select and retrieve all records from the current table, and/or
791
	 * execute current compiled query
792
	 *
793
	 * @param string $table
794
	 * @param int|bool $limit
795
	 * @param int|bool $offset
796
	 * @return PDOStatement
797
	 */
798
	public function get($table='', $limit=FALSE, $offset=FALSE): PDOStatement
799
	{
800
		// Set the table
801
		if ( ! empty($table))
802
		{
803
			$this->from($table);
804
		}
805
806
		// Set the limit, if it exists
807
		if (\is_int($limit))
808
		{
809
			$this->limit($limit, $offset);
810
		}
811
812
		return $this->_run('get', $table);
813
	}
814
815
	/**
816
	 * Convenience method for get() with a where clause
817
	 *
818
	 * @param string $table
819
	 * @param array $where
820
	 * @param int|bool $limit
821
	 * @param int|bool $offset
822
	 * @return PDOStatement
823
	 */
824
	public function getWhere($table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement
825
	{
826
		// Create the where clause
827
		$this->where($where);
828
829
		// Return the result
830
		return $this->get($table, $limit, $offset);
831
	}
832
833
	/**
834
	 * Retrieve the number of rows in the selected table
835
	 *
836
	 * @param string $table
837
	 * @return int
838
	 */
839
	public function countAll($table): int
840
	{
841
		$sql = 'SELECT * FROM '.$this->driver->quoteTable($table);
842
		$res = $this->driver->query($sql);
843
		return (int) count($res->fetchAll());
844
	}
845
846
	/**
847
	 * Retrieve the number of results for the generated query - used
848
	 * in place of the get() method
849
	 *
850
	 * @param string $table
851
	 * @param boolean $reset
852
	 * @return int
853
	 */
854
	public function countAllResults(string $table='', bool $reset = TRUE): int
855
	{
856
		// Set the table
857
		if ( ! empty($table))
858
		{
859
			$this->from($table);
860
		}
861
862
		$result = $this->_run('get', $table, NULL, NULL, $reset);
863
		$rows = $result->fetchAll();
864
865
		return (int) count($rows);
866
	}
867
868
	/**
869
	 * Creates an insert clause, and executes it
870
	 *
871
	 * @param string $table
872
	 * @param mixed $data
873
	 * @return PDOStatement
874
	 */
875
	public function insert($table, $data=[]): PDOStatement
876
	{
877
		if ( ! empty($data))
878
		{
879
			$this->set($data);
880
		}
881
882
		return $this->_run('insert', $table);
883
	}
884
885
	/**
886
	 * Creates and executes a batch insertion query
887
	 *
888
	 * @param string $table
889
	 * @param array $data
890
	 * @return PDOStatement
891
	 */
892 View Code Duplication
	public function insertBatch($table, $data=[]): PDOStatement
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
893
	{
894
		// Get the generated values and sql string
895
		list($sql, $data) = $this->driver->insertBatch($table, $data);
896
897
		return ( ! is_null($sql))
898
			? $this->_run('', $table, $sql, $data)
899
			: NULL;
900
	}
901
902
	/**
903
	 * Creates an update clause, and executes it
904
	 *
905
	 * @param string $table
906
	 * @param mixed $data
907
	 * @return PDOStatement
908
	 */
909
	public function update($table, $data=[]): PDOStatement
910
	{
911
		if ( ! empty($data))
912
		{
913
			$this->set($data);
914
		}
915
916
		return $this->_run('update', $table);
917
	}
918
919
	/**
920
	 * Creates a batch update, and executes it.
921
	 * Returns the number of affected rows
922
	 *
923
	 * @param string $table
924
	 * @param array|object $data
925
	 * @param string $where
926
	 * @return int|null
927
	 */
928 View Code Duplication
	public function updateBatch($table, $data, $where)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
929
	{
930
		// Get the generated values and sql string
931
		list($sql, $data) = $this->driver->updateBatch($table, $data, $where);
932
933
		return ( ! is_null($sql))
0 ignored issues
show
Bug Compatibility introduced by
The expression !is_null($sql) ? $this->...e, $sql, $data) : NULL; of type PDOStatement|null adds the type PDOStatement to the return on line 933 which is incompatible with the return type declared by the interface Query\QueryBuilderInterface::updateBatch of type integer|null.
Loading history...
934
			? $this->_run('', $table, $sql, $data)
935
			: NULL;
936
	}
937
938
	/**
939
	 * Insertion with automatic overwrite, rather than attempted duplication
940
	 *
941
	 * @param string $table
942
	 * @param array $data
943
	 * @return \PDOStatement|null
944
	 */
945
	public function replace($table, $data=[])
946
	{
947
		if ( ! empty($data))
948
		{
949
			$this->set($data);
950
		}
951
952
		return $this->_run('replace', $table);
953
	}
954
955
	/**
956
	 * Deletes data from a table
957
	 *
958
	 * @param string $table
959
	 * @param mixed $where
960
	 * @return PDOStatement
961
	 */
962
	public function delete($table, $where=''): PDOStatement
963
	{
964
		// Set the where clause
965
		if ( ! empty($where))
966
		{
967
			$this->where($where);
968
		}
969
970
		return $this->_run('delete', $table);
971
	}
972
973
	// --------------------------------------------------------------------------
974
	// ! SQL Returning Methods
975
	// --------------------------------------------------------------------------
976
977
	/**
978
	 * Returns the generated 'select' sql query
979
	 *
980
	 * @param string $table
981
	 * @param bool $reset
982
	 * @return string
983
	 */
984
	public function getCompiledSelect(string $table='', bool $reset=TRUE): string
985
	{
986
		// Set the table
987
		if ( ! empty($table))
988
		{
989
			$this->from($table);
990
		}
991
992
		return $this->_getCompile('select', $table, $reset);
993
	}
994
995
	/**
996
	 * Returns the generated 'insert' sql query
997
	 *
998
	 * @param string $table
999
	 * @param bool $reset
1000
	 * @return string
1001
	 */
1002
	public function getCompiledInsert(string $table, bool $reset=TRUE): string
1003
	{
1004
		return $this->_getCompile('insert', $table, $reset);
1005
	}
1006
1007
	/**
1008
	 * Returns the generated 'update' sql query
1009
	 *
1010
	 * @param string $table
1011
	 * @param bool $reset
1012
	 * @return string
1013
	 */
1014
	public function getCompiledUpdate(string $table='', bool $reset=TRUE): string
1015
	{
1016
		return $this->_getCompile('update', $table, $reset);
1017
	}
1018
1019
	/**
1020
	 * Returns the generated 'delete' sql query
1021
	 *
1022
	 * @param string $table
1023
	 * @param bool $reset
1024
	 * @return string
1025
	 */
1026
	public function getCompiledDelete(string $table='', bool $reset=TRUE): string
1027
	{
1028
		return $this->_getCompile('delete', $table, $reset);
1029
	}
1030
1031
	// --------------------------------------------------------------------------
1032
	// ! Miscellaneous Methods
1033
	// --------------------------------------------------------------------------
1034
1035
	/**
1036
	 * Clear out the class variables, so the next query can be run
1037
	 *
1038
	 * @return void
1039
	 */
1040
	public function resetQuery(): void
1041
	{
1042
		// Reset strings and booleans
1043
		foreach($this->stringVars as $var)
1044
		{
1045
			$this->$var = NULL;
1046
		}
1047
1048
		// Reset arrays
1049
		foreach($this->arrayVars as $var)
1050
		{
1051
			$this->$var = [];
1052
		}
1053
	}
1054
1055
	/**
1056
	 * Set values in the class, with either an array or key value pair
1057
	 *
1058
	 * @param array $var
1059
	 * @param mixed $key
1060
	 * @param mixed $val
1061
	 * @param int $valType
1062
	 * @return array
1063
	 */
1064
	protected function _mixedSet(array &$var, $key, $val=NULL, int $valType=self::BOTH): array
1065
	{
1066
		$arg = (is_scalar($key) && is_scalar($val))
1067
			? [$key => $val]
1068
			: $key;
1069
1070
		foreach($arg as $k => $v)
1071
		{
1072
			if (\in_array($valType, [self::KEY, self::VALUE], TRUE))
1073
			{
1074
				$var[] = ($valType === self::KEY)
1075
					? $k
1076
					: $v;
1077
			}
1078
			else
1079
			{
1080
				$var[$k] = $v;
1081
			}
1082
		}
1083
1084
		return $var;
1085
	}
1086
1087
	/**
1088
	 * Method to simplify select_ methods
1089
	 *
1090
	 * @param string $field
1091
	 * @param string|bool $as
1092
	 * @return string
1093
	 */
1094
	protected function _select(string $field, $as = FALSE): string
1095
	{
1096
		// Escape the identifiers
1097
		$field = $this->driver->quoteIdent($field);
1098
1099
		if ( ! \is_string($as))
1100
		{
1101
			return $field;
1102
		}
1103
1104
		$as = $this->driver->quoteIdent($as);
1105
		return "({$field}) AS {$as} ";
1106
	}
1107
1108
	/**
1109
	 * Helper function for returning sql strings
1110
	 *
1111
	 * @param string $type
1112
	 * @param string $table
1113
	 * @param bool $reset
1114
	 * @return string
1115
	 */
1116
	protected function _getCompile(string $type, string $table, bool $reset): string
1117
	{
1118
		$sql = $this->_compile($type, $table);
1119
1120
		// Reset the query builder for the next query
1121
		if ($reset)
1122
		{
1123
			$this->resetQuery();
1124
		}
1125
1126
		return $sql;
1127
	}
1128
1129
	/**
1130
	 * Simplify 'like' methods
1131
	 *
1132
	 * @param string $field
1133
	 * @param mixed $val
1134
	 * @param string $pos
1135
	 * @param string $like
1136
	 * @param string $conj
1137
	 * @return self
1138
	 */
1139
	protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): self
1140
	{
1141
		$field = $this->driver->quoteIdent($field);
1142
1143
		// Add the like string into the order map
1144
		$like = $field. " {$like} ?";
1145
1146
		if ($pos === 'before')
1147
		{
1148
			$val = "%{$val}";
1149
		}
1150
		elseif ($pos === 'after')
1151
		{
1152
			$val = "{$val}%";
1153
		}
1154
		else
1155
		{
1156
			$val = "%{$val}%";
1157
		}
1158
1159
		$conj = empty($this->queryMap) ? ' WHERE ' : " {$conj} ";
1160
		$this->_appendMap($conj, $like, 'like');
1161
1162
		// Add to the values array
1163
		$this->whereValues[] = $val;
1164
1165
		return $this;
1166
	}
1167
1168
	/**
1169
	 * Simplify building having clauses
1170
	 *
1171
	 * @param mixed $key
1172
	 * @param mixed $values
1173
	 * @param string $conj
1174
	 * @return self
1175
	 */
1176
	protected function _having($key, $values=[], string $conj='AND'): self
1177
	{
1178
		$where = $this->_where($key, $values);
1179
1180
		// Create key/value placeholders
1181
		foreach($where as $f => $val)
1182
		{
1183
			// Split each key by spaces, in case there
1184
			// is an operator such as >, <, !=, etc.
1185
			$fArray = explode(' ', trim($f));
1186
1187
			$item = $this->driver->quoteIdent($fArray[0]);
1188
1189
			// Simple key value, or an operator
1190
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
1191
1192
			// Put in the having map
1193
			$this->havingMap[] = [
1194
				'conjunction' => ( ! empty($this->havingMap)) ? " {$conj} " : ' HAVING ',
1195
				'string' => $item
1196
			];
1197
		}
1198
1199
		return $this;
1200
	}
1201
1202
	/**
1203
	 * Do all the redundant stuff for where/having type methods
1204
	 *
1205
	 * @param mixed $key
1206
	 * @param mixed $val
1207
	 * @return array
1208
	 */
1209
	protected function _where($key, $val=[]): array
1210
	{
1211
		$where = [];
1212
		$this->_mixedSet($where, $key, $val);
1213
		$this->_mixedSet($this->whereValues, $key, $val, self::VALUE);
1214
		return $where;
1215
	}
1216
1217
	/**
1218
	 * Simplify generating where string
1219
	 *
1220
	 * @param mixed $key
1221
	 * @param mixed $values
1222
	 * @param string $defaultConj
1223
	 * @return self
1224
	 */
1225
	protected function _whereString($key, $values=[], string $defaultConj='AND'): self
1226
	{
1227
		// Create key/value placeholders
1228
		foreach($this->_where($key, $values) as $f => $val)
1229
		{
1230
			// Split each key by spaces, in case there
1231
			// is an operator such as >, <, !=, etc.
1232
			$fArray = explode(' ', trim($f));
1233
1234
			$item = $this->driver->quoteIdent($fArray[0]);
1235
1236
			// Simple key value, or an operator
1237
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
1238
			$lastItem = end($this->queryMap);
1239
1240
			// Determine the correct conjunction
1241
			$conjunctionList = array_column($this->queryMap, 'conjunction');
1242
			if (empty($this->queryMap) || ( ! regex_in_array($conjunctionList, "/^ ?\n?WHERE/i")))
1243
			{
1244
				$conj = "\nWHERE ";
1245
			}
1246
			elseif ($lastItem['type'] === 'group_start')
1247
			{
1248
				$conj = '';
1249
			}
1250
			else
1251
			{
1252
				$conj = " {$defaultConj} ";
1253
			}
1254
1255
			$this->_appendMap($conj, $item, 'where');
1256
		}
1257
1258
		return $this;
1259
	}
1260
1261
	/**
1262
	 * Simplify where_in methods
1263
	 *
1264
	 * @param mixed $key
1265
	 * @param mixed $val
1266
	 * @param string $in - The (not) in fragment
1267
	 * @param string $conj - The where in conjunction
1268
	 * @return self
1269
	 */
1270
	protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): self
1271
	{
1272
		$key = $this->driver->quoteIdent($key);
1273
		$params = array_fill(0, count($val), '?');
1274
1275
		foreach($val as $v)
1276
		{
1277
			$this->whereValues[] = $v;
1278
		}
1279
1280
		$conjunction = ( ! empty($this->queryMap)) ? " {$conj} " : ' WHERE ';
1281
		$str = $key . " {$in} (".implode(',', $params).') ';
1282
1283
		$this->_appendMap($conjunction, $str, 'where_in');
1284
1285
		return $this;
1286
	}
1287
1288
	/**
1289
	 * Executes the compiled query
1290
	 *
1291
	 * @param string $type
1292
	 * @param string $table
1293
	 * @param string $sql
1294
	 * @param array|null $vals
1295
	 * @param boolean $reset
1296
	 * @return PDOStatement
1297
	 */
1298
	protected function _run(string $type, string $table, $sql=NULL, $vals=NULL, bool $reset=TRUE): PDOStatement
1299
	{
1300
		if ($sql === NULL)
1301
		{
1302
			$sql = $this->_compile($type, $table);
1303
		}
1304
1305
		if ($vals === NULL)
1306
		{
1307
			$vals = array_merge($this->values, (array) $this->whereValues);
1308
		}
1309
1310
		$startTime = microtime(TRUE);
1311
1312
		$res = empty($vals)
1313
			? $this->driver->query($sql)
1314
			: $this->driver->prepareExecute($sql, $vals);
1315
1316
		$endTime = microtime(TRUE);
1317
		$totalTime = number_format($endTime - $startTime, 5);
1318
1319
		// Add this query to the list of executed queries
1320
		$this->_appendQuery($vals, $sql, (int) $totalTime);
1321
1322
		// Reset class state for next query
1323
		if ($reset)
1324
		{
1325
			$this->resetQuery();
1326
		}
1327
1328
		return $res;
1329
	}
1330
1331
	/**
1332
	 * Add an additional set of mapping pairs to a internal map
1333
	 *
1334
	 * @param string $conjunction
1335
	 * @param string $string
1336
	 * @param string $type
1337
	 * @return void
1338
	 */
1339
	protected function _appendMap(string $conjunction = '', string $string = '', string $type = '')
1340
	{
1341
		$this->queryMap[] = [
1342
			'type' => $type,
1343
			'conjunction' => $conjunction,
1344
			'string' => $string
1345
		];
1346
	}
1347
1348
	/**
1349
	 * Convert the prepared statement into readable sql
1350
	 *
1351
	 * @param array $vals
1352
	 * @param string $sql
1353
	 * @param int $totalTime
1354
	 * @return void
1355
	 */
1356
	protected function _appendQuery($vals, string $sql, int $totalTime)
1357
	{
1358
		$evals = \is_array($vals) ? $vals : [];
1359
		$esql = str_replace('?', "%s", $sql);
1360
1361
		// Quote string values
1362
		foreach($evals as &$v)
1363
		{
1364
			$v = ( ! is_numeric($v))
1365
				? htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8')
1366
				: $v;
1367
		}
1368
1369
		// Add the query onto the array of values to pass
1370
		// as arguments to sprintf
1371
		array_unshift($evals, $esql);
1372
1373
		// Add the interpreted query to the list of executed queries
1374
		$this->queries[] = [
1375
			'time' => $totalTime,
1376
			'sql' => sprintf(...$evals)
1377
		];
1378
1379
		$this->queries['total_time'] += $totalTime;
1380
1381
		// Set the last query to get rowcounts properly
1382
		$this->driver->setLastQuery($sql);
1383
	}
1384
1385
	/**
1386
	 * Sub-method for generating sql strings
1387
	 *
1388
	 * @param string $type
1389
	 * @param string $table
1390
	 * @return string
1391
	 */
1392
	protected function _compileType(string $type='', string $table=''): string
1393
	{
1394
		switch($type)
1395
		{
1396
			case 'insert':
1397
				$paramCount = count($this->setArrayKeys);
1398
				$params = array_fill(0, $paramCount, '?');
1399
				$sql = "INSERT INTO {$table} ("
1400
					. implode(',', $this->setArrayKeys)
1401
					. ")\nVALUES (".implode(',', $params).')';
1402
				break;
1403
1404
			case 'update':
1405
				$sql = "UPDATE {$table}\nSET {$this->setString}";
1406
				break;
1407
1408
			case 'replace':
1409
				// @TODO implement
1410
				$sql = '';
1411
				break;
1412
1413
			case 'delete':
1414
				$sql = "DELETE FROM {$table}";
1415
				break;
1416
1417
			// Get queries
1418
			default:
1419
				$sql = "SELECT * \nFROM {$this->fromString}";
1420
1421
				// Set the select string
1422
				if ( ! empty($this->selectString))
1423
				{
1424
					// Replace the star with the selected fields
1425
					$sql = str_replace('*', $this->selectString, $sql);
1426
				}
1427
				break;
1428
		}
1429
1430
		return $sql;
1431
	}
1432
1433
	/**
1434
	 * String together the sql statements for sending to the db
1435
	 *
1436
	 * @param string $type
1437
	 * @param string $table
1438
	 * @return string
1439
	 */
1440
	protected function _compile(string $type='', string $table=''): string
1441
	{
1442
		// Get the base clause for the query
1443
		$sql = $this->_compileType($type, $this->driver->quoteTable($table));
1444
1445
		$clauses = [
1446
			'queryMap',
1447
			'groupString',
1448
			'orderString',
1449
			'havingMap',
1450
		];
1451
1452
		// Set each type of subclause
1453
		foreach($clauses as $clause)
1454
		{
1455
			$param = $this->$clause;
1456
			if (\is_array($param))
1457
			{
1458
				foreach($param as $q)
1459
				{
1460
					$sql .= $q['conjunction'] . $q['string'];
1461
				}
1462
			}
1463
			else
1464
			{
1465
				$sql .= $param;
1466
			}
1467
		}
1468
1469
		// Set the limit via the class variables
1470
		if (is_numeric($this->limit))
1471
		{
1472
			$sql = $this->sql->limit($sql, $this->limit, $this->offset);
1473
		}
1474
1475
		// See if the query plan, rather than the
1476
		// query data should be returned
1477
		if ($this->explain === TRUE)
1478
		{
1479
			$sql = $this->sql->explain($sql);
1480
		}
1481
1482
		return $sql;
1483
	}
1484
}
1485