QueryBuilder::orNotGroupStart()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 6
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 function regexInArray;
18
19
use BadMethodCallException;
20
use PDOStatement;
21
use Query\Drivers\DriverInterface;
22
23
/**
24
 * Convenience class for creating sql queries
25
 */
26
class QueryBuilder implements QueryBuilderInterface {
27
28
	/**
29
	 * Convenience property for connection management
30
	 * @var string
31
	 */
32
	public $connName = '';
33
34
	/**
35
	 * List of queries executed
36
	 * @var array
37
	 */
38
	public $queries = [
39
		'total_time' => 0
40
	];
41
42
	/**
43
	 * Whether to do only an explain on the query
44
	 * @var boolean
45
	 */
46
	protected $explain = FALSE;
47
48
	/**
49
	 * The current database driver
50
	 * @var DriverInterface
51
	 */
52
	public $driver;
53
54
	/**
55
	 * Query parser class instance
56
	 * @var QueryParser
57
	 */
58
	protected $parser;
59
60
	/**
61
	 * Query Builder state
62
	 * @var State
63
	 */
64
	protected $state;
65
66
	// --------------------------------------------------------------------------
67
	// ! Methods
68
	// --------------------------------------------------------------------------
69
70
	/**
71
	 * Constructor
72
	 *
73
	 * @param DriverInterface $driver
74
	 * @param QueryParser $parser
75
	 */
76
	public function __construct(DriverInterface $driver, QueryParser $parser)
77
	{
78
		// Inject driver and parser
79
		$this->driver = $driver;
80
		$this->parser = $parser;
81
82
		// Create new State object
83
		$this->state = new State();
84
	}
85
86
	/**
87
	 * Destructor
88
	 * @codeCoverageIgnore
89
	 */
90
	public function __destruct()
91
	{
92
		$this->driver = NULL;
93
	}
94
95
	/**
96
	 * Calls a function further down the inheritance chain
97
	 *
98
	 * @param string $name
99
	 * @param array $params
100
	 * @return mixed
101
	 * @throws BadMethodCallException
102
	 */
103
	public function __call(string $name, array $params)
104
	{
105
		if (method_exists($this->driver, $name))
106
		{
107
			return \call_user_func_array([$this->driver, $name], $params);
108
		}
109
110
		throw new BadMethodCallException('Method does not exist');
111
	}
112
113
	// --------------------------------------------------------------------------
114
	// ! Select Queries
115
	// --------------------------------------------------------------------------
116
117
	/**
118
	 * Specifies rows to select in a query
119
	 *
120
	 * @param string $fields
121
	 * @return QueryBuilderInterface
122
	 */
123
	public function select(string $fields): QueryBuilderInterface
124
	{
125
		// Split fields by comma
126
		$fieldsArray = explode(',', $fields);
127
		$fieldsArray = array_map('mb_trim', $fieldsArray);
128
129
		// Split on 'As'
130
		foreach ($fieldsArray as $key => $field)
131
		{
132
			if (stripos($field, 'as') !== FALSE)
133
			{
134
				$fieldsArray[$key] = preg_split('` as `i', $field);
135
				$fieldsArray[$key] = array_map('mb_trim', $fieldsArray[$key]);
136
			}
137
		}
138
139
		// Quote the identifiers
140
		$safeArray = $this->driver->quoteIdent($fieldsArray);
141
142
		unset($fieldsArray);
143
144
		// Join the strings back together
145
		for($i = 0, $c = count($safeArray); $i < $c; $i++)
146
		{
147
			if (\is_array($safeArray[$i]))
148
			{
149
				$safeArray[$i] = implode(' AS ', $safeArray[$i]);
150
			}
151
		}
152
153
		$this->state->appendSelectString(implode(', ', $safeArray));
154
155
		return $this;
156
	}
157
158
	/**
159
	 * Selects the maximum value of a field from a query
160
	 *
161
	 * @param string $field
162
	 * @param string|bool $as
163
	 * @return QueryBuilderInterface
164
	 */
165
	public function selectMax(string $field, $as=FALSE): QueryBuilderInterface
166
	{
167
		// Create the select string
168
		$this->state->appendSelectString(' MAX'.$this->_select($field, $as));
169
		return $this;
170
	}
171
172
	/**
173
	 * Selects the minimum value of a field from a query
174
	 *
175
	 * @param string $field
176
	 * @param string|bool $as
177
	 * @return QueryBuilderInterface
178
	 */
179
	public function selectMin(string $field, $as=FALSE): QueryBuilderInterface
180
	{
181
		// Create the select string
182
		$this->state->appendSelectString(' MIN'.$this->_select($field, $as));
183
		return $this;
184
	}
185
186
	/**
187
	 * Selects the average value of a field from a query
188
	 *
189
	 * @param string $field
190
	 * @param string|bool $as
191
	 * @return QueryBuilderInterface
192
	 */
193
	public function selectAvg(string $field, $as=FALSE): QueryBuilderInterface
194
	{
195
		// Create the select string
196
		$this->state->appendSelectString(' AVG'.$this->_select($field, $as));
197
		return $this;
198
	}
199
200
	/**
201
	 * Selects the sum of a field from a query
202
	 *
203
	 * @param string $field
204
	 * @param string|bool $as
205
	 * @return QueryBuilderInterface
206
	 */
207
	public function selectSum(string $field, $as=FALSE): QueryBuilderInterface
208
	{
209
		// Create the select string
210
		$this->state->appendSelectString(' SUM'.$this->_select($field, $as));
211
		return $this;
212
	}
213
214
	/**
215
	 * Adds the 'distinct' keyword to a query
216
	 *
217
	 * @return QueryBuilderInterface
218
	 */
219
	public function distinct(): QueryBuilderInterface
220
	{
221
		// Prepend the keyword to the select string
222
		$this->state->setSelectString(' DISTINCT' . $this->state->getSelectString());
223
		return $this;
224
	}
225
226
	/**
227
	 * Tell the database to give you the query plan instead of result set
228
	 *
229
	 * @return QueryBuilderInterface
230
	 */
231
	public function explain(): QueryBuilderInterface
232
	{
233
		$this->explain = TRUE;
234
		return $this;
235
	}
236
237
	/**
238
	 * Specify the database table to select from
239
	 *
240
	 * @param string $tblname
241
	 * @return QueryBuilderInterface
242
	 */
243
	public function from(string $tblname): QueryBuilderInterface
244
	{
245
		// Split identifiers on spaces
246
		$identArray = explode(' ', \mb_trim($tblname));
247
		$identArray = array_map('\\mb_trim', $identArray);
248
249
		// Quote the identifiers
250
		$identArray[0] = $this->driver->quoteTable($identArray[0]);
251
		$identArray = $this->driver->quoteIdent($identArray);
252
253
		// Paste it back together
254
		$this->state->setFromString(implode(' ', $identArray));
255
256
		return $this;
257
	}
258
259
	// --------------------------------------------------------------------------
260
	// ! 'Like' methods
261
	// --------------------------------------------------------------------------
262
263
	/**
264
	 * Creates a Like clause in the sql statement
265
	 *
266
	 * @param string $field
267
	 * @param mixed $val
268
	 * @param string $pos
269
	 * @return QueryBuilderInterface
270
	 */
271
	public function like(string $field, $val, string $pos='both'): QueryBuilderInterface
272
	{
273
		return $this->_like($field, $val, $pos);
274
	}
275
276
	/**
277
	 * Generates an OR Like clause
278
	 *
279
	 * @param string $field
280
	 * @param mixed $val
281
	 * @param string $pos
282
	 * @return QueryBuilderInterface
283
	 */
284
	public function orLike(string $field, $val, string $pos='both'): QueryBuilderInterface
285
	{
286
		return $this->_like($field, $val, $pos, 'LIKE', 'OR');
287
	}
288
289
	/**
290
	 * Generates a NOT LIKE clause
291
	 *
292
	 * @param string $field
293
	 * @param mixed $val
294
	 * @param string $pos
295
	 * @return QueryBuilderInterface
296
	 */
297
	public function notLike(string $field, $val, string $pos='both'): QueryBuilderInterface
298
	{
299
		return $this->_like($field, $val, $pos, 'NOT LIKE');
300
	}
301
302
	/**
303
	 * Generates a OR NOT LIKE clause
304
	 *
305
	 * @param string $field
306
	 * @param mixed $val
307
	 * @param string $pos
308
	 * @return QueryBuilderInterface
309
	 */
310
	public function orNotLike(string $field, $val, string $pos='both'): QueryBuilderInterface
311
	{
312
		return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR');
313
	}
314
315
	// --------------------------------------------------------------------------
316
	// ! Having methods
317
	// --------------------------------------------------------------------------
318
319
	/**
320
	 * Generates a 'Having' clause
321
	 *
322
	 * @param mixed $key
323
	 * @param mixed $val
324
	 * @return QueryBuilderInterface
325
	 */
326
	public function having($key, $val=[]): QueryBuilderInterface
327
	{
328
		return $this->_having($key, $val);
329
	}
330
331
	/**
332
	 * Generates a 'Having' clause prefixed with 'OR'
333
	 *
334
	 * @param mixed $key
335
	 * @param mixed $val
336
	 * @return QueryBuilderInterface
337
	 */
338
	public function orHaving($key, $val=[]): QueryBuilderInterface
339
	{
340
		return $this->_having($key, $val, 'OR');
341
	}
342
343
	// --------------------------------------------------------------------------
344
	// ! 'Where' methods
345
	// --------------------------------------------------------------------------
346
347
	/**
348
	 * Specify condition(s) in the where clause of a query
349
	 * Note: this function works with key / value, or a
350
	 * passed array with key / value pairs
351
	 *
352
	 * @param mixed $key
353
	 * @param mixed $val
354
	 * @param mixed $escape
355
	 * @return QueryBuilderInterface
356
	 */
357
	public function where($key, $val=[], $escape=NULL): QueryBuilderInterface
358
	{
359
		return $this->_whereString($key, $val);
360
	}
361
362
	/**
363
	 * Where clause prefixed with "OR"
364
	 *
365
	 * @param string $key
366
	 * @param mixed $val
367
	 * @return QueryBuilderInterface
368
	 */
369
	public function orWhere($key, $val=[]): QueryBuilderInterface
370
	{
371
		return $this->_whereString($key, $val, 'OR');
372
	}
373
374
	/**
375
	 * Where clause with 'IN' statement
376
	 *
377
	 * @param mixed $field
378
	 * @param mixed $val
379
	 * @return QueryBuilderInterface
380
	 */
381
	public function whereIn($field, $val=[]): QueryBuilderInterface
382
	{
383
		return $this->_whereIn($field, $val);
384
	}
385
386
	/**
387
	 * Where in statement prefixed with "or"
388
	 *
389
	 * @param string $field
390
	 * @param mixed $val
391
	 * @return QueryBuilderInterface
392
	 */
393
	public function orWhereIn($field, $val=[]): QueryBuilderInterface
394
	{
395
		return $this->_whereIn($field, $val, 'IN', 'OR');
396
	}
397
398
	/**
399
	 * WHERE NOT IN (FOO) clause
400
	 *
401
	 * @param string $field
402
	 * @param mixed $val
403
	 * @return QueryBuilderInterface
404
	 */
405
	public function whereNotIn($field, $val=[]): QueryBuilderInterface
406
	{
407
		return $this->_whereIn($field, $val, 'NOT IN');
408
	}
409
410
	/**
411
	 * OR WHERE NOT IN (FOO) clause
412
	 *
413
	 * @param string $field
414
	 * @param mixed $val
415
	 * @return QueryBuilderInterface
416
	 */
417
	public function orWhereNotIn($field, $val=[]): QueryBuilderInterface
418
	{
419
		return $this->_whereIn($field, $val, 'NOT IN', 'OR');
420
	}
421
422
	// --------------------------------------------------------------------------
423
	// ! Other Query Modifier methods
424
	// --------------------------------------------------------------------------
425
426
	/**
427
	 * Sets values for inserts / updates / deletes
428
	 *
429
	 * @param mixed $key
430
	 * @param mixed $val
431
	 * @return QueryBuilderInterface
432
	 */
433
	public function set($key, $val = NULL): QueryBuilderInterface
434
	{
435
		if (is_scalar($key))
436
		{
437
			$pairs = [$key => $val];
438
		}
439
		else
440
		{
441
			$pairs = $key;
442
		}
443
444
		$keys = array_keys($pairs);
445
		$values = array_values($pairs);
446
447
		$this->state->appendSetArrayKeys($keys);
448
		$this->state->appendValues($values);
449
450
		// Use the keys of the array to make the insert/update string
451
		// Escape the field names
452
		$this->state->setSetArrayKeys(
453
			array_map([$this->driver, '_quote'], $this->state->getSetArrayKeys())
454
		);
455
456
		// Generate the "set" string
457
		$setString = implode('=?,', $this->state->getSetArrayKeys());
458
		$setString .= '=?';
459
460
		$this->state->setSetString($setString);
461
462
		return $this;
463
	}
464
465
	/**
466
	 * Creates a join phrase in a compiled query
467
	 *
468
	 * @param string $table
469
	 * @param string $condition
470
	 * @param string $type
471
	 * @return QueryBuilderInterface
472
	 */
473
	public function join(string $table, string $condition, string $type=''): QueryBuilderInterface
474
	{
475
		// Prefix and quote table name
476
		$table = explode(' ', mb_trim($table));
477
		$table[0] = $this->driver->quoteTable($table[0]);
478
		$table = $this->driver->quoteIdent($table);
479
		$table = implode(' ', $table);
480
481
		// Parse out the join condition
482
		$parsedCondition = $this->parser->compileJoin($condition);
483
		$condition = $table . ' ON ' . $parsedCondition;
484
485
		$this->state->appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, 'join');
486
487
		return $this;
488
	}
