Completed
Push — develop ( 2db7ad...3eb4d8 )
by Timothy
10:51
created

AbstractDriver::insert_batch()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 3
nop 2
dl 0
loc 32
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Query
4
 *
5
 * SQL Query Builder / Database Abstraction Layer
6
 *
7
 * PHP version 5.4
8
 *
9
 * @package     Query
10
 * @author      Timothy J. Warren <[email protected]>
11
 * @copyright   2012 - 2015 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 PDO;
19
use PDOStatement;
20
use InvalidArgumentException;
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 $escape_char_open = '"';
43
44
	/**
45
	 * End character to escape identifiers
46
	 * @var string
47
	 */
48
	protected $escape_char_close = '"';
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 $last_query;
67
68
	/**
69
	 * Prefix to apply to table names
70
	 * @var string
71
	 */
72
	protected $table_prefix = '';
73
74
	/**
75
	 * Whether the driver supports 'TRUNCATE'
76
	 * @var boolean
77
	 */
78
	protected $has_truncate = TRUE;
79
80
	/**
81
	 * PDO constructor wrapper
82
	 *
83
	 * @param string $dsn
84
	 * @param string $username
85
	 * @param string $password
86
	 * @param array $driver_options
87
	 */
88
	public function __construct($dsn, $username=NULL, $password=NULL, array $driver_options=[])
89
	{
90
		// Set PDO to display errors as exceptions, and apply driver options
91
		$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
92
		parent::__construct($dsn, $username, $password, $driver_options);
93
94
		$this->_load_sub_classes();
95
	}
96
97
	// --------------------------------------------------------------------------
98
99
	/**
100
	 * Loads the subclasses for the driver
101
	 *
102
	 * @return void
103
	 */
104
	protected function _load_sub_classes()
105
	{
106
		// Load the sql and util class for the driver
107
		$this_class = get_class($this);
108
		$ns_array = explode("\\", $this_class);
109
		array_pop($ns_array);
110
		$driver = array_pop($ns_array);
111
		$sql_class = __NAMESPACE__ . "\\{$driver}\\SQL";
112
		$util_class = __NAMESPACE__ . "\\{$driver}\\Util";
113
114
		$this->sql = new $sql_class();
115
		$this->util = new $util_class($this);
116
	}
117
118
	// --------------------------------------------------------------------------
119
120
	/**
121
	 * Allow invoke to work on table object
122
	 *
123
	 * @codeCoverageIgnore
124
	 * @param string $name
125
	 * @param array $args
126
	 * @return mixed
127
	 */
128
	public function __call($name, $args = [])
129
	{
130
		if (
131
			isset($this->$name)
132
			&& is_object($this->$name)
133
			&& method_exists($this->$name, '__invoke')
134
		)
135
		{
136
			return call_user_func_array([$this->$name, '__invoke'], $args);
137
		}
138
	}
139
140
	// --------------------------------------------------------------------------
141
	// ! Accessors / Mutators
142
	// --------------------------------------------------------------------------
143
144
	/**
145
	 * Get the last sql query executed
146
	 *
147
	 * @return string
148
	 */
149
	public function get_last_query()
150
	{
151
		return $this->last_query;
152
	}
153
154
	// --------------------------------------------------------------------------
155
156
	/**
157
	 * Set the last query sql
158
	 *
159
	 * @param string $query_string
160
	 * @return void
161
	 */
162
	public function set_last_query($query_string)
163
	{
164
		$this->last_query = $query_string;
165
	}
166
167
	// --------------------------------------------------------------------------
168
169
	/**
170
	 * Get the SQL class for the current driver
171
	 *
172
	 * @return SQLInterface
173
	 */
174
	public function get_sql()
175
	{
176
		return $this->sql;
177
	}
178
179
	// --------------------------------------------------------------------------
180
181
	/**
182
	 * Get the Util class for the current driver
183
	 *
184
	 * @return AbstractUtil
185
	 */
186
	public function get_util()
187
	{
188
		return $this->util;
189
	}
190
191
	// --------------------------------------------------------------------------
192
193
	/**
194
	 * Set the common table name prefix
195
	 *
196
	 * @param string $prefix
197
	 * @return void
198
	 */
199
	public function set_table_prefix($prefix)
200
	{
201
		$this->table_prefix = $prefix;
202
	}
203
204
	// --------------------------------------------------------------------------
205
	// ! Concrete functions that can be overridden in child classes
206
	// --------------------------------------------------------------------------
