Completed
Push — develop ( 111dce...b8d476 )
by Timothy
02:35
created

AbstractDriver::prepare_query()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 24
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 2
dl 0
loc 24
rs 8.5125
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
8
 *
9
 * @package     Query
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2012 - 2016 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
16
namespace Query\Drivers;
17
18
use InvalidArgumentException;
19
use PDO;
20
use PDOStatement;
21
22
/**
23
 * Base Database class
24
 *
25
 * Extends PDO to simplify cross-database issues
26
 *
27
 * @package Query
28
 * @subpackage Drivers
29
 */
30
abstract class AbstractDriver extends PDO 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($dsn, $username=NULL, $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()
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)
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($prefix)
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($sql, $data)
205
	{
206
		// Prepare the sql, save the statement for easy access later
207
		$this->statement = $this->prepare($sql);
208
209
		if( ! (is_array($data) || is_object($data)))
210
		{
211
			throw new InvalidArgumentException("Data argument must be an object or associative array");
212
		}
213
214
		// Bind the parameters
215
		foreach($data as $k => $value)
216
		{
217
			// Parameters are 1-based, the data is 0-based
218
			// So, if the key is numeric, add 1
219
			if(is_numeric($k))
220
			{
221
				$k++;
222
			}
223
			$this->statement->bindValue($k, $value);
224
		}
225
226
		return $this->statement;
227
	}
228
229
	/**
230
	 * Create and execute a prepared statement with the provided parameters
231
	 *
232
	 * @param string $sql
233
	 * @param array $params
234
	 * @return PDOStatement
235
	 */
236
	public function prepareExecute($sql, $params)
237
	{
238
		$this->statement = $this->prepareQuery($sql, $params);
239
		$this->statement->execute();
240
241
		return $this->statement;
242
	}
243
244
	/**
245
	 * Returns number of rows affected by an INSERT, UPDATE, DELETE type query
246
	 *
247
	 * @return int
248
	 */
249
	public function affectedRows()
250
	{
251
		// Return number of rows affected
252
		return $this->statement->rowCount();
253
	}
254
255
	/**
256
	 * Prefixes a table if it is not already prefixed
257
	 * @param string $table
258
	 * @return string
259
	 */
260
	public function prefixTable($table)
261
	{
262
		// Add the prefix to the table name
263
		// before quoting it
264
		if ( ! empty($this->tablePrefix))
265
		{
266
			// Split identifier by period, will split into:
267
			// database.schema.table OR
268
			// schema.table OR
269
			// database.table OR
270
			// table
271
			$identifiers = explode('.', $table);
272
			$segments = count($identifiers);
273
274
			// Quote the last item, and add the database prefix
275
			$identifiers[$segments - 1] = $this->_prefix(end($identifiers));
276
277
			// Rejoin
278
			$table = implode('.', $identifiers);
279
		}
280
281
		return $table;
282
	}
283
284
	/**
285
	 * Quote database table name, and set prefix
286
	 *
287
	 * @param string $table
288
	 * @return string
289
	 */
290
	public function quoteTable($table)
291
	{
292
		$table = $this->prefixTable($table);
293
294
		// Finally, quote the table
295
		return $this->quoteIdent($table);
296
	}
297
298
	/**
299
	 * Surrounds the string with the databases identifier escape characters
300
	 *
301
	 * @param mixed $identifier
302
	 * @return string
303
	 */
304
	public function quoteIdent($identifier)
305
	{
306
		if (is_array($identifier))
307
		{
308
			return array_map([$this, __METHOD__], $identifier);
309
		}
310
311
		// Handle comma-separated identifiers
312
		if (strpos($identifier, ',') !== FALSE)
313
		{
314
			$parts = array_map('mb_trim', explode(',', $identifier));
315
			$parts = array_map([$this, __METHOD__], $parts);
316
			$identifier = implode(',', $parts);
317
		}
318
319
		// Split each identifier by the period
320
		$hiers = explode('.', $identifier);
321
		$hiers = array_map('mb_trim', $hiers);
322
323
		// Re-compile the string
324
		$raw = implode('.', array_map([$this, '_quote'], $hiers));
325
326
		// Fix functions
327
		$funcs = [];
328
		preg_match_all("#{$this->escapeCharOpen}([a-zA-Z0-9_]+(\((.*?)\))){$this->escapeCharClose}#iu", $raw, $funcs, PREG_SET_ORDER);
329
		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...
330
		{
331
			// Unquote the function
332
			$raw = str_replace($f[0], $f[1], $raw);
333
334
			// Quote the inside identifiers
335
			$raw = str_replace($f[3], $this->quoteIdent($f[3]), $raw);
336
		}
337
338
		return $raw;
339
340
	}