489
490
	/**
491
	 * Group the results by the selected field(s)
492
	 *
493
	 * @param mixed $field
494
	 * @return QueryBuilderInterface
495
	 */
496
	public function groupBy($field): QueryBuilderInterface
497
	{
498
		if ( ! is_scalar($field))
499
		{
500
			$newGroupArray = array_map([$this->driver, 'quoteIdent'], $field);
501
			$this->state->setGroupArray(
502
				array_merge($this->state->getGroupArray(), $newGroupArray)
503
			);
504
		}
505
		else
506
		{
507
			$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...
508
		}
509
510
		$this->state->setGroupString(' GROUP BY ' . implode(',', $this->state->getGroupArray()));
511
512
		return $this;
513
	}
514
515
	/**
516
	 * Order the results by the selected field(s)
517
	 *
518
	 * @param string $field
519
	 * @param string $type
520
	 * @return QueryBuilderInterface
521
	 */
522
	public function orderBy(string $field, string $type=''): QueryBuilderInterface
523
	{
524
		// When ordering by random, do an ascending order if the driver
525
		// doesn't support random ordering
526
		if (stripos($type, 'rand') !== FALSE)
527
		{
528
			$rand = $this->driver->getSql()->random();
529
			$type = $rand ?? 'ASC';
530
		}
531
532
		// Set fields for later manipulation
533
		$field = $this->driver->quoteIdent($field);
534
		$this->state->setOrderArray($field, $type);
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->driver->quoteIdent($field) on line 533 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...
535
536
		$orderClauses = [];
537
538
		// Flatten key/val pairs into an array of space-separated pairs
539
		foreach($this->state->getOrderArray() as $k => $v)
540
		{
541
			$orderClauses[] = $k . ' ' . strtoupper($v);
542
		}
543
544
		// Set the final string
545
		$orderString =  ! isset($rand)
546
			? "\nORDER BY ".implode(', ', $orderClauses)
547
			: "\nORDER BY".$rand;
548
549
		$this->state->setOrderString($orderString);
550
551
		return $this;
552
	}