207
208
	/**
209
	 * Simplifies prepared statements for database queries
210
	 *
211
	 * @param string $sql
212
	 * @param array $data
213
	 * @return PDOStatement | FALSE
214
	 * @throws InvalidArgumentException
215
	 */
216
	public function prepare_query($sql, $data)
217
	{
218
		// Prepare the sql, save the statement for easy access later
219
		$this->statement = $this->prepare($sql);
220
221
		if( ! (is_array($data) || is_object($data)))
222
		{
223
			throw new InvalidArgumentException("Data argument must be an object or associative array");
224
		}
225
226
		// Bind the parameters
227
		foreach($data as $k => $value)
228
		{
229
			// Parameters are 1-based, the data is 0-based
230
			// So, if the key is numeric, add 1
231
			if(is_numeric($k))
232
			{
233
				$k++;
234
			}
235
			$this->statement->bindValue($k, $value);
236
		}
237
238
		return $this->statement;
239
	}
240
241
	// -------------------------------------------------------------------------
242
243
	/**
244
	 * Create and execute a prepared statement with the provided parameters
245
	 *
246
	 * @param string $sql
247
	 * @param array $params
248
	 * @return PDOStatement
249
	 */
250
	public function prepare_execute($sql, $params)
251
	{
252
		$this->statement = $this->prepare_query($sql, $params);
253
		$this->statement->execute();
254
255
		return $this->statement;
256
	}
257
258
	// -------------------------------------------------------------------------
259
260
	/**
261
	 * Returns number of rows affected by an INSERT, UPDATE, DELETE type query
262
	 *
263
	 * @return int
264
	 */
265
	public function affected_rows()
266
	{
267
		// Return number of rows affected
268
		return $this->statement->rowCount();
269
	}
270
271
	// --------------------------------------------------------------------------
272
273
	/**
274
	 * Prefixes a table if it is not already prefixed
275
	 * @param string $table
276
	 * @return string
277
	 */
278
	public function prefix_table($table)
279
	{
280
		// Add the prefix to the table name
281
		// before quoting it
282
		if ( ! empty($this->table_prefix))
283
		{
284
			// Split identifier by period, will split into:
285
			// database.schema.table OR
286
			// schema.table OR
287
			// database.table OR
288
			// table
289
			$identifierifiers = explode('.', $table);
290
			$segments = count($identifierifiers);
291
292
			// Quote the last item, and add the database prefix
293
			$identifierifiers[$segments - 1] = $this->_prefix(end($identifierifiers));
294
295
			// Rejoin
296
			$table = implode('.', $identifierifiers);
297
		}
298
299
		return $table;
300
	}
301
302
	// --------------------------------------------------------------------------
303
304
	/**
305
	 * Quote database table name, and set prefix
306
	 *
307
	 * @param string $table
308
	 * @return string
309
	 */
310
	public function quote_table($table)
311
	{
312
		$table = $this->prefix_table($table);
313
314
		// Finally, quote the table
315
		return $this->quote_ident($table);
316
	}
317
318
	// --------------------------------------------------------------------------
319
320
	/**
321
	 * Surrounds the string with the databases identifier escape characters
322
	 *
323
	 * @param mixed $identifier
324
	 * @return string
325
	 */
326
	public function quote_ident($identifier)
327
	{
328
		if (is_array($identifier))
329
		{
330
			return array_map([$this, __METHOD__], $identifier);
331
		}
332
333
		// Handle comma-separated identifiers
334
		if (strpos($identifier, ',') !== FALSE)
335
		{
336
			$parts = array_map('mb_trim', explode(',', $identifier));
337
			$parts = array_map([$this, __METHOD__], $parts);
338
			$identifier = implode(',', $parts);
339
		}
340
341
		// Split each identifier by the period
342
		$hiers = explode('.', $identifier);
343
		$hiers = array_map('mb_trim', $hiers);
344
345
		// Re-compile the string
346
		$raw = implode('.', array_map([$this, '_quote'], $hiers));
347
348
		// Fix functions
349
		$funcs = [];
350
		preg_match_all("#{$this->escape_char_open}([a-zA-Z0-9_]+(\((.*?)\))){$this->escape_char_close}#iu", $raw, $funcs, PREG_SET_ORDER);
351
		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...
352
		{
353
			// Unquote the function
354
			$raw = str_replace($f[0], $f[1], $raw);
355
356
			// Quote the inside identifiers
357
			$raw = str_replace($f[3], $this->quote_ident($f[3]), $raw);
358
		}
359
360
		return $raw;
361
362
	}