341
342
	/**
343
	 * Return schemas for databases that list them
344
	 *
345
	 * @return array
346
	 */
347
	public function getSchemas()
348
	{
349
		return NULL;
350
	}
351
352
	/**
353
	 * Return list of tables for the current database
354
	 *
355
	 * @return array
356
	 */
357
	public function getTables()
358
	{
359
		$tables = $this->driverQuery('tableList');
360
		natsort($tables);
361
		return $tables;
362
	}
363
364
	/**
365
	 * Return list of dbs for the current connection, if possible
366
	 *
367
	 * @return array
368
	 */
369
	public function getDbs()
370
	{
371
		return $this->driverQuery('dbList');
372
	}
373
374
	/**
375
	 * Return list of views for the current database
376
	 *
377
	 * @return array
378
	 */
379
	public function getViews()
380
	{
381
		$views = $this->driverQuery('viewList');
382
		sort($views);
383
		return $views;
384
	}
385
386
	/**
387
	 * Return list of sequences for the current database, if they exist
388
	 *
389
	 * @return array
390
	 */
391
	public function getSequences()
392
	{
393
		return $this->driverQuery('sequenceList');
394
	}
395
396
	/**
397
	 * Return list of functions for the current database
398
	 *
399
	 * @return array
400
	 */
401
	public function getFunctions()
402
	{
403
		return $this->driverQuery('functionList', FALSE);
404
	}
405
406
	/**
407
	 * Return list of stored procedures for the current database
408
	 *
409
	 * @return array
410
	 */
411
	public function getProcedures()
412
	{
413
		return $this->driverQuery('procedureList', FALSE);
414
	}
415
416
	/**
417
	 * Return list of triggers for the current database
418
	 *
419
	 * @return array
420
	 */
421
	public function getTriggers()
422
	{
423
		return $this->driverQuery('triggerList', FALSE);
424
	}
425
426
	/**
427
	 * Retrieves an array of non-user-created tables for
428
	 * the connection/database
429
	 *
430
	 * @return array
431
	 */
432
	public function getSystemTables()
433
	{
434
		return $this->driverQuery('systemTableList');
435
	}
436
437
	/**
438
	 * Retrieve column information for the current database table
439
	 *
440
	 * @param string $table
441
	 * @return array
442
	 */
443
	public function getColumns($table)
444
	{
445
		return $this->driverQuery($this->getSql()->columnList($this->prefixTable($table)), FALSE);
446
	}
447
448
	/**
449
	 * Retrieve foreign keys for the table
450
	 *
451
	 * @param string $table
452
	 * @return array
453
	 */
454
	public function getFks($table)
455
	{
456
		return $this->driverQuery($this->getSql()->fkList($table), FALSE);
457
	}
458
459
	/**
460
	 * Retrieve indexes for the table
461
	 *
462
	 * @param string $table
463
	 * @return array
464
	 */
465
	public function getIndexes($table)
466
	{
467
		return $this->driverQuery($this->getSql()->indexList($this->prefixTable($table)), FALSE);
468
	}
469
470
	/**
471
	 * Retrieve list of data types for the database
472
	 *
473
	 * @return array
474
	 */
475
	public function getTypes()
476
	{
477
		return $this->driverQuery('typeList', FALSE);
478
	}
479
480
	/**
481
	 * Method to simplify retrieving db results for meta-data queries
482
	 *
483
	 * @param string|array|null $query
484
	 * @param bool $filteredIndex
485
	 * @return array
486
	 */
487
	public function driverQuery($query, $filteredIndex=TRUE)