553
554
	/**
555
	 * Set a limit on the current sql statement
556
	 *
557
	 * @param int $limit
558
	 * @param int|bool $offset
559
	 * @return QueryBuilderInterface
560
	 */
561
	public function limit(int $limit, $offset=FALSE): QueryBuilderInterface
562
	{
563
		$this->state->setLimit($limit);
564
		$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...
565
566
		return $this;
567
	}
568
569
	// --------------------------------------------------------------------------
570
	// ! Query Grouping Methods
571
	// --------------------------------------------------------------------------
572
573
	/**
574
	 * Adds a paren to the current query for query grouping
575
	 *
576
	 * @return QueryBuilderInterface
577
	 */
578
	public function groupStart(): QueryBuilderInterface
579
	{
580
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' ';
581
582
		$this->state->appendMap($conj, '(', 'group_start');
583
584
		return $this;
585
	}
586
587
	/**
588
	 * Adds a paren to the current query for query grouping,
589
	 * prefixed with 'NOT'
590
	 *
591
	 * @return QueryBuilderInterface
592
	 */
593
	public function notGroupStart(): QueryBuilderInterface
594
	{
595
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' AND ';
596
597
		$this->state->appendMap($conj, ' NOT (', 'group_start');
598
599
		return $this;
600
	}