363
364
	// -------------------------------------------------------------------------
365
366
	/**
367
	 * Return schemas for databases that list them
368
	 *
369
	 * @return array
370
	 */
371
	public function get_schemas()
372
	{
373
		return NULL;
374
	}
375
376
	// -------------------------------------------------------------------------
377
378
	/**
379
	 * Return list of tables for the current database
380
	 *
381
	 * @return array
382
	 */
383
	public function get_tables()
384
	{
385
		$tables = $this->driver_query('table_list');
386
		natsort($tables);
387
		return $tables;
388
	}
389
390
	// -------------------------------------------------------------------------
391
392
	/**
393
	 * Return list of dbs for the current connection, if possible
394
	 *
395
	 * @return array
396
	 */
397
	public function get_dbs()
398
	{
399
		return $this->driver_query('db_list');
400
	}
401
402
	// -------------------------------------------------------------------------
403
404
	/**
405
	 * Return list of views for the current database
406
	 *
407
	 * @return array
408
	 */
409
	public function get_views()
410
	{
411
		$views = $this->driver_query('view_list');
412
		sort($views);
413
		return $views;
414
	}
415
416
	// -------------------------------------------------------------------------
417
418
	/**
419
	 * Return list of sequences for the current database, if they exist
420
	 *
421
	 * @return array
422
	 */
423
	public function get_sequences()
424
	{
425
		return $this->driver_query('sequence_list');
426
	}
427
428
	// -------------------------------------------------------------------------
429
430
	/**
431
	 * Return list of functions for the current database
432
	 *
433
	 * @return array
434
	 */
435
	public function get_functions()
436
	{
437
		return $this->driver_query('function_list', FALSE);
438
	}
439
440
	// -------------------------------------------------------------------------
441
442
	/**
443
	 * Return list of stored procedures for the current database
444
	 *
445
	 * @return array
446
	 */
447
	public function get_procedures()
448
	{
449
		return $this->driver_query('procedure_list', FALSE);
450
	}
451
452
	// -------------------------------------------------------------------------
453
454
	/**
455
	 * Return list of triggers for the current database
456
	 *
457
	 * @return array
458
	 */
459
	public function get_triggers()
460
	{
461
		return $this->driver_query('trigger_list', FALSE);
462
	}
463
464
	// -------------------------------------------------------------------------
465
466
	/**
467
	 * Retrieves an array of non-user-created tables for
468
	 * the connection/database
469
	 *
470
	 * @return array
471
	 */
472
	public function get_system_tables()
473
	{
474
		return $this->driver_query('system_table_list');
475
	}
476
477
	// --------------------------------------------------------------------------
478
479
	/**
480
	 * Retrieve column information for the current database table
481
	 *
482
	 * @param string $table
483
	 * @return array
484
	 */
485
	public function get_columns($table)
486
	{
487
		return $this->driver_query($this->get_sql()->column_list($this->prefix_table($table)), FALSE);
488
	}
489
490
	// --------------------------------------------------------------------------
491
492
	/**
493
	 * Retrieve foreign keys for the table
494
	 *
495
	 * @param string $table
496
	 * @return array
497
	 */
498
	public function get_fks($table)
499
	{
500
		return $this->driver_query($this->get_sql()->fk_list($table), FALSE);
501
	}
502
503
	// --------------------------------------------------------------------------
504
505
	/**
506
	 * Retrieve indexes for the table
507
	 *
508
	 * @param string $table
509
	 * @return array
510
	 */
511
	public function get_indexes($table)
512
	{
513
		return $this->driver_query($this->get_sql()->index_list($this->prefix_table($table)), FALSE);
514
	}
515
516
	// --------------------------------------------------------------------------
517
518
	/**
519
	 * Retrieve list of data types for the database
520
	 *
521
	 * @return array
522
	 */
523
	public function get_types()
524
	{
525
		return $this->driver_query('type_list', FALSE);
526
	}
527
528
	// -------------------------------------------------------------------------
529
530
	/**
531
	 * Method to simplify retrieving db results for meta-data queries
532
	 *
533
	 * @param string|array|null $query
534
	 * @param bool $filtered_index
535
	 * @return array
536
	 */
537
	public function driver_query($query, $filtered_index=TRUE)