488
	{
489
		// Call the appropriate method, if it exists
490
		if (is_string($query) && method_exists($this->sql, $query))
491
		{
492
			$query = $this->getSql()->$query();
493
		}
494
495
		// Return if the values are returned instead of a query,
496
		// or if the query doesn't apply to the driver
497
		if ( ! is_string($query))
498
		{
499
			return $query;
500
		}
501
502
		// Run the query!
503
		$res = $this->query($query);
504
505
		$flag = ($filteredIndex) ? PDO::FETCH_NUM : PDO::FETCH_ASSOC;
506
		$all = $res->fetchAll($flag);
507
508
		return ($filteredIndex) ? \db_filter($all, 0) : $all;
509
	}
510
511
	/**
512
	 * Return the number of rows returned for a SELECT query
513
	 *
514
	 * @see http://us3.php.net/manual/en/pdostatement.rowcount.php#87110
515
	 * @return int|null
516
	 */
517
	public function numRows()
518
	{
519
		$regex = '/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/i';
520
		$output = [];
521
522
		if (preg_match($regex, $this->lastQuery, $output) > 0)
523
		{
524
			$stmt = $this->query("SELECT COUNT(*) FROM {$output[1]}");
525
			return (int) $stmt->fetchColumn();
526
		}
527
528
		return NULL;
529
	}
530
531
	/**
532
	 * Create sql for batch insert
533
	 *
534
	 * @param string $table
535
	 * @param array|object $data
536
	 * @return null|array<string|array|null>
537
	 */
538
	public function insertBatch($table, $data=[])
539
	{
540
		$data = (array) $data;
541
		$firstRow = (array) current($data);
542
		if (is_scalar($firstRow))
543
		{
544
			return NULL;
545
		}
546
547
		// Values for insertion
548
		$vals = [];
549
		foreach($data as $group)
550
		{
551
			$vals = array_merge($vals, array_values($group));
552
		}
553
		$table = $this->quoteTable($table);
554
		$fields = array_keys($firstRow);
555
556
		$sql = "INSERT INTO {$table} ("
557
			. implode(',', $this->quoteIdent($fields))
558
			. ") VALUES ";
559
560
		// Create the placeholder groups
561
		$params = array_fill(0, count($fields), '?');
562
		$paramString = "(" . implode(',', $params) . ")";
563
		$paramList = array_fill(0, count($data), $paramString);
564
565
		// Append the placeholder groups to the query
566
		$sql .= implode(',', $paramList);
567
568
		return [$sql, $vals];
569
	}
570
571
	/**
572
	 * Creates a batch update, and executes it.
573
	 * Returns the number of affected rows
574
	 *
575
	 * @param string $table
576
	 * @param array|object $data
577
	 * @param string $where
578
	 * @return int|null
579
	 */
580
	public function updateBatch($table, $data, $where)
581
	{
582
		// @TODO implement
583
		return NULL;
584
	}
585
586
	/**
587
	 * Helper method for quote_ident
588
	 *
589
	 * @param mixed $str
590
	 * @return mixed
591
	 */
592
	public function _quote($str)
593
	{
594
		// Check that the current value is a string,
595
		// and is not already quoted before quoting
596
		// that value, otherwise, return the original value
597
		return (
598
			is_string($str)
599
			&& strpos($str, $this->escapeCharOpen) !== 0
600
			&& strrpos($str, $this->escapeCharClose) !== 0
601
		)
602
			? "{$this->escapeCharOpen}{$str}{$this->escapeCharClose}"
603
			: $str;
604
605
	}
606
607
	/**
608
	 * Sets the table prefix on the passed string
609
	 *
610
	 * @param string $str
611
	 * @return string
612
	 */
613
	protected function _prefix($str)
614
	{
615
		// Don't prefix an already prefixed table
616
		if (strpos($str, $this->tablePrefix) !== FALSE)
617
		{
618
			return $str;
619
		}
620
621
		return $this->tablePrefix . $str;
622
	}
623
624
	/**
625
	 * Empty the passed table
626
	 *
627
	 * @param string $table
628
	 * @return PDOStatement
629
	 */
630
	public function truncate($table)
631
	{
632
		$sql = ($this->hasTruncate)
633
			? 'TRUNCATE TABLE '
634
			: 'DELETE FROM ';
635
636
		$sql .= $this->quoteTable($table);
637
638
		$this->statement = $this->query($sql);
639
		return $this->statement;
640
	}
641
642
}