601
602
	/**
603
	 * Adds a paren to the current query for query grouping,
604
	 * prefixed with 'OR'
605
	 *
606
	 * @return QueryBuilderInterface
607
	 */
608
	public function orGroupStart(): QueryBuilderInterface
609
	{
610
		$this->state->appendMap('', ' OR (', 'group_start');
611
612
		return $this;
613
	}
614
615
	/**
616
	 * Adds a paren to the current query for query grouping,
617
	 * prefixed with 'OR NOT'
618
	 *
619
	 * @return QueryBuilderInterface
620
	 */
621
	public function orNotGroupStart(): QueryBuilderInterface
622
	{
623
		$this->state->appendMap('', ' OR NOT (', 'group_start');
624
625
		return $this;
626
	}
627
628
	/**
629
	 * Ends a query group
630
	 *
631
	 * @return QueryBuilderInterface
632
	 */
633
	public function groupEnd(): QueryBuilderInterface
634
	{
635
		$this->state->appendMap('', ')', 'group_end');
636
637
		return $this;
638
	}
639
640
	// --------------------------------------------------------------------------
641
	// ! Query execution methods
642
	// --------------------------------------------------------------------------
643
644
	/**
645
	 * Select and retrieve all records from the current table, and/or
646
	 * execute current compiled query
647
	 *
648
	 * @param string $table
649
	 * @param int|bool $limit
650
	 * @param int|bool $offset
651
	 * @return PDOStatement
652
	 */
