Completed
Push — develop ( 2839e6...41394c )
by Timothy
04:15
created

QueryBuilder::replace()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 9
rs 9.6666
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\DriverInterface;
20
21
/**
22
 * Convenience class for creating sql queries
23
 */
24
class QueryBuilder implements QueryBuilderInterface {
25
26
	/**
27
	 * Convenience property for connection management
28
	 * @var string
29
	 */
30
	public $connName = '';
31
32
	/**
33
	 * List of queries executed
34
	 * @var array
35
	 */
36
	public $queries = [
37
		'total_time' => 0
38
	];
39
40
	/**
41
	 * Whether to do only an explain on the query
42
	 * @var boolean
43
	 */
44
	protected $explain = FALSE;
45
46
	/**
47
	 * The current database driver
48
	 * @var DriverInterface
49
	 */
50
	public $driver;
51
52
	/**
53
	 * Query parser class instance
54
	 * @var QueryParser
55
	 */
56
	protected $parser;
57
58
	/**
59
	 * Query Builder state
60
	 * @var State
61
	 */
62
	protected $state;
63
64
	// --------------------------------------------------------------------------
65
	// ! Methods
66
	// --------------------------------------------------------------------------
67
68
	/**
69
	 * Constructor
70
	 *
71
	 * @param DriverInterface $driver
72
	 * @param QueryParser $parser
73
	 */
74
	public function __construct(DriverInterface $driver, QueryParser $parser)
75
	{
76
		// Inject driver and parser
77
		$this->driver = $driver;
78
		$this->parser = $parser;
79
80
		// Create new State object
81
		$this->state = new State();
82
	}
83
84
	/**
85
	 * Destructor
86
	 * @codeCoverageIgnore
87
	 */
88
	public function __destruct()
89
	{
90
		$this->driver = NULL;
91
	}
92
93
	/**
94
	 * Calls a function further down the inheritance chain
95
	 *
96
	 * @param string $name
97
	 * @param array $params
98
	 * @return mixed
99
	 * @throws BadMethodCallException
100
	 */
101
	public function __call(string $name, array $params)
102
	{
103
		if (method_exists($this->driver, $name))
104
		{
105
			return \call_user_func_array([$this->driver, $name], $params);
106
		}
107
108
		throw new BadMethodCallException('Method does not exist');
109
	}
110
111
	// --------------------------------------------------------------------------
112
	// ! Select Queries
113
	// --------------------------------------------------------------------------
114
115
	/**
116
	 * Specifies rows to select in a query
117
	 *
118
	 * @param string $fields
119
	 * @return QueryBuilderInterface
120
	 */
121
	public function select(string $fields): QueryBuilderInterface
122
	{
123
		// Split fields by comma
124
		$fieldsArray = explode(',', $fields);
125
		$fieldsArray = array_map('mb_trim', $fieldsArray);
126
127
		// Split on 'As'
128
		foreach ($fieldsArray as $key => $field)
129
		{
130
			if (stripos($field, 'as') !== FALSE)
131
			{
132
				$fieldsArray[$key] = preg_split('` as `i', $field);
133
				$fieldsArray[$key] = array_map('mb_trim', $fieldsArray[$key]);
134
			}
135
		}
136
137
		// Quote the identifiers
138
		$safeArray = $this->driver->quoteIdent($fieldsArray);
139
140
		unset($fieldsArray);
141
142
		// Join the strings back together
143
		for($i = 0, $c = count($safeArray); $i < $c; $i++)
144
		{
145
			if (\is_array($safeArray[$i]))
146
			{
147
				$safeArray[$i] = implode(' AS ', $safeArray[$i]);
148
			}
149
		}
150
151
		$this->state->appendSelectString(implode(', ', $safeArray));
152
153
		return $this;
154
	}
155
156
	/**
157
	 * Selects the maximum value of a field from a query
158
	 *
159
	 * @param string $field
160
	 * @param string|bool $as
161
	 * @return QueryBuilderInterface
162
	 */
163
	public function selectMax(string $field, $as=FALSE): QueryBuilderInterface
164
	{
165
		// Create the select string
166
		$this->state->appendSelectString(' MAX'.$this->_select($field, $as));
167
		return $this;
168
	}
169
170
	/**
171
	 * Selects the minimum value of a field from a query
172
	 *
173
	 * @param string $field
174
	 * @param string|bool $as
175
	 * @return QueryBuilderInterface
176
	 */
177
	public function selectMin(string $field, $as=FALSE): QueryBuilderInterface
178
	{
179
		// Create the select string
180
		$this->state->appendSelectString(' MIN'.$this->_select($field, $as));
181
		return $this;
182
	}
183
184
	/**
185
	 * Selects the average value of a field from a query
186
	 *
187
	 * @param string $field
188
	 * @param string|bool $as
189
	 * @return QueryBuilderInterface
190
	 */
191
	public function selectAvg(string $field, $as=FALSE): QueryBuilderInterface
192
	{
193
		// Create the select string
194
		$this->state->appendSelectString(' AVG'.$this->_select($field, $as));
195
		return $this;
196
	}
197
198
	/**
199
	 * Selects the sum of a field from a query
200
	 *
201
	 * @param string $field
202
	 * @param string|bool $as
203
	 * @return QueryBuilderInterface
204
	 */
205
	public function selectSum(string $field, $as=FALSE): QueryBuilderInterface
206
	{
207
		// Create the select string
208
		$this->state->appendSelectString(' SUM'.$this->_select($field, $as));
209
		return $this;
210
	}
211
212
	/**
213
	 * Adds the 'distinct' keyword to a query
214
	 *
215
	 * @return QueryBuilderInterface
216
	 */
217
	public function distinct(): QueryBuilderInterface
218
	{
219
		// Prepend the keyword to the select string
220
		$this->state->setSelectString(' DISTINCT' . $this->state->getSelectString());
221
		return $this;
222
	}
223
224
	/**
225
	 * Tell the database to give you the query plan instead of result set
226
	 *
227
	 * @return QueryBuilderInterface
228
	 */
229
	public function explain(): QueryBuilderInterface
230
	{
231
		$this->explain = TRUE;
232
		return $this;
233
	}
234
235
	/**
236
	 * Specify the database table to select from
237
	 *
238
	 * @param string $tblname
239
	 * @return QueryBuilderInterface
240
	 */
241
	public function from(string $tblname): QueryBuilderInterface
242
	{
243
		// Split identifiers on spaces
244
		$identArray = explode(' ', \mb_trim($tblname));
245
		$identArray = array_map('\\mb_trim', $identArray);
246
247
		// Quote the identifiers
248
		$identArray[0] = $this->driver->quoteTable($identArray[0]);
249
		$identArray = $this->driver->quoteIdent($identArray);
250
251
		// Paste it back together
252
		$this->state->setFromString(implode(' ', $identArray));
253
254
		return $this;
255
	}
256
257
	// --------------------------------------------------------------------------
258
	// ! 'Like' methods
259
	// --------------------------------------------------------------------------
260
261
	/**
262
	 * Creates a Like clause in the sql statement
263
	 *
264
	 * @param string $field
265
	 * @param mixed $val
266
	 * @param string $pos
267
	 * @return QueryBuilderInterface
268
	 */
269
	public function like(string $field, $val, string $pos='both'): QueryBuilderInterface
270
	{
271
		return $this->_like($field, $val, $pos);
272
	}
273
274
	/**
275
	 * Generates an OR Like clause
276
	 *
277
	 * @param string $field
278
	 * @param mixed $val
279
	 * @param string $pos
280
	 * @return QueryBuilderInterface
281
	 */
282
	public function orLike(string $field, $val, string $pos='both'): QueryBuilderInterface
283
	{
284
		return $this->_like($field, $val, $pos, 'LIKE', 'OR');
285
	}
286
287
	/**
288
	 * Generates a NOT LIKE clause
289
	 *
290
	 * @param string $field
291
	 * @param mixed $val
292
	 * @param string $pos
293
	 * @return QueryBuilderInterface
294
	 */
295
	public function notLike(string $field, $val, string $pos='both'): QueryBuilderInterface
296
	{
297
		return $this->_like($field, $val, $pos, 'NOT LIKE');
298
	}
299
300
	/**
301
	 * Generates a OR NOT LIKE clause
302
	 *
303
	 * @param string $field
304
	 * @param mixed $val
305
	 * @param string $pos
306
	 * @return QueryBuilderInterface
307
	 */
308
	public function orNotLike(string $field, $val, string $pos='both'): QueryBuilderInterface
309
	{
310
		return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR');
311
	}
312
313
	// --------------------------------------------------------------------------
314
	// ! Having methods
315
	// --------------------------------------------------------------------------
316
317
	/**
318
	 * Generates a 'Having' clause
319
	 *
320
	 * @param mixed $key
321
	 * @param mixed $val
322
	 * @return QueryBuilderInterface
323
	 */
324
	public function having($key, $val=[]): QueryBuilderInterface
325
	{
326
		return $this->_having($key, $val);
327
	}
328
329
	/**
330
	 * Generates a 'Having' clause prefixed with 'OR'
331
	 *
332
	 * @param mixed $key
333
	 * @param mixed $val
334
	 * @return QueryBuilderInterface
335
	 */
336
	public function orHaving($key, $val=[]): QueryBuilderInterface
337
	{
338
		return $this->_having($key, $val, 'OR');
339
	}
340
341
	// --------------------------------------------------------------------------
342
	// ! 'Where' methods
343
	// --------------------------------------------------------------------------
344
345
	/**
346
	 * Specify condition(s) in the where clause of a query
347
	 * Note: this function works with key / value, or a
348
	 * passed array with key / value pairs
349
	 *
350
	 * @param mixed $key
351
	 * @param mixed $val
352
	 * @param mixed $escape
353
	 * @return QueryBuilderInterface
354
	 */
355
	public function where($key, $val=[], $escape=NULL): QueryBuilderInterface
356
	{
357
		return $this->_whereString($key, $val);
358
	}
359
360
	/**
361
	 * Where clause prefixed with "OR"
362
	 *
363
	 * @param string $key
364
	 * @param mixed $val
365
	 * @return QueryBuilderInterface
366
	 */
367
	public function orWhere($key, $val=[]): QueryBuilderInterface
368
	{
369
		return $this->_whereString($key, $val, 'OR');
370
	}
371
372
	/**
373
	 * Where clause with 'IN' statement
374
	 *
375
	 * @param mixed $field
376
	 * @param mixed $val
377
	 * @return QueryBuilderInterface
378
	 */
379
	public function whereIn($field, $val=[]): QueryBuilderInterface
380
	{
381
		return $this->_whereIn($field, $val);
382
	}
383
384
	/**
385
	 * Where in statement prefixed with "or"
386
	 *
387
	 * @param string $field
388
	 * @param mixed $val
389
	 * @return QueryBuilderInterface
390
	 */
391
	public function orWhereIn($field, $val=[]): QueryBuilderInterface
392
	{
393
		return $this->_whereIn($field, $val, 'IN', 'OR');
394
	}
395
396
	/**
397
	 * WHERE NOT IN (FOO) clause
398
	 *
399
	 * @param string $field
400
	 * @param mixed $val
401
	 * @return QueryBuilderInterface
402
	 */
403
	public function whereNotIn($field, $val=[]): QueryBuilderInterface
404
	{
405
		return $this->_whereIn($field, $val, 'NOT IN');
406
	}
407
408
	/**
409
	 * OR WHERE NOT IN (FOO) clause
410
	 *
411
	 * @param string $field
412
	 * @param mixed $val
413
	 * @return QueryBuilderInterface
414
	 */
415
	public function orWhereNotIn($field, $val=[]): QueryBuilderInterface
416
	{
417
		return $this->_whereIn($field, $val, 'NOT IN', 'OR');
418
	}
419
420
	// --------------------------------------------------------------------------
421
	// ! Other Query Modifier methods
422
	// --------------------------------------------------------------------------
423
424
	/**
425
	 * Sets values for inserts / updates / deletes
426
	 *
427
	 * @param mixed $key
428
	 * @param mixed $val
429
	 * @return QueryBuilderInterface
430
	 */
431
	public function set($key, $val = NULL): QueryBuilderInterface
432
	{
433
		if (is_scalar($key))
434
		{
435
			$pairs = [$key => $val];
436
		}
437
		else
438
		{
439
			$pairs = $key;
440
		}
441
442
		$keys = array_keys($pairs);
443
		$values = array_values($pairs);
444
445
		$this->state->appendSetArrayKeys($keys);
446
		$this->state->appendValues($values);
447
448
		// Use the keys of the array to make the insert/update string
449
		// Escape the field names
450
		$this->state->setSetArrayKeys(
451
			array_map([$this->driver, '_quote'], $this->state->getSetArrayKeys())
452
		);
453
454
		// Generate the "set" string
455
		$setString = implode('=?,', $this->state->getSetArrayKeys());
456
		$setString .= '=?';
457
458
		$this->state->setSetString($setString);
459
460
		return $this;
461
	}
462
463
	/**
464
	 * Creates a join phrase in a compiled query
465
	 *
466
	 * @param string $table
467
	 * @param string $condition
468
	 * @param string $type
469
	 * @return QueryBuilderInterface
470
	 */
471
	public function join(string $table, string $condition, string $type=''): QueryBuilderInterface
472
	{
473
		// Prefix and quote table name
474
		$table = explode(' ', mb_trim($table));
475
		$table[0] = $this->driver->quoteTable($table[0]);
476
		$table = $this->driver->quoteIdent($table);
477
		$table = implode(' ', $table);
478
479
		// Parse out the join condition
480
		$parsedCondition = $this->parser->compileJoin($condition);
481
		$condition = $table . ' ON ' . $parsedCondition;
482
483
		$this->state->appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, 'join');
484
485
		return $this;
486
	}