538
	{
539
		// Call the appropriate method, if it exists
540
		if (is_string($query) && method_exists($this->sql, $query))
541
		{
542
			$query = $this->get_sql()->$query();
543
		}
544
545
		// Return if the values are returned instead of a query,
546
		// or if the query doesn't apply to the driver
547
		if ( ! is_string($query))
548
		{
549
			return $query;
550
		}
551
552
		// Run the query!
553
		$res = $this->query($query);
554
555
		$flag = ($filtered_index) ? PDO::FETCH_NUM : PDO::FETCH_ASSOC;
556
		$all = $res->fetchAll($flag);
557
558
		return ($filtered_index) ? \db_filter($all, 0) : $all;
559
	}
560
561
	// --------------------------------------------------------------------------
562
563
	/**
564
	 * Return the number of rows returned for a SELECT query
565
	 *
566
	 * @see http://us3.php.net/manual/en/pdostatement.rowcount.php#87110
567
	 * @return int|null
568
	 */
569
	public function num_rows()
570
	{
571
		$regex = '/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/i';
572
		$output = [];
573
574
		if (preg_match($regex, $this->last_query, $output) > 0)
575
		{
576
			$stmt = $this->query("SELECT COUNT(*) FROM {$output[1]}");
577
			return (int) $stmt->fetchColumn();
578
		}
579
580
		return NULL;
581
	}
582
583
	// --------------------------------------------------------------------------
584
585
	/**
586
	 * Create sql for batch insert
587
	 *
588
	 * @param string $table
589
	 * @param array|object $data
590
	 * @return null|array<string|array|null>
591
	 */
592
	public function insert_batch($table, $data=[])
593
	{
594
		$data = (array) $data;
595
		$first_row = (array) current($data);
596
		if (is_scalar($first_row))
597
		{
598
			return NULL;
599
		}
600
601
		// Values for insertion
602
		$vals = [];
603
		foreach($data as $group)
604
		{
605
			$vals = array_merge($vals, array_values($group));
606
		}
607
		$table = $this->quote_table($table);
608
		$fields = array_keys($first_row);
609
610
		$sql = "INSERT INTO {$table} ("
611
			. implode(',', $this->quote_ident($fields))
612
			. ") VALUES ";
613
614
		// Create the placeholder groups
615
		$params = array_fill(0, count($fields), '?');
616
		$param_string = "(" . implode(',', $params) . ")";
617
		$param_list = array_fill(0, count($data), $param_string);
618
619
		// Append the placeholder groups to the query
620
		$sql .= implode(',', $param_list);
621
622
		return [$sql, $vals];
623
	}
624
625
	// --------------------------------------------------------------------------
626
627
	/**
628
	 * Creates a batch update, and executes it.
629
	 * Returns the number of affected rows
630
	 *
631
	 * @param string $table
632
	 * @param array|object $data
633
	 * @param string $where
634
	 * @return int|null
635
	 */
636
	public function update_batch($table, $data, $where)
637
	{
638
		// @TODO implement
639
		return NULL;
640
	}
641
642
	// --------------------------------------------------------------------------
643
644
	/**
645
	 * Helper method for quote_ident
646
	 *
647
	 * @param mixed $str
648
	 * @return mixed
649
	 */
650
	public function _quote($str)
651
	{
652
		// Check that the current value is a string,
653
		// and is not already quoted before quoting
654
		// that value, otherwise, return the original value
655
		return (
656
			is_string($str)
657
			&& strpos($str, $this->escape_char_open) !== 0
658
			&& strrpos($str, $this->escape_char_close) !== 0
659
		)
660
			? "{$this->escape_char_open}{$str}{$this->escape_char_close}"
661
			: $str;
662
663
	}
664
665
	// --------------------------------------------------------------------------
666
667
	/**
668
	 * Sets the table prefix on the passed string
669
	 *
670
	 * @param string $str
671
	 * @return string
672
	 */
673
	protected function _prefix($str)
674
	{
675
		// Don't prefix an already prefixed table
676
		if (strpos($str, $this->table_prefix) !== FALSE)
677
		{
678
			return $str;
679
		}
680
681
		return $this->table_prefix.$str;
682
	}
683
684
	// -------------------------------------------------------------------------
685
686
	/**
687
	 * Empty the passed table
688
	 *
689
	 * @param string $table
690
	 * @return PDOStatement
691
	 */
692
	public function truncate($table)
693
	{
694
		$sql = ($this->has_truncate)
695
			? 'TRUNCATE TABLE '
696
			: 'DELETE FROM ';
697
698
		$sql .= $this->quote_table($table);
699
700
		$this->statement = $this->query($sql);
701
		return $this->statement;
702
	}
703
704
}
705
// End of db_pdo.php