653
	public function get(string $table='', $limit=FALSE, $offset=FALSE): PDOStatement
654
	{
655
		// Set the table
656
		if ( ! empty($table))
657
		{
658
			$this->from($table);
659
		}
660
661
		// Set the limit, if it exists
662
		if (\is_int($limit))
663
		{
664
			$this->limit($limit, $offset);
665
		}
666
667
		return $this->_run('get', $table);
668
	}
669
670
	/**
671
	 * Convenience method for get() with a where clause
672
	 *
673
	 * @param string $table
674
	 * @param mixed $where
675
	 * @param int|bool $limit
676
	 * @param int|bool $offset
677
	 * @return PDOStatement
678
	 */
679
	public function getWhere(string $table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement
680
	{
681
		// Create the where clause
682
		$this->where($where);
683
684
		// Return the result
685
		return $this->get($table, $limit, $offset);
686
	}
687
688
	/**
689
	 * Retrieve the number of rows in the selected table
690
	 *
691
	 * @param string $table
692
	 * @return int
693
	 */
694
	public function countAll(string $table): int
695
	{
696
		$sql = 'SELECT * FROM '.$this->driver->quoteTable($table);
697
		$res = $this->driver->query($sql);
698
		return (int) count($res->fetchAll());
699
	}
700
701
	/**
702
	 * Retrieve the number of results for the generated query - used
703
	 * in place of the get() method
704
	 *
705
	 * @param string $table
706
	 * @param boolean $reset
707
	 * @return int
708
	 */
709
	public function countAllResults(string $table='', bool $reset = TRUE): int
710
	{
711
		// Set the table
712
		if ( ! empty($table))
713
		{
714
			$this->from($table);
715
		}
716
717
		$result = $this->_run('get', $table, NULL, NULL, $reset);
718
		$rows = $result->fetchAll();
719
720
		return (int) count($rows);
721
	}
722
723
	/**
724
	 * Creates an insert clause, and executes it
725
	 *
726
	 * @param string $table
727
	 * @param mixed $data
728
	 * @return PDOStatement
729
	 */
730
	public function insert(string $table, $data=[]): PDOStatement
731
	{
732
		if ( ! empty($data))
733
		{
734
			$this->set($data);
735
		}
736
737
		return $this->_run('insert', $table);
738
	}
739
740
	/**
741
	 * Creates and executes a batch insertion query
742
	 *
743
	 * @param string $table
744
	 * @param array $data
745
	 * @return PDOStatement
746
	 */
747
	public function insertBatch(string $table, $data=[]): PDOStatement
748
	{
749
		// Get the generated values and sql string
750
		[$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...
751
752
		return $sql !== NULL
753
			? $this->_run('', $table, $sql, $data)
754
			: NULL;
755
	}
756
757
	/**
758
	 * Creates an update clause, and executes it
759
	 *
760
	 * @param string $table
761
	 * @param mixed $data
762
	 * @return PDOStatement
763
	 */
764
	public function update(string $table, $data=[]): PDOStatement
765
	{
766
		if ( ! empty($data))
767
		{
768
			$this->set($data);
769
		}
770
771
		return $this->_run('update', $table);
772
	}
773
774
	/**
775
	 * Creates a batch update, and executes it.
776
	 * Returns the number of affected rows
777
	 *
778
	 * @param string $table
779
	 * @param array $data
780
	 * @param string $where
781
	 * @return int|null
782
	 */
783
	public function updateBatch(string $table, array $data, string $where): ?int
784
	{
785
		if (empty($table) || empty($data) || empty($where))
786
		{
787
			return NULL;
788
		}
789
790
		// Get the generated values and sql string
791
		[$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...
792
793
		$this->_run('', $table, $sql, $data);
794
		return $affectedRows;
795
	}
796
797
	/**
798
	 * Deletes data from a table
799
	 *
800
	 * @param string $table
801
	 * @param mixed $where
802
	 * @return PDOStatement
803
	 */
804
	public function delete(string $table, $where=''): PDOStatement
805
	{
806
		// Set the where clause
807
		if ( ! empty($where))
808
		{
809
			$this->where($where);
810
		}
811
812
		return $this->_run('delete', $table);
813
	}
814
815
	// --------------------------------------------------------------------------
816
	// ! SQL Returning Methods
817
	// --------------------------------------------------------------------------
818
819
	/**
820
	 * Returns the generated 'select' sql query
821
	 *
822
	 * @param string $table
823
	 * @param bool $reset
824
	 * @return string
825
	 */
826
	public function getCompiledSelect(string $table='', bool $reset=TRUE): string
827
	{
828
		// Set the table
829
		if ( ! empty($table))
830
		{
831
			$this->from($table);
832
		}
833
834
		return $this->_getCompile('select', $table, $reset);
835
	}
836
837
	/**
838
	 * Returns the generated 'insert' sql query
839
	 *
840
	 * @param string $table
841
	 * @param bool $reset
842
	 * @return string
843
	 */
844
	public function getCompiledInsert(string $table, bool $reset=TRUE): string
845
	{
846
		return $this->_getCompile('insert', $table, $reset);
847
	}
848
849
	/**
850
	 * Returns the generated 'update' sql query
851
	 *
852
	 * @param string $table
853
	 * @param bool $reset
854
	 * @return string
855
	 */
856
	public function getCompiledUpdate(string $table='', bool $reset=TRUE): string
857
	{
858
		return $this->_getCompile('update', $table, $reset);
859
	}
860
861
	/**
862
	 * Returns the generated 'delete' sql query
863
	 *
864
	 * @param string $table
865
	 * @param bool $reset
866
	 * @return string
867
	 */
868
	public function getCompiledDelete(string $table='', bool $reset=TRUE): string
869
	{
870
		return $this->_getCompile('delete', $table, $reset);
871
	}
872
873
	// --------------------------------------------------------------------------
874
	// ! Miscellaneous Methods
875
	// --------------------------------------------------------------------------
876
877
	/**
878
	 * Clear out the class variables, so the next query can be run
879
	 *
880
	 * @return void
881
	 */
882
	public function resetQuery(): void
883
	{
884
		$this->state = new State();
885
		$this->explain = FALSE;
886
	}
887
888
	/**
889
	 * Method to simplify select_ methods
890
	 *
891
	 * @param string $field
892
	 * @param string|bool $as
893
	 * @return string
894
	 */
895
	protected function _select(string $field, $as = FALSE): string
896
	{
897
		// Escape the identifiers
898
		$field = $this->driver->quoteIdent($field);
899
900
		if ( ! \is_string($as))
901
		{
902
			// @codeCoverageIgnoreStart
903
			return $field;
904
			// @codeCoverageIgnoreEnd
905
		}
906
907
		$as = $this->driver->quoteIdent($as);
908
		return "({$field}) AS {$as} ";
909
	}
910
911
	/**
912
	 * Helper function for returning sql strings
913
	 *
914
	 * @param string $type
915
	 * @param string $table
916
	 * @param bool $reset
917
	 * @return string
918
	 */
919
	protected function _getCompile(string $type, string $table, bool $reset): string
920
	{
921
		$sql = $this->_compile($type, $table);
922
923
		// Reset the query builder for the next query
924
		if ($reset)
925
		{
926
			$this->resetQuery();
927
		}
928
929
		return $sql;
930
	}
931
932
	/**
933
	 * Simplify 'like' methods
934
	 *
935
	 * @param string $field
936
	 * @param mixed $val
937
	 * @param string $pos
938
	 * @param string $like
939
	 * @param string $conj
940
	 * @return self
941
	 */
942
	protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): self
943
	{
944
		$field = $this->driver->quoteIdent($field);
945
946
		// Add the like string into the order map
947
		$like = $field. " {$like} ?";
948
949
		if ($pos === 'before')
950
		{
951
			$val = "%{$val}";
952
		}
953
		elseif ($pos === 'after')
954
		{
955
			$val = "{$val}%";
956
		}
957
		else
958
		{
959
			$val = "%{$val}%";
960
		}
961
962
		$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
963
		$this->state->appendMap($conj, $like, 'like');
964
965
		// Add to the values array
966
		$this->state->appendWhereValues($val);
967
968
		return $this;
969
	}
970
971
	/**
972
	 * Simplify building having clauses
973
	 *
974
	 * @param mixed $key
975
	 * @param mixed $values
976
	 * @param string $conj
977
	 * @return self
978
	 */
979
	protected function _having($key, $values=[], string $conj='AND'): self
980
	{
981
		$where = $this->_where($key, $values);
982
983
		// Create key/value placeholders
984
		foreach($where as $f => $val)
985
		{
986
			// Split each key by spaces, in case there
987
			// is an operator such as >, <, !=, etc.
988
			$fArray = explode(' ', trim($f));
989
990
			$item = $this->driver->quoteIdent($fArray[0]);
991
992
			// Simple key value, or an operator
993
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
994
995
			// Put in the having map
996
			$this->state->appendHavingMap([
997
				'conjunction' => empty($this->state->getHavingMap())
998
					? ' HAVING '
999
					: " {$conj} ",
1000
				'string' => $item
1001
			]);
1002
		}
1003
1004
		return $this;
1005
	}
1006
1007
	/**
1008
	 * Do all the redundant stuff for where/having type methods
1009
	 *
1010
	 * @param mixed $key
1011
	 * @param mixed $val
1012
	 * @return array
1013
	 */
1014
	protected function _where($key, $val=[]): array
1015
	{
1016
		$where = [];
1017
		$pairs = [];
1018
1019
		if (is_scalar($key))
1020
		{
1021
			$pairs[$key] = $val;
1022
		}
1023
		else
1024
		{
1025
			$pairs = $key;
1026
		}
1027
1028
		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...
1029
		{
1030
			$where[$k] = $v;
1031
			$this->state->appendWhereValues($v);
1032
		}
1033
1034
		return $where;
1035
	}
1036
1037
	/**
1038
	 * Simplify generating where string
1039
	 *
1040
	 * @param mixed $key
1041
	 * @param mixed $values
1042
	 * @param string $defaultConj
1043
	 * @return self
1044
	 */
1045
	protected function _whereString($key, $values=[], string $defaultConj='AND'): self
1046
	{
1047
		// Create key/value placeholders
1048
		foreach($this->_where($key, $values) as $f => $val)
1049
		{
1050
			$queryMap = $this->state->getQueryMap();
1051
1052
			// Split each key by spaces, in case there
1053
			// is an operator such as >, <, !=, etc.
1054
			$fArray = explode(' ', trim($f));
1055
1056
			$item = $this->driver->quoteIdent($fArray[0]);
1057
1058
			// Simple key value, or an operator
1059
			$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
1060
			$lastItem = end($queryMap);
1061
1062
			// Determine the correct conjunction
1063
			$conjunctionList = array_column($queryMap, 'conjunction');
1064
			if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i")))
1065
			{
1066
				$conj = "\nWHERE ";
1067
			}
1068
			elseif ($lastItem['type'] === 'group_start')
1069
			{
1070
				$conj = '';
1071
			}
1072
			else
1073
			{
1074
				$conj = " {$defaultConj} ";
1075
			}
1076
1077
			$this->state->appendMap($conj, $item, 'where');
1078
		}
1079
1080
		return $this;
1081
	}
1082
1083
	/**
1084
	 * Simplify where_in methods
1085
	 *
1086
	 * @param mixed $key
1087
	 * @param mixed $val
1088
	 * @param string $in - The (not) in fragment
1089
	 * @param string $conj - The where in conjunction
1090
	 * @return self
1091
	 */
1092
	protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): self
1093
	{
1094
		$key = $this->driver->quoteIdent($key);
1095
		$params = array_fill(0, count($val), '?');
1096
		$this->state->appendWhereValues($val);
1097
1098
		$conjunction =  empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
1099
		$str = $key . " {$in} (".implode(',', $params).') ';
1100
1101
		$this->state->appendMap($conjunction, $str, 'where_in');
1102
1103
		return $this;
1104
	}
1105
1106
	/**
1107
	 * Executes the compiled query
1108
	 *
1109
	 * @param string $type
1110
	 * @param string $table
1111
	 * @param string $sql
1112
	 * @param array|null $vals
1113
	 * @param boolean $reset
1114
	 * @return PDOStatement
1115
	 */
1116
	protected function _run(string $type, string $table, string $sql=NULL, array $vals=NULL, bool $reset=TRUE): PDOStatement
1117
	{
1118
		if ($sql === NULL)
1119
		{
1120
			$sql = $this->_compile($type, $table);
1121
		}
1122
1123
		if ($vals === NULL)
1124
		{
1125
			$vals = array_merge($this->state->getValues(), $this->state->getWhereValues());
1126
		}
1127
1128
		$startTime = microtime(TRUE);
1129
1130
		$res = empty($vals)
1131
			? $this->driver->query($sql)
1132
			: $this->driver->prepareExecute($sql, $vals);
1133
1134
		$endTime = microtime(TRUE);
1135
		$totalTime = number_format($endTime - $startTime, 5);
1136
1137
		// Add this query to the list of executed queries
1138
		$this->_appendQuery($vals, $sql, (int) $totalTime);
1139
1140
		// Reset class state for next query
1141
		if ($reset)
1142
		{
1143
			$this->resetQuery();
1144
		}
1145
1146
		return $res;
1147
	}
1148
1149
	/**
1150
	 * Convert the prepared statement into readable sql
1151
	 *
1152
	 * @param array $values
1153
	 * @param string $sql
1154
	 * @param int $totalTime
1155
	 * @return void
1156
	 */
1157
	protected function _appendQuery(array $values, string $sql, int $totalTime): void
1158
	{
1159
		$evals = \is_array($values) ? $values : [];
1160
		$esql = str_replace('?', '%s', $sql);
1161
1162
		// Quote string values
1163
		foreach($evals as &$v)
1164
		{
1165
			$v = ( ! is_numeric($v))
1166
				? htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8')
1167
				: $v;
1168
		}
1169
1170
		// Add the query onto the array of values to pass
1171
		// as arguments to sprintf
1172
		array_unshift($evals, $esql);
1173
1174
		// Add the interpreted query to the list of executed queries
1175
		$this->queries[] = [
1176
			'time' => $totalTime,
1177
			'sql' => sprintf(...$evals)
1178
		];
1179
1180
		$this->queries['total_time'] += $totalTime;
1181
1182
		// Set the last query to get rowcounts properly
1183
		$this->driver->setLastQuery($sql);
1184
	}
1185
1186
	/**
1187
	 * Sub-method for generating sql strings
1188
	 *
1189
	 * @codeCoverageIgnore
1190
	 * @param string $type
1191
	 * @param string $table
1192
	 * @return string
1193
	 */
1194
	protected function _compileType(string $type='', string $table=''): string
1195
	{
1196
		$setArrayKeys = $this->state->getSetArrayKeys();
1197
		switch($type)
1198
		{
1199
			case 'insert':
1200
				$paramCount = count($setArrayKeys);
1201
				$params = array_fill(0, $paramCount, '?');
1202
				$sql = "INSERT INTO {$table} ("
1203
					. implode(',', $setArrayKeys)
1204
					. ")\nVALUES (".implode(',', $params).')';
1205
				break;
1206
1207
			case 'update':
1208
				$setString = $this->state->getSetString();
1209
				$sql = "UPDATE {$table}\nSET {$setString}";
1210
				break;
1211
1212
			case 'delete':
1213
				$sql = "DELETE FROM {$table}";
1214
				break;
1215
1216
			// Get queries
1217
			default:
1218
				$fromString = $this->state->getFromString();
1219
				$selectString = $this->state->getSelectString();
1220
1221
				$sql = "SELECT * \nFROM {$fromString}";
1222
1223
				// Set the select string
1224
				if ( ! empty($selectString))
1225
				{
1226
					// Replace the star with the selected fields
1227
					$sql = str_replace('*', $selectString, $sql);
1228
				}
1229
				break;
1230
		}
1231
1232
		return $sql;
1233
	}
1234
1235
	/**
1236
	 * String together the sql statements for sending to the db
1237
	 *
1238
	 * @param string $type
1239
	 * @param string $table
1240
	 * @return string
1241
	 */
1242
	protected function _compile(string $type='', string $table=''): string
1243
	{
1244
		// Get the base clause for the query
1245
		$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...
1246
1247
		$clauses = [
1248
			'queryMap',
1249
			'groupString',
1250
			'orderString',
1251
			'havingMap',
1252
		];
1253
1254
		// Set each type of subclause
1255
		foreach($clauses as $clause)
1256
		{
1257
			$func = 'get' . ucFirst($clause);
1258
			$param = $this->state->$func();
1259
			if (\is_array($param))
1260
			{
1261
				foreach($param as $q)
1262
				{
1263
					$sql .= $q['conjunction'] . $q['string'];
1264
				}
1265
			}
1266
			else
1267
			{
1268
				$sql .= $param;
1269
			}
1270
		}
1271
1272
		// Set the limit via the class variables
1273
		$limit = $this->state->getLimit();
1274
		if (is_numeric($limit))
1275
		{
1276
			$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...
1277
		}
1278
1279
		// See if the query plan, rather than the
1280
		// query data should be returned
1281
		if ($this->explain === TRUE)
1282
		{
1283
			$sql = $this->driver->getSql()->explain($sql);
1284
		}
1285
1286
		return $sql;
1287
	}
1288
}
1289