487
488
	/**
489
	 * Group the results by the selected field(s)
490
	 *
491
	 * @param mixed $field
492
	 * @return QueryBuilderInterface
493
	 */
494
	public function groupBy($field): QueryBuilderInterface
495
	{
496
		if ( ! is_scalar($field))
497
		{
498
			$newGroupArray = array_map([$this->driver, 'quoteIdent'], $field);
499
			$this->state->setGroupArray(
500
				array_merge($this->state->getGroupArray(), $newGroupArray)
501
			);
502
		}
503
		else
504
		{
505
			$this->state->appendGroupArray($this->driver->quoteIdent($field));
0 ignored issues
show
Bug introduced by
It seems like $this->driver->quoteIdent($field) targeting Query\Drivers\DriverInterface::quoteIdent() can also be of type array; however, Query\State::appendGroupArray() 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...
506
		}
507
508
		$this->state->setGroupString(' GROUP BY ' . implode(',', $this->state->getGroupArray()));
509
510
		return $this;
511
	}
512
513
	/**
514
	 * Order the results by the selected field(s)
515
	 *
516
	 * @param string $field
517
	 * @param string $type
518
	 * @return QueryBuilderInterface
519
	 */
520
	public function orderBy(string $field, string $type=''): QueryBuilderInterface
