Completed
Push — develop ( 56c929...306797 )
by Timothy
02:51
created

AbstractDriver::updateBatch()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 63
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 32
nc 8
nop 3
dl 0
loc 63
rs 8.6498
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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