AbstractDriver::affectedRows()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
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\Drivers;
16
17
use function dbFilter;
18
19
use InvalidArgumentException;
20
use PDO;
21
use PDOStatement;
22
23
/**
24
 * Base Database class
25
 *
26
 * Extends PDO to simplify cross-database issues
27
 */
28
abstract class AbstractDriver
29
	extends PDO
30
	implements DriverInterface {
31
32
	/**
33
	 * Reference to the last executed query
34
	 * @var PDOStatement
35
	 */
36
	protected $statement;
37
38
	/**
39
	 * Start character to escape identifiers
40
	 * @var string
41
	 */
42
	protected $escapeCharOpen = '"';
43
44
	/**
45
	 * End character to escape identifiers
46
	 * @var string
47
	 */
48
	protected $escapeCharClose = '"';
49
50
	/**
51
	 * Reference to sql class
52
	 * @var SQLInterface
53
	 */
54
	protected $sql;
55
56
	/**
57
	 * Reference to util class
58
	 * @var AbstractUtil
59
	 */
60
	protected $util;
61
62
	/**
63
	 * Last query executed
64
	 * @var string
65
	 */
66
	protected $lastQuery = '';
67
68
	/**
69
	 * Prefix to apply to table names
70
	 * @var string
71
	 */
72
	protected $tablePrefix = '';
73
74
	/**
75
	 * Whether the driver supports 'TRUNCATE'
76
	 * @var boolean
77
	 */
78
	protected $hasTruncate = TRUE;
79
80
	/**
81
	 * PDO constructor wrapper
82
	 *
83
	 * @param string $dsn
84
	 * @param string $username
85
	 * @param string $password
86
	 * @param array $driverOptions
87
	 */
88
	public function __construct(string $dsn, string $username=NULL, string $password=NULL, array $driverOptions=[])
89
	{
90
		// Set PDO to display errors as exceptions, and apply driver options
91
		$driverOptions[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
92
		parent::__construct($dsn, $username, $password, $driverOptions);
93
94
		$this->_loadSubClasses();
95
	}
96
97
	/**
98
	 * Loads the subclasses for the driver
99
	 *
100
	 * @return void
101
	 */
102
	protected function _loadSubClasses(): void
103
	{
104
		// Load the sql and util class for the driver
105
		$thisClass = \get_class($this);
106
		$nsArray = explode("\\", $thisClass);
107
		array_pop($nsArray);
108
		$driver = array_pop($nsArray);
109
		$sqlClass = __NAMESPACE__ . "\\{$driver}\\SQL";
110
		$utilClass = __NAMESPACE__ . "\\{$driver}\\Util";
111
112
		$this->sql = new $sqlClass();
113
		$this->util = new $utilClass($this);
114
	}
115
116
	/**
117
	 * Allow invoke to work on table object
118
	 *
119
	 * @codeCoverageIgnore
120
	 * @param string $name
121
	 * @param array $args
122
	 * @return mixed
123
	 */
124
	public function __call(string $name, array $args = [])
125
	{
126
		if (
127
			isset($this->$name)
128
			&& \is_object($this->$name)
129
			&& method_exists($this->$name, '__invoke')
130
		)
131
		{
132
			return \call_user_func_array([$this->$name, '__invoke'], $args);
133
		}
134
	}
135
136
	// --------------------------------------------------------------------------
137
	// ! Accessors / Mutators
138
	// --------------------------------------------------------------------------
139
140
	/**
141
	 * Get the last sql query executed
142
	 *
143
	 * @return string
144
	 */
145
	public function getLastQuery(): string
146
	{
147
		return $this->lastQuery;
148
	}
149
150
	/**
151
	 * Set the last query sql
152
	 *
153
	 * @param string $queryString
154
	 * @return void
155
	 */
156
	public function setLastQuery(string $queryString): void
157
	{
158
		$this->lastQuery = $queryString;
159
	}
160
161
	/**
162
	 * Get the SQL class for the current driver
163
	 *
164
	 * @return SQLInterface
165
	 */
166
	public function getSql(): SQLInterface
167
	{
168
		return $this->sql;
169
	}
170
171
	/**
172
	 * Get the Util class for the current driver
173
	 *
174
	 * @return AbstractUtil
175
	 */
176
	public function getUtil(): AbstractUtil
177
	{
178
		return $this->util;
179
	}
180
181
	/**
182
	 * Set the common table name prefix
183
	 *
184
	 * @param string $prefix
185
	 * @return void
186
	 */
187
	public function setTablePrefix(string $prefix): void
188
	{
189
		$this->tablePrefix = $prefix;
190
	}
191
192
	// --------------------------------------------------------------------------
193
	// ! Concrete functions that can be overridden in child classes
194
	// --------------------------------------------------------------------------
195
196
	/**
197
	 * Simplifies prepared statements for database queries
198
	 *
199
	 * @param string $sql
200
	 * @param array $data
201
	 * @return PDOStatement | FALSE
202
	 * @throws InvalidArgumentException
203
	 */
204
	public function prepareQuery(string $sql, array $data): PDOStatement
205
	{
206
		// Prepare the sql, save the statement for easy access later
207
		$this->statement = $this->prepare($sql);
208
209
		// Bind the parameters
210
		foreach($data as $k => $value)
211
		{
212
			// Parameters are 1-based, the data is 0-based
213
			// So, if the key is numeric, add 1
214
			if(is_numeric($k))
215
			{
216
				$k++;
217
			}
218
			$this->statement->bindValue($k, $value);
219
		}
220
221
		return $this->statement;
222
	}
223
224
	/**
225
	 * Create and execute a prepared statement with the provided parameters
226
	 *
227
	 * @param string $sql
228
	 * @param array $params
229
	 * @throws InvalidArgumentException
230
	 * @return PDOStatement
231
	 */
232
	public function prepareExecute(string $sql, array $params): PDOStatement
233
	{
234
		$this->statement = $this->prepareQuery($sql, $params);
235
		$this->statement->execute();
236
237
		return $this->statement;
238
	}
239
240
	/**
241
	 * Returns number of rows affected by an INSERT, UPDATE, DELETE type query
242
	 *
243
	 * @return int
244
	 */
245
	public function affectedRows(): int
246
	{
247
		// Return number of rows affected
248
		return $this->statement->rowCount();
249
	}
250
251
	/**
252
	 * Prefixes a table if it is not already prefixed
253
	 * @param string $table
254
	 * @return string
255
	 */
256
	public function prefixTable(string $table): string
257
	{
258
		// Add the prefix to the table name
259
		// before quoting it
260
		if ( ! empty($this->tablePrefix))
261
		{
262
			// Split identifier by period, will split into:
263
			// database.schema.table OR
264
			// schema.table OR
265
			// database.table OR
266
			// table
267
			$identifiers = explode('.', $table);
268
			$segments = count($identifiers);
269
270
			// Quote the last item, and add the database prefix
271
			$identifiers[$segments - 1] = $this->_prefix(end($identifiers));
272
273
			// Rejoin
274
			$table = implode('.', $identifiers);
275
		}
276
277
		return $table;
278
	}
279
280
	/**
281
	 * Quote database table name, and set prefix
282
	 *
283
	 * @param string $table
284
	 * @return string
285
	 */
286
	public function quoteTable($table): string
287
	{
288
		$table = $this->prefixTable($table);
289
290
		// Finally, quote the table
291
		return $this->quoteIdent($table);
292
	}
293
294
	/**
295
	 * Surrounds the string with the databases identifier escape characters
296
	 *
297
	 * @param mixed $identifier
298
	 * @return string|array
299
	 */
300
	public function quoteIdent($identifier)
301
	{
302
		if (is_array($identifier))
303
		{
304
			return array_map([$this, __METHOD__], $identifier);
305
		}
306
307
		// Handle comma-separated identifiers
308
		if (strpos($identifier, ',') !== FALSE)
309
		{
310
			$parts = array_map('mb_trim', explode(',', $identifier));
311
			$parts = array_map([$this, __METHOD__], $parts);
312
			$identifier = implode(',', $parts);
313
		}
314
315
		// Split each identifier by the period
316
		$hiers = explode('.', $identifier);
317
		$hiers = array_map('mb_trim', $hiers);
318
319
		// Re-compile the string
320
		$raw = implode('.', array_map([$this, '_quote'], $hiers));
321
322
		// Fix functions
323
		$funcs = [];
324
		preg_match_all("#{$this->escapeCharOpen}([a-zA-Z0-9_]+(\((.*?)\))){$this->escapeCharClose}#iu", $raw, $funcs, PREG_SET_ORDER);
325
		foreach($funcs as $f)
0 ignored issues
show
Bug introduced by
The expression $funcs of type null|array<integer,array<integer,string>> 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...
326
		{
327
			// Unquote the function
328
			$raw = str_replace($f[0], $f[1], $raw);
329
330
			// Quote the inside identifiers
331
			$raw = str_replace($f[3], $this->quoteIdent($f[3]), $raw);
332
		}
333
334
		return $raw;
335
	}
336
337
	/**
338
	 * Return schemas for databases that list them
339
	 *
340
	 * @return array
341
	 */
342
	public function getSchemas(): ?array
343
	{
344
		return NULL;
345
	}
346
347
	/**
348
	 * Return list of tables for the current database
349
	 *
350
	 * @return array
351
	 */
352
	public function getTables(): ?array
353
	{
354
		$tables = $this->driverQuery('tableList');
355
		natsort($tables);
356
		return $tables;
357
	}
358
359
	/**
360
	 * Return list of dbs for the current connection, if possible
361
	 *
362
	 * @return array
363
	 */
364
	public function getDbs(): array
365
	{
366
		return $this->driverQuery('dbList');
367
	}
368
369
	/**
370
	 * Return list of views for the current database
371
	 *
372
	 * @return array
373
	 */
374
	public function getViews(): ?array
375
	{
376
		$views = $this->driverQuery('viewList');
377
		sort($views);
378
		return $views;
379
	}
380
381
	/**
382
	 * Return list of sequences for the current database, if they exist
383
	 *
384
	 * @return array
385
	 */
386
	public function getSequences(): ?array
387
	{
388
		return $this->driverQuery('sequenceList');
389
	}
390
391
	/**
392
	 * Return list of functions for the current database
393
	 *
394
	 * @return array
395
	 */
396
	public function getFunctions(): ?array
397
	{
398
		return $this->driverQuery('functionList', FALSE);
399
	}
400
401
	/**
402
	 * Return list of stored procedures for the current database
403
	 *
404
	 * @return array
405
	 */
406
	public function getProcedures(): ?array
407
	{
408
		return $this->driverQuery('procedureList', FALSE);
409
	}
410
411
	/**
412
	 * Return list of triggers for the current database
413
	 *
414
	 * @return array
415
	 */
416
	public function getTriggers(): ?array
417
	{
418
		return $this->driverQuery('triggerList', FALSE);
419
	}
420
421
	/**
422
	 * Retrieves an array of non-user-created tables for
423
	 * the connection/database
424
	 *
425
	 * @return array
426
	 */
427
	public function getSystemTables(): ?array
428
	{
429
		return $this->driverQuery('systemTableList');
430
	}
431
432
	/**
433
	 * Retrieve column information for the current database table
434
	 *
435
	 * @param string $table
436
	 * @return array
437
	 */
438
	public function getColumns($table): ?array
439
	{
440
		return $this->driverQuery($this->getSql()->columnList($this->prefixTable($table)), FALSE);
441
	}
442
443
	/**
444
	 * Retrieve foreign keys for the table
445
	 *
446
	 * @param string $table
447
	 * @return array
448
	 */
449
	public function getFks($table): ?array
450
	{
451
		return $this->driverQuery($this->getSql()->fkList($table), FALSE);
452
	}
453
454
	/**
455
	 * Retrieve indexes for the table
456
	 *
457
	 * @param string $table
458
	 * @return array
459
	 */
460
	public function getIndexes($table): ?array
461
	{
462
		return $this->driverQuery($this->getSql()->indexList($this->prefixTable($table)), FALSE);
463
	}
464
465
	/**
466
	 * Retrieve list of data types for the database
467
	 *
468
	 * @return array
469
	 */
470
	public function getTypes(): ?array
471
	{
472
		return $this->driverQuery('typeList', FALSE);
473
	}
474
475
	/**
476
	 * Method to simplify retrieving db results for meta-data queries
477
	 *
478
	 * @param string|array|null $query
479
	 * @param bool $filteredIndex
480
	 * @return array|null
481
	 */
482
	public function driverQuery($query, $filteredIndex=TRUE): ?array
483
	{
484
		// Call the appropriate method, if it exists
485
		if (is_string($query) && method_exists($this->sql, $query))
486
		{
487
			$query = $this->getSql()->$query();
488
		}
489
490
		// Return if the values are returned instead of a query,
491
		// or if the query doesn't apply to the driver
492
		if ( ! is_string($query))
493
		{
494
			return $query;
495
		}
496
497
		// Run the query!
498
		$res = $this->query($query);
499
500
		$flag = $filteredIndex ? PDO::FETCH_NUM : PDO::FETCH_ASSOC;
501
		$all = $res->fetchAll($flag);
502
503
		return $filteredIndex ? dbFilter($all, 0) : $all;
504
	}
505
506
	/**
507
	 * Return the number of rows returned for a SELECT query
508
	 *
509
	 * @see http://us3.php.net/manual/en/pdostatement.rowcount.php#87110
510
	 * @return int|null
511
	 */
512
	public function numRows(): ?int
513
	{
514
		$regex = '/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/i';
515
		$output = [];
516
517
		if (preg_match($regex, $this->lastQuery, $output) > 0)
518
		{
519
			$stmt = $this->query("SELECT COUNT(*) FROM {$output[1]}");
520
			return (int) $stmt->fetchColumn();
521
		}
522
523
		return NULL;
524
	}
525
526
	/**
527
	 * Create sql for batch insert
528
	 *
529
	 * @param string $table
530
	 * @param mixed $data
531
	 * @return array<string|array|null>
532
	 */
533
	public function insertBatch(string $table, array $data=[]): array
534
	{
535
		$data = (array) $data;
536
		$firstRow = (array) current($data);
537
538
		// Values for insertion
539
		$vals = [];
540
		foreach($data as $group)
541
		{
542
			$vals = array_merge($vals, array_values($group));
543
		}
544
545
		$table = $this->quoteTable($table);
546
		$fields = array_keys($firstRow);
547
548
		$sql = "INSERT INTO {$table} ("
549
			. implode(',', $this->quoteIdent($fields))
550
			. ') VALUES ';
551
552
		// Create the placeholder groups
553
		$params = array_fill(0, count($fields), '?');
554
		$paramString = '(' . implode(',', $params) . ')';
555
		$paramList = array_fill(0, count($data), $paramString);
556
557
		// Append the placeholder groups to the query
558
		$sql .= implode(',', $paramList);
559
560
		return [$sql, $vals];
561
	}
562
563
	/**
564
	 * Creates a batch update, and executes it.
565
	 * Returns the number of affected rows
566
	 *
567
	 * @param string $table The table to update
568
	 * @param array $data an array of update values
569
	 * @param string $where The where key
570
	 * @return array<string,array,int>
0 ignored issues
show
Documentation introduced by
The doc-type array<string,array,int> could not be parsed: Expected ">" at position 5, but found ",". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
571
	 */
572
	public function updateBatch(string $table, array $data, string $where): array
573
	{
574
		$affectedRows = 0;
575
		$insertData = [];
576
		$fieldLines = [];
577
578
		$sql = 'UPDATE ' . $this->quoteTable($table) . ' SET ';
579
580
		// Get the keys of the current set of data, except the one used to
581
		// set the update condition
582
		$fields = array_unique(
583
			array_reduce($data, static function ($previous, $current) use (&$affectedRows, $where) {
584
				$affectedRows++;
585
				$keys = array_diff(array_keys($current), [$where]);
586
587
				if ($previous === NULL)
588
				{
589
					return $keys;
590
				}
591
592
				return array_merge($previous, $keys);
593
			})
594
		);
595
596
		// Create the CASE blocks for each data set
597
		foreach ($fields as $field)
598
		{
599
			$line =  $this->quoteIdent($field) . " = CASE\n";
600
601
			$cases = [];
602
			foreach ($data as $case)
603
			{
604
				if (array_key_exists($field, $case))
605
				{
606
					$insertData[] = $case[$where];
607
					$insertData[] = $case[$field];
608
					$cases[] = 'WHEN ' . $this->quoteIdent($where) . ' =? '
609
						. 'THEN ? ';
610
				}
611
			}
612
613
			$line .= implode("\n", $cases) . "\n";
614
			$line .= 'ELSE ' . $this->quoteIdent($field) . ' END';
615
616
			$fieldLines[] = $line;
617
		}
618
619
		$sql .= implode(",\n", $fieldLines) . "\n";
620
621
		$whereValues = array_column($data, $where);
622
		foreach ($whereValues as $value)
623
		{
624
			$insertData[] = $value;
625
		}
626
627
		// Create the placeholders for the WHERE IN clause
628
		$placeholders = array_fill(0, count($whereValues), '?');
629
630
		$sql .= 'WHERE ' . $this->quoteIdent($where) . ' IN ';
631
		$sql .= '(' . implode(',', $placeholders) . ')';
632
633
		return [$sql, $insertData, $affectedRows];
634
	}
635
636
	/**
637
	 * Empty the passed table
638
	 *
639
	 * @param string $table
640
	 * @return PDOStatement
641
	 */
642
	public function truncate(string $table): PDOStatement
643
	{
644
		$sql = $this->hasTruncate
645
			? 'TRUNCATE TABLE '
646
			: 'DELETE FROM ';
647
648
		$sql .= $this->quoteTable($table);
649
650
		$this->statement = $this->query($sql);
651
		return $this->statement;
652
	}
653
654
	/**
655
	 * Helper method for quote_ident
656
	 *
657
	 * @param mixed $str
658
	 * @return mixed
659
	 */
660
	public function _quote($str)
661
	{
662
		// Check that the current value is a string,
663
		// and is not already quoted before quoting
664
		// that value, otherwise, return the original value
665
		return (
666
			\is_string($str)
667
			&& strpos($str, $this->escapeCharOpen) !== 0
668
			&& strrpos($str, $this->escapeCharClose) !== 0
669
		)
670
			? "{$this->escapeCharOpen}{$str}{$this->escapeCharClose}"
671
			: $str;
672
673
	}
674
675
	/**
676
	 * Sets the table prefix on the passed string
677
	 *
678
	 * @param string $str
679
	 * @return string
680
	 */
681
	protected function _prefix(string $str): string
682
	{
683
		// Don't prefix an already prefixed table
684
		if (strpos($str, $this->tablePrefix) !== FALSE)
685
		{
686
			return $str;
687
		}
688
689
		return $this->tablePrefix . $str;
690
	}
691
}
692