521
	{
522
		// When ordering by random, do an ascending order if the driver
523
		// doesn't support random ordering
524
		if (stripos($type, 'rand') !== FALSE)
525
		{
526
			$rand = $this->driver->getSql()->random();
527
			$type = $rand ?? 'ASC';
528
		}
529
530
		// Set fields for later manipulation
531
		$field = $this->driver->quoteIdent($field);
532
		$this->state->setOrderArray($field, $type);
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->driver->quoteIdent($field) on line 531 can also be of type array; however, Query\State::setOrderArray() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
533
534
		$orderClauses = [];
535
536
		// Flatten key/val pairs into an array of space-separated pairs
537
		foreach($this->state->getOrderArray() as $k => $v)
538
		{
539
			$orderClauses[] = $k . ' ' . strtoupper($v);
540
		}
541
542
		// Set the final string
543
		$orderString =  ! isset($rand)
544
			? "\nORDER BY ".implode(', ', $orderClauses)
545
			: "\nORDER BY".$rand;
546
547
		$this->state->setOrderString($orderString);
548
549
		return $this;
550
	}
551
552
	/**
553
	 * Set a limit on the current sql statement
554
	 *
555
	 * @param int $limit
556
	 * @param int|bool $offset
557
	 * @return QueryBuilderInterface
558
	 */
