Completed
Push — develop ( 8401cc...e4ca03 )
by Timothy
01:58
created

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