559
	public function limit(int $limit, $offset=FALSE): QueryBuilderInterface
560
	{
561
		$this->state->setLimit($limit);
562
		$this->state->setOffset($offset);
0 ignored issues
show
Documentation introduced by
$offset is of type integer|boolean, but the function expects a string|false.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
563
564
		return $this;
565
	}
566
567
	// --------------------------------------------------------------------------
568
	// ! Query Grouping Methods
569
	// --------------------------------------------------------------------------
570
571
	/**
572
	 * Adds a paren to the current query for query grouping
573
	 *
574
	 * @return QueryBuilderInterface
575
	 */
576
	public function groupStart(): QueryBuilderInterface
577
	{
578
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' ';
579
580
		$this->state->appendMap($conj, '(', 'group_start');
581
582
		return $this;
583
	}
584
585
	/**
586
	 * Adds a paren to the current query for query grouping,
587
	 * prefixed with 'NOT'
588
	 *
589
	 * @return QueryBuilderInterface
590
	 */
591
	public function notGroupStart(): QueryBuilderInterface
592
	{
593
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' AND ';
594
595
		$this->state->appendMap($conj, ' NOT (', 'group_start');
596
597
		return $this;
598
	}
599
600
	/**
601
	 * Adds a paren to the current query for query grouping,
602
	 * prefixed with 'OR'
603
	 *
604
	 * @return QueryBuilderInterface
605
	 */
606
	public function orGroupStart(): QueryBuilderInterface
607
	{
608
		$this->state->appendMap('', ' OR (', 'group_start');
609
610
		return $this;
611
	}
612
613
	/**
614
	 * Adds a paren to the current query for query grouping,
615
	 * prefixed with 'OR NOT'
616
	 *
617
	 * @return QueryBuilderInterface
618
	 */
619
	public function orNotGroupStart(): QueryBuilderInterface
620
	{
621
		$this->state->appendMap('', ' OR NOT (', 'group_start');
622
623
		return $this;
624
	}
625
626
	/**
627
	 * Ends a query group
628
	 *
629
	 * @return QueryBuilderInterface
630
	 */
631
	public function groupEnd(): QueryBuilderInterface
632
	{
633
		$this->state->appendMap('', ')', 'group_end');
634
635
		return $this;
636
	}
637
638
	// --------------------------------------------------------------------------
639
	// ! Query execution methods
640
	// --------------------------------------------------------------------------
641
642
	/**
643
	 * Select and retrieve all records from the current table, and/or
644
	 * execute current compiled query
645
	 *
646
	 * @param string $table
647
	 * @param int|bool $limit
648
	 * @param int|bool $offset
649
	 * @return PDOStatement
650
	 */
651
	public function get(string $table='', $limit=FALSE, $offset=FALSE): PDOStatement
652
	{
653
		// Set the table
654
		if ( ! empty($table))
655
		{
656
			$this->from($table);
657
		}
658
659
		// Set the limit, if it exists
660
		if (\is_int($limit))
661
		{
662
			$this->limit($limit, $offset);
663
		}
664
665
		return $this->_run('get', $table);
666
	}
667
668
	/**
669
	 * Convenience method for get() with a where clause
670
	 *
671
	 * @param string $table
672
	 * @param mixed $where
673
	 * @param int|bool $limit
674
	 * @param int|bool $offset
675
	 * @return PDOStatement
676
	 */
677
	public function getWhere(string $table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement
678
	{
679
		// Create the where clause
680
		$this->where($where);
681
682
		// Return the result
683
		return $this->get($table, $limit, $offset);
684
	}
685
686
	/**
687
	 * Retrieve the number of rows in the selected table
688
	 *
689
	 * @param string $table
690
	 * @return int
691
	 */
692
	public function countAll(string $table): int
693
	{
694
		$sql = 'SELECT * FROM '.$this->driver->quoteTable($table);
695
		$res = $this->driver->query($sql);
696
		return (int) count($res->fetchAll());
697
	}
698
699
	/**
700
	 * Retrieve the number of results for the generated query - used
701
	 * in place of the get() method
702
	 *
703
	 * @param string $table
704
	 * @param boolean $reset
705
	 * @return int
706
	 */
707
	public function countAllResults(string $table='', bool $reset = TRUE): int
708
	{
709
		// Set the table
710
		if ( ! empty($table))
711
		{
712
			$this->from($table);
713
		}
714
715
		$result = $this->_run('get', $table, NULL, NULL, $reset);
716
		$rows = $result->fetchAll();
717
718
		return (int) count($rows);
719
	}
720
721
	/**
722
	 * Creates an insert clause, and executes it
723
	 *
724
	 * @param string $table
725
	 * @param mixed $data
726
	 * @return PDOStatement
727
	 */
728
	public function insert(string $table, $data=[]): PDOStatement
729
	{
730
		if ( ! empty($data))
731
		{
732
			$this->set($data);
733
		}
734
735
		return $this->_run('insert', $table);
736
	}
737
738
	/**
739
	 * Creates and executes a batch insertion query
740
	 *
741
	 * @param string $table
742
	 * @param array $data
743
	 * @return PDOStatement
744
	 */
745
	public function insertBatch(string $table, $data=[]): PDOStatement
746
	{
747
		// Get the generated values and sql string
748
		[$sql, $data] = $this->driver->insertBatch($table, $data);
0 ignored issues
show
Bug introduced by
The variable $sql does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
749
750
		return $sql !== NULL
751
			? $this->_run('', $table, $sql, $data)
752
			: NULL;
753
	}
754
755
	/**
756
	 * Creates an update clause, and executes it
757
	 *
758
	 * @param string $table
759
	 * @param mixed $data
760
	 * @return PDOStatement
761
	 */
762
	public function update(string $table, $data=[]): PDOStatement
763
	{
764
		if ( ! empty($data))
765
		{
766
			$this->set($data);
767
		}
768
769
		return $this->_run('update', $table);
770
	}
771
772
	/**
773
	 * Creates a batch update, and executes it.
774
	 * Returns the number of affected rows
775
	 *
776
	 * @param string $table
777
	 * @param array $data
778
	 * @param string $where
779
	 * @return int|null
780
	 */
781
	public function updateBatch(string $table, array $data, string $where): ?int
782
	{
783
		if (empty($table) || empty($data) || empty($where))
784
		{
785
			return NULL;
786
		}
787
788
		// Get the generated values and sql string
789
		[$sql, $data, $affectedRows] = $this->driver->updateBatch($table, $data, $where);
0 ignored issues
show
Bug introduced by
The variable $sql does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $affectedRows does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
790
791
		$this->_run('', $table, $sql, $data);
792
		return $affectedRows;
793
	}
794
795
	/**
796
	 * Deletes data from a table
797
	 *
798
	 * @param string $table
799
	 * @param mixed $where
800
	 * @return PDOStatement
801
	 */
802
	public function delete(string $table, $where=''): PDOStatement
803
	{
804
		// Set the where clause
805
		if ( ! empty($where))
806
		{
807
			$this->where($where);
808
		}
809
810
		return $this->_run('delete', $table);
811
	}
812
813
	// --------------------------------------------------------------------------
814
	// ! SQL Returning Methods
815
	// --------------------------------------------------------------------------
816
817
	/**
818
	 * Returns the generated 'select' sql query
819
	 *
820
	 * @param string $table
821
	 * @param bool $reset
822
	 * @return string
823
	 */
824
	public function getCompiledSelect(string $table='', bool $reset=TRUE): string
825
	{
826
		// Set the table
827
		if ( ! empty($table))
828
		{
829
			$this->from($table);
830
		}
831
832
		return $this->_getCompile('select', $table, $reset);
833
	}
834
835
	/**
836
	 * Returns the generated 'insert' sql query
837
	 *
838
	 * @param string $table
839
	 * @param bool $reset
840
	 * @return string
841
	 */
842
	public function getCompiledInsert(string $table, bool $reset=TRUE): string
843
	{
844
		return $this->_getCompile('insert', $table, $reset);
845
	}
846
847
	/**
848
	 * Returns the generated 'update' sql query
849
	 *
850
	 * @param string $table
851
	 * @param bool $reset
852
	 * @return string
853
	 */
854
	public function getCompiledUpdate(string $table='', bool $reset=TRUE): string
855
	{
856
		return $this->_getCompile('update', $table, $reset);
857
	}
858
859
	/**
860
	 * Returns the generated 'delete' sql query
861
	 *
862
	 * @param string $table
863
	 * @param bool $reset
864
	 * @return string
865
	 */
866
	public function getCompiledDelete(string $table='', bool $reset=TRUE): string
867
	{
868
		return $this->_getCompile('delete', $table, $reset);
869
	}
870
871
	// --------------------------------------------------------------------------
872
	// ! Miscellaneous Methods
873
	// --------------------------------------------------------------------------
874
875
	/**
876
	 * Clear out the class variables, so the next query can be run
877
	 *
878
	 * @return void
879
	 */
880
	public function resetQuery(): void
881
	{
882
		$this->state = new State();
883
		$this->explain = FALSE;
884
	}
885
886
	/**
887
	 * Method to simplify select_ methods
888
	 *
889
	 * @param string $field
890
	 * @param string|bool $as
891
	 * @return string
892
	 */
893
	protected function _select(string $field, $as = FALSE): string
894
	{
895
		// Escape the identifiers
896
		$field = $this->driver->quoteIdent($field);
897
898
		if ( ! \is_string($as))
899
		{
900
			// @codeCoverageIgnoreStart
901
			return $field;
902
			// @codeCoverageIgnoreEnd
903
		}
904
905
		$as = $this->driver->quoteIdent($as);
906
		return "({$field}) AS {$as} ";
907
	}
908
909
	/**
910
	 * Helper function for returning sql strings
911
	 *
912
	 * @param string $type
913
	 * @param string $table
914
	 * @param bool $reset
915
	 * @return string
916
	 */
917
	protected function _getCompile(string $type, string $table, bool $reset): string
918
	{
919
		$sql = $this->_compile($type, $table);
920
921
		// Reset the query builder for the next query
922
		if ($reset)
923
		{
924
			$this->resetQuery();
925
		}
926
927
		return $sql;
928
	}
929
930
	/**
931
	 * Simplify 'like' methods
932
	 *
933
	 * @param string $field
934
	 * @param mixed $val
935
	 * @param string $pos
936
	 * @param string $like
937
	 * @param string $conj
938
	 * @return self
939
	 */
940
	protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): self
941
	{
942
		$field = $this->driver->quoteIdent($field);
943
944
		// Add the like string into the order map
945
		$like = $field. " {$like} ?";
946
947
		if ($pos === 'before')
948
		{
949
			$val = "%{$val}";
950
		}
951
		elseif ($pos === 'after')
952
		{
953
			$val = "{$val}%";
954
		}
955
		else
956
		{
957
			$val = "%{$val}%";
958
		}
959
960
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
961
		$this->state->appendMap($conj, $like, 'like');
962
963
		// Add to the values array
964
		$this->state->appendWhereValues($val);
965
966
		return $this;
967
	}
968
969
	/**
970
	 * Simplify building having clauses
971
	 *
972
	 * @param mixed $key
973
	 * @param mixed $values
974
	 * @param string $conj
975
	 * @return self
976
	 */
977
	protected function _having($key, $values=[], string $conj='AND'): self
978
	{
979
		$where = $this->_where($key, $values);
980
981
		// Create key/value placeholders
982
		foreach($where as $f => $val)
983
		{
984
			// Split each key by spaces, in case there
985
			// is an operator such as >, <, !=, etc.
986
			$fArray = explode(' ', trim($f));
987
988
			$item = $this->driver->quoteIdent($fArray[0]);
989
990
			// Simple key value, or an operator
991
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
992
993
			// Put in the having map
994
			$this->state->appendHavingMap([
995
				'conjunction' => empty($this->state->getHavingMap())
996
					? ' HAVING '
997
					: " {$conj} ",
998
				'string' => $item
999
			]);
1000
		}
1001
1002
		return $this;
1003
	}
1004
1005
	/**
1006
	 * Do all the redundant stuff for where/having type methods
1007
	 *
1008
	 * @param mixed $key
1009
	 * @param mixed $val
1010
	 * @return array
1011
	 */
1012
	protected function _where($key, $val=[]): array
1013
	{
1014
		$where = [];
1015
		$pairs = [];
1016
1017
		if (is_scalar($key))
1018
		{
1019
			$pairs[$key] = $val;
1020
		}
1021
		else
1022
		{
1023
			$pairs = $key;
1024
		}
1025
1026
		foreach($pairs as $k => $v)
0 ignored issues
show
Bug introduced by
The expression $pairs of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1027
		{
1028
			$where[$k] = $v;
1029
			$this->state->appendWhereValues($v);
1030
		}
1031
1032
		return $where;
1033
	}
1034
1035
	/**
1036
	 * Simplify generating where string
1037
	 *
1038
	 * @param mixed $key
1039
	 * @param mixed $values
1040
	 * @param string $defaultConj
1041
	 * @return self
1042
	 */
1043
	protected function _whereString($key, $values=[], string $defaultConj='AND'): self
1044
	{
1045
		// Create key/value placeholders
1046
		foreach($this->_where($key, $values) as $f => $val)
1047
		{
1048
			$queryMap = $this->state->getQueryMap();
1049
1050
			// Split each key by spaces, in case there
1051
			// is an operator such as >, <, !=, etc.
1052
			$fArray = explode(' ', trim($f));
1053
1054
			$item = $this->driver->quoteIdent($fArray[0]);
1055
1056
			// Simple key value, or an operator
1057
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
1058
			$lastItem = end($queryMap);
1059
1060
			// Determine the correct conjunction
1061
			$conjunctionList = array_column($queryMap, 'conjunction');
1062
			if (empty($queryMap) || ( ! \regexInArray($conjunctionList, "/^ ?\n?WHERE/i")))
1063
			{
1064
				$conj = "\nWHERE ";
1065
			}
1066
			elseif ($lastItem['type'] === 'group_start')
1067
			{
1068
				$conj = '';
1069
			}
1070
			else
1071
			{
1072
				$conj = " {$defaultConj} ";
1073
			}
1074
1075
			$this->state->appendMap($conj, $item, 'where');
1076
		}
1077
1078
		return $this;
1079
	}
1080
1081
	/**
1082
	 * Simplify where_in methods
1083
	 *
1084
	 * @param mixed $key
1085
	 * @param mixed $val
1086
	 * @param string $in - The (not) in fragment
1087
	 * @param string $conj - The where in conjunction
1088
	 * @return self
1089
	 */
1090
	protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): self
1091
	{
1092
		$key = $this->driver->quoteIdent($key);
1093
		$params = array_fill(0, count($val), '?');
1094
		$this->state->appendWhereValues($val);
1095
1096
		$conjunction =  empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
1097
		$str = $key . " {$in} (".implode(',', $params).') ';
1098
1099
		$this->state->appendMap($conjunction, $str, 'where_in');
1100
1101
		return $this;
1102
	}
1103
1104
	/**
1105
	 * Executes the compiled query
1106
	 *
1107
	 * @param string $type
1108
	 * @param string $table
1109
	 * @param string $sql
1110
	 * @param array|null $vals
1111
	 * @param boolean $reset
1112
	 * @return PDOStatement
1113
	 */
1114
	protected function _run(string $type, string $table, string $sql=NULL, array $vals=NULL, bool $reset=TRUE): PDOStatement
1115
	{
1116
		if ($sql === NULL)
1117
		{
1118
			$sql = $this->_compile($type, $table);
1119
		}
1120
1121
		if ($vals === NULL)
1122
		{
1123
			$vals = array_merge($this->state->getValues(), $this->state->getWhereValues());
1124
		}
1125
1126
		$startTime = microtime(TRUE);
1127
1128
		$res = empty($vals)
1129
			? $this->driver->query($sql)
1130
			: $this->driver->prepareExecute($sql, $vals);
1131
1132
		$endTime = microtime(TRUE);
1133
		$totalTime = number_format($endTime - $startTime, 5);
1134
1135
		// Add this query to the list of executed queries
1136
		$this->_appendQuery($vals, $sql, (int) $totalTime);
1137
1138
		// Reset class state for next query
1139
		if ($reset)
1140
		{
1141
			$this->resetQuery();
1142
		}
1143
1144
		return $res;
1145
	}
1146
1147
	/**
1148
	 * Convert the prepared statement into readable sql
1149
	 *
1150
	 * @param array $values
1151
	 * @param string $sql
1152
	 * @param int $totalTime
1153
	 * @return void
1154
	 */
1155
	protected function _appendQuery(array $values = NULL, string $sql, int $totalTime): void
1156
	{
1157
		$evals = \is_array($values) ? $values : [];
1158
		$esql = str_replace('?', "%s", $sql);
1159
1160
		// Quote string values
1161
		foreach($evals as &$v)
1162
		{
1163
			$v = ( ! is_numeric($v))
1164
				? htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8')
1165
				: $v;
1166
		}
1167
1168
		// Add the query onto the array of values to pass
1169
		// as arguments to sprintf
1170
		array_unshift($evals, $esql);
1171
1172
		// Add the interpreted query to the list of executed queries
1173
		$this->queries[] = [
1174
			'time' => $totalTime,
1175
			'sql' => sprintf(...$evals)
1176
		];
1177
1178
		$this->queries['total_time'] += $totalTime;
1179
1180
		// Set the last query to get rowcounts properly
1181
		$this->driver->setLastQuery($sql);
1182
	}
1183
1184
	/**
1185
	 * Sub-method for generating sql strings
1186
	 *
1187
	 * @codeCoverageIgnore
1188
	 * @param string $type
1189
	 * @param string $table
1190
	 * @return string
1191
	 */
1192
	protected function _compileType(string $type='', string $table=''): string
1193
	{
1194
		$setArrayKeys = $this->state->getSetArrayKeys();
1195
		switch($type)
1196
		{
1197
			case 'insert':
1198
				$paramCount = count($setArrayKeys);
1199
				$params = array_fill(0, $paramCount, '?');
1200
				$sql = "INSERT INTO {$table} ("
1201
					. implode(',', $setArrayKeys)
1202
					. ")\nVALUES (".implode(',', $params).')';
1203
				break;
1204
1205
			case 'update':
1206
				$setString = $this->state->getSetString();
1207
				$sql = "UPDATE {$table}\nSET {$setString}";
1208
				break;
1209
1210
			case 'delete':
1211
				$sql = "DELETE FROM {$table}";
1212
				break;
1213
1214
			// Get queries
1215
			default:
1216
				$fromString = $this->state->getFromString();
1217
				$selectString = $this->state->getSelectString();
1218
1219
				$sql = "SELECT * \nFROM {$fromString}";
1220
1221
				// Set the select string
1222
				if ( ! empty($selectString))
1223
				{
1224
					// Replace the star with the selected fields
1225
					$sql = str_replace('*', $selectString, $sql);
1226
				}
1227
				break;
1228
		}
1229
1230
		return $sql;
1231
	}
1232
1233
	/**
1234
	 * String together the sql statements for sending to the db
1235
	 *
1236
	 * @param string $type
1237
	 * @param string $table
1238
	 * @return string
1239
	 */
1240
	protected function _compile(string $type='', string $table=''): string
1241
	{
1242
		// Get the base clause for the query
1243
		$sql = $this->_compileType($type, $this->driver->quoteTable($table));
0 ignored issues
show
Bug introduced by
It seems like $this->driver->quoteTable($table) targeting Query\Drivers\DriverInterface::quoteTable() can also be of type array; however, Query\QueryBuilder::_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...
1244
1245
		$clauses = [
1246
			'queryMap',
1247
			'groupString',
1248
			'orderString',
1249
			'havingMap',
1250
		];
1251
1252
		// Set each type of subclause
1253
		foreach($clauses as $clause)
1254
		{
1255
			$func = 'get' . ucFirst($clause);
1256
			$param = $this->state->$func();
1257
			if (\is_array($param))
1258
			{
1259
				foreach($param as $q)
1260
				{
1261
					$sql .= $q['conjunction'] . $q['string'];
1262
				}
1263
			}
1264
			else
1265
			{
1266
				$sql .= $param;
1267
			}
1268
		}
1269
1270
		// Set the limit via the class variables
1271
		$limit = $this->state->getLimit();
1272
		if (is_numeric($limit))
1273
		{
1274
			$sql = $this->driver->getSql()->limit($sql, $limit, $this->state->getOffset());
0 ignored issues
show
Bug introduced by
It seems like $this->state->getOffset() targeting Query\State::getOffset() can also be of type string; however, Query\Drivers\SQLInterface::limit() does only seem to accept integer|boolean, 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...
1275
		}
1276
1277
		// See if the query plan, rather than the
1278
		// query data should be returned
1279
		if ($this->explain === TRUE)
1280
		{
1281
			$sql = $this->driver->getSql()->explain($sql);
1282
		}
1283
1284
		return $sql;
1285
	}
1286
}
1287