Completed
Push — master ( 56efe1...a850eb )
by Lars
100:15 queued 98:37
created

DB::multi_query()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 57
Code Lines 35

Duplication

Lines 5
Ratio 8.77 %

Code Coverage

Tests 18
CRAP Score 13.169

Importance

Changes 0
Metric Value
dl 5
loc 57
ccs 18
cts 20
cp 0.9
rs 6.5962
c 0
b 0
f 0
cc 13
eloc 35
nc 12
nop 1
crap 13.169

How to fix   Long Method    Complexity   

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
2
3
namespace voku\db;
4
5
use voku\cache\Cache;
6
use voku\db\exceptions\DBConnectException;
7
use voku\db\exceptions\DBGoneAwayException;
8
use voku\db\exceptions\QueryException;
9
use voku\helper\UTF8;
10
11
/**
12
 * DB: this handles DB queries via MySQLi
13
 *
14
 * @package voku\db
15
 */
16
final class DB
17
{
18
19
  /**
20
   * @var int
21
   */
22
  public $query_count = 0;
23
24
  /**
25
   * @var \mysqli
26
   */
27
  private $link = false;
28
29
  /**
30
   * @var bool
31
   */
32
  private $connected = false;
33
34
  /**
35
   * @var array
36
   */
37
  private $mysqlDefaultTimeFunctions;
38
39
  /**
40
   * @var string
41
   */
42
  private $hostname = '';
43
44
  /**
45
   * @var string
46
   */
47
  private $username = '';
48
49
  /**
50
   * @var string
51
   */
52
  private $password = '';
53
54
  /**
55
   * @var string
56
   */
57
  private $database = '';
58
59
  /**
60
   * @var int
61
   */
62
  private $port = 3306;
63
64
  /**
65
   * @var string
66
   */
67
  private $charset = 'utf8';
68
69
  /**
70
   * @var string
71
   */
72
  private $socket = '';
73
74
  /**
75
   * @var bool
76
   */
77
  private $session_to_db = false;
78
79
  /**
80
   * @var bool
81
   */
82
  private $_in_transaction = false;
83
84
  /**
85
   * @var bool
86
   */
87
  private $_convert_null_to_empty_string = false;
88
89
  /**
90
   * @var bool
91
   */
92
  private $_ssl = false;
93
  /**
94
   * The path name to the key file
95
   *
96
   * @var string
97
   */
98
  private $_clientkey;
99
  /**
100
   * The path name to the certificate file
101
   *
102
   * @var string
103
   */
104
  private $_clientcert;
105
  /**
106
   * The path name to the certificate authority file
107
   *
108
   * @var string
109 10
   */
110
  private $_cacert;
111 10
112
  /**
113 10
   * @var Debug
114
   */
115 10
  private $_debug;
116 10
117 10
  /**
118 10
   * __construct()
119 10
   *
120 10
   * @param string         $hostname
121 10
   * @param string         $username
122 10
   * @param string         $password
123 10
   * @param string         $database
124 10
   * @param int            $port
125 10
   * @param string         $charset
126
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
127 10
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
128
   * @param string         $logger_class_name
129 7
   * @param string         $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
130
   * @param array          $extra_config <p>
131 4
   *                                     'session_to_db' => false|true<br>
132
   *                                     'socket' => 'string (path)'<br>
133 4
   *                                     'ssl' => 'bool'<br>
134
   *                                     'clientkey' => 'string (path)'<br>
135 4
   *                                     'clientcert' => 'string (path)'<br>
136
   *                                     'cacert' => 'string (path)'<br>
137 4
   *                                     </p>
138
   */
139 4
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config = array())
140
  {
141 4
    $this->connected = false;
142
143 4
    $this->_debug = new Debug($this);
144
145 4
    $this->_loadConfig(
146
        $hostname,
147 4
        $username,
148
        $password,
149 4
        $database,
150
        $port,
151 4
        $charset,
152
        $exit_on_error,
153 4
        $echo_on_error,
154
        $logger_class_name,
155 4
        $logger_level,
156
        $extra_config
157 4
    );
158
159 4
    $this->connect();
160
161
    $this->mysqlDefaultTimeFunctions = array(
162
      // Returns the current date.
163
      'CURDATE()',
164
      // CURRENT_DATE	| Synonyms for CURDATE()
165
      'CURRENT_DATE()',
166
      // CURRENT_TIME	| Synonyms for CURTIME()
167
      'CURRENT_TIME()',
168
      // CURRENT_TIMESTAMP | Synonyms for NOW()
169
      'CURRENT_TIMESTAMP()',
170
      // Returns the current time.
171
      'CURTIME()',
172
      // Synonym for NOW()
173
      'LOCALTIME()',
174
      // Synonym for NOW()
175
      'LOCALTIMESTAMP()',
176
      // Returns the current date and time.
177
      'NOW()',
178 10
      // Returns the time at which the function executes.
179
      'SYSDATE()',
180 10
      // Returns a UNIX timestamp.
181 10
      'UNIX_TIMESTAMP()',
182 10
      // Returns the current UTC date.
183 10
      'UTC_DATE()',
184
      // Returns the current UTC time.
185 10
      'UTC_TIME()',
186 4
      // Returns the current UTC date and time.
187 4
      'UTC_TIMESTAMP()',
188
    );
189 10
  }
190 4
191 4
  /**
192
   * Prevent the instance from being cloned.
193
   *
194 7
   * @return void
195
   */
196
  private function __clone()
197
  {
198 10
  }
199
200
  /**
201
   * __destruct
202 10
   *
203
   */
204 10
  public function __destruct()
205 10
  {
206
    // close the connection only if we don't save PHP-SESSION's in DB
207 10
    if ($this->session_to_db === false) {
208 10
      $this->close();
209 10
    }
210
  }
211 10
212 10
  /**
213 10
   * __wakeup
214
   *
215 10
   * @return void
216 10
   */
217
  public function __wakeup()
218 10
  {
219
    $this->reconnect();
220 10
  }
221
222
  /**
223
   * Load the config from the constructor.
224
   *
225
   * @param string         $hostname
226
   * @param string         $username
227
   * @param string         $password
228
   * @param string         $database
229
   * @param int|string     $port          <p>default is (int)3306</p>
230 10
   * @param string         $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
231
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
232
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
233
   * @param string         $logger_class_name
234 10
   * @param string         $logger_level
235 10
   * @param array          $extra_config <p>
236 9
   *                                     'session_to_db' => false|true<br>
237 9
   *                                     'socket' => 'string (path)'<br>
238 8
   *                                     'ssl' => 'bool'<br>
239 10
   *                                     'clientkey' => 'string (path)'<br>
240
   *                                     'clientcert' => 'string (path)'<br>
241 3
   *                                     'cacert' => 'string (path)'<br>
242 1
   *                                     </p>
243
   *
244
   * @return bool
245 2
   */
246 1
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
247
  {
248
    $this->hostname = (string)$hostname;
249 1
    $this->username = (string)$username;
250 1
    $this->password = (string)$password;
251
    $this->database = (string)$database;
252
253
    if ($charset) {
254
      $this->charset = (string)$charset;
255
    }
256 7
257
    if ($port) {
258
      $this->port = (int)$port;
259
    } else {
260
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
261
      /** @noinspection UsageOfSilenceOperatorInspection */
262
      $this->port = (int)@ini_get('mysqli.default_port');
263
    }
264
265
    // fallback
266 9
    if (!$this->port) {
267
      $this->port = 3306;
268 9
    }
269 1
270
    if (!$this->socket) {
271
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
272 9
      $this->socket = @ini_get('mysqli.default_socket');
273
    }
274 9
275
    if ($exit_on_error === true || $exit_on_error === false) {
276 9
      $this->_debug->setExitOnError($exit_on_error);
277 9
    }
278 9
279
    if ($echo_on_error === true || $echo_on_error === false) {
280
      $this->_debug->setEchoOnError($echo_on_error);
281 9
    }
282 9
283 9
    $this->_debug->setLoggerClassName($logger_class_name);
284 9
    $this->_debug->setLoggerLevel($logger_level);
285 9
286 9
    if (is_array($extra_config) === true) {
287 9
288 9
      if (isset($extra_config['session_to_db'])) {
289 9
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
290 9
      }
291 3
292 3
      if (isset($extra_config['socket'])) {
293 3
        $this->socket = $extra_config['socket'];
294
      }
295 6
296
      if (isset($extra_config['ssl'])) {
297 6
        $this->_ssl = $extra_config['ssl'];
298
      }
299
300
      if (isset($extra_config['clientkey'])) {
301
        $this->_clientkey = $extra_config['clientkey'];
302
      }
303
304 6
      if (isset($extra_config['clientcert'])) {
305
        $this->_clientcert = $extra_config['clientcert'];
306 6
      }
307
308
      if (isset($extra_config['cacert'])) {
309
        $this->_cacert = $extra_config['cacert'];
310
      }
311
312
    } else {
313
      // only for backward compatibility
314 45
      $this->session_to_db = (boolean)$extra_config;
315
    }
316 45
317
    return $this->showConfigError();
318
  }
319
320
  /**
321
   * Parses arrays with value pairs and generates SQL to use in queries.
322
   *
323
   * @param array  $arrayPair
324
   * @param string $glue <p>This is the separator.</p>
325
   *
326 2
   * @return string
327
   *
328 2
   * @internal
329
   */
330
  public function _parseArrayPair($arrayPair, $glue = ',')
331
  {
332
    // init
333
    $sql = '';
334
335
    /** @noinspection IsEmptyFunctionUsageInspection */
336
    if (empty($arrayPair)) {
337
      return '';
338
    }
339
340
    $arrayPairCounter = 0;
341
    foreach ($arrayPair as $_key => $_value) {
342
      $_connector = '=';
343
      $_glueHelper = '';
344
      $_key_upper = strtoupper($_key);
345
346
      if (strpos($_key_upper, ' NOT') !== false) {
347
        $_connector = 'NOT';
348
      }
349
350
      if (strpos($_key_upper, ' IS') !== false) {
351
        $_connector = 'IS';
352
      }
353
354
      if (strpos($_key_upper, ' IS NOT') !== false) {
355
        $_connector = 'IS NOT';
356
      }
357
358
      if (strpos($_key_upper, ' IN') !== false) {
359
        $_connector = 'IN';
360
      }
361
362
      if (strpos($_key_upper, ' NOT IN') !== false) {
363
        $_connector = 'NOT IN';
364
      }
365
366
      if (strpos($_key_upper, ' BETWEEN') !== false) {
367
        $_connector = 'BETWEEN';
368
      }
369
370
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
371
        $_connector = 'NOT BETWEEN';
372
      }
373
374
      if (strpos($_key_upper, ' LIKE') !== false) {
375
        $_connector = 'LIKE';
376
      }
377
378
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
379
        $_connector = 'NOT LIKE';
380
      }
381
382 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
383 57
        $_connector = '>';
384
      }
385
386 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
387
        $_connector = '<';
388 57
      }
389
390
      if (strpos($_key_upper, ' >=') !== false) {
391
        $_connector = '>=';
392
      }
393 57
394
      if (strpos($_key_upper, ' <=') !== false) {
395
        $_connector = '<=';
396 57
      }
397 57
398 11
      if (strpos($_key_upper, ' <>') !== false) {
399 57
        $_connector = '<>';
400 11
      }
401
402
      if (strpos($_key_upper, ' OR') !== false) {
403 57
        $_glueHelper = 'OR';
404 57
      }
405 57
406
      if (strpos($_key_upper, ' AND') !== false) {
407 57
        $_glueHelper = 'AND';
408 10
      }
409 10
410 10
      if (is_array($_value) === true) {
411 10
        foreach ($_value as $oldKey => $oldValue) {
412 10
          $_value[$oldKey] = $this->secure($oldValue);
413 10
        }
414 10
415 10
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
416 10
          $_value = '(' . implode(',', $_value) . ')';
417 10
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
418 10
          $_value = '(' . implode(' AND ', $_value) . ')';
419
        }
420 10
421
      } else {
422 4
        $_value = $this->secure($_value);
423 1
      }
424 1
425 4
      $quoteString = $this->quote_string(
426
          trim(
427 57
              str_ireplace(
428
                  array(
429
                      $_connector,
430
                      $_glueHelper,
431
                  ),
432
                  '',
433
                  $_key
434
              )
435
          )
436
      );
437
438
      if (!is_array($_value)) {
439
        $_value = array($_value);
440
      }
441
442
      if (!$_glueHelper) {
443
        $_glueHelper = $glue;
444
      }
445
446
      $tmpCounter = 0;
447
      foreach ($_value as $valueInner) {
448
449
        $_glueHelperInner = $_glueHelper;
450 35
451
        if ($arrayPairCounter === 0) {
452 35
453
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
454
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
455
          } elseif ($tmpCounter === 0) {
456 35
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
457 4
          }
458
459 4
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
460
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
461
        }
462
463
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
464 33
        $tmpCounter++;
465 3
      }
466 33
467 3
      if ($_glueHelper === 'OR') {
468 33
        $sql .= ' ) ';
469 3
      }
470 3
471
      $arrayPairCounter++;
472 33
    }
473 33
474 33
    return $sql;
475
  }
476 33
477
  /**
478 33
   * _parseQueryParams
479 33
   *
480 28
   * @param string $sql
481 28
   * @param array  $params
482 24
   *
483
   * @return string
484
   */
485 33
  private function _parseQueryParams($sql, array $params)
486
  {
487
    // is there anything to parse?
488 27
    if (strpos($sql, '?') === false) {
489
      return $sql;
490
    }
491 27
492
    if (count($params) > 0) {
493
      $parseKey = md5(uniqid((string)mt_rand(), true));
494
      $sql = str_replace('?', $parseKey, $sql);
495 25
496
      $k = 0;
497
      while (strpos($sql, $parseKey) !== false) {
498 23
        $value = $this->secure($params[$k]);
499 22
        $sql = UTF8::str_replace_first($parseKey, $value, $sql);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by \voku\helper\UTF8::str_r...parseKey, $value, $sql) on line 499 can also be of type array<integer,string>; however, voku\helper\UTF8::str_replace_first() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
500 22
        $k++;
501
      }
502 22
    }
503
504
    return $sql;
505
  }
506 8
507 8
  /**
508 8
   * Gets the number of affected rows in a previous MySQL operation.
509
   *
510 8
   * @return int
511
   */
512
  public function affected_rows()
513
  {
514
    return \mysqli_affected_rows($this->link);
515
  }
516
517
  /**
518
   * Begins a transaction, by turning off auto commit.
519
   *
520 8
   * @return bool <p>This will return true or false indicating success of transaction</p>
521
   */
522 8 View Code Duplication
  public function beginTransaction()
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...
523
  {
524
    if ($this->_in_transaction === true) {
525
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
526
      return false;
527
    }
528
529
    $this->clearErrors(); // needed for "$this->endTransaction()"
530
    $this->_in_transaction = true;
531
    $return = \mysqli_autocommit($this->link, false);
532
    if ($return === false) {
533 3
      $this->_in_transaction = false;
534
    }
535
536 3
    return $return;
537
  }
538
539
540 3
  /**
541 3
   * Clear the errors in "_debug->_errors".
542 3
   *
543
   * @return bool
544 3
   */
545 3
  public function clearErrors()
546 3
  {
547 3
    return $this->_debug->clearErrors();
548 3
  }
549 3
550 3
  /**
551
   * Closes a previously opened database connection.
552 3
   */
553
  public function close()
554
  {
555
    $this->connected = false;
556
    if (!$this->link) {
557
      return;
558
    }
559
560
    \mysqli_close($this->link);
561
  }
562
563
  /**
564
   * Open a new connection to the MySQL server.
565
   *
566
   * @return bool
567
   *
568
   * @throws DBConnectException
569
   */
570
  public function connect()
571
  {
572
    if ($this->isReady()) {
573
      return true;
574
    }
575
576
    $flags = null;
577
578
    \mysqli_report(MYSQLI_REPORT_STRICT);
579
    try {
580
      $this->link = \mysqli_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like \mysqli_init() of type object<mysql> is incompatible with the declared type object<mysqli> of property $link.

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...
581
582
      if (Helper::isMysqlndIsUsed() === true) {
583
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
584
      }
585
586
      if ($this->_ssl === true) {
587
588
        if (empty($this->clientcert)) {
0 ignored issues
show
Bug introduced by
The property clientcert does not seem to exist. Did you mean _clientcert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
589
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
590
        }
591
592
        if (empty($this->clientkey)) {
0 ignored issues
show
Bug introduced by
The property clientkey does not seem to exist. Did you mean _clientkey?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
593
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
594
        }
595
596
        if (empty($this->cacert)) {
0 ignored issues
show
Bug introduced by
The property cacert does not seem to exist. Did you mean _cacert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
597 26
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
598
        }
599
600
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
601 26
        \mysqli_ssl_set(
602 26
            $this->link,
603 26
            $this->_clientkey,
604 1
            $this->_clientcert,
605
            $this->_cacert,
606
            null,
607 26
            null
608 1
        );
609
        $flags = MYSQLI_CLIENT_SSL;
610
      }
611 26
612 22
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
613 22
      $this->connected = @\mysqli_real_connect(
614
          $this->link,
615 26
          $this->hostname,
616
          $this->username,
617 26
          $this->password,
618 22
          $this->database,
619 22
          $this->port,
620
          $this->socket,
621 26
          $flags
622
      );
623
    } catch (\Exception $e) {
624
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
625
      $this->_debug->displayError($error, false);
626
      throw new DBConnectException($error, 100, $e);
627
    }
628
    \mysqli_report(MYSQLI_REPORT_OFF);
629
630
    $errno = mysqli_connect_errno();
631
    if (!$this->connected || $errno) {
632
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
633
      $this->_debug->displayError($error, false);
634
      throw new DBConnectException($error, 101);
635
    }
636
637
    $this->set_charset($this->charset);
638
639
    return $this->isReady();
640
  }
641 33
642
  /**
643 33
   * Execute a "delete"-query.
644 2
   *
645
   * @param string       $table
646
   * @param string|array $where
647 33
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
648 2
   *
649
   * @return false|int <p>false on error</p>
650
   *
651
   *    * @throws QueryException
652 33
   */
653 33 View Code Duplication
  public function delete($table, $where, $databaseName = null)
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...
654 33
  {
655
    // init
656
    $table = trim($table);
657
658 33
    if ($table === '') {
659
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
660 33
661 33
      return false;
662
    }
663 33
664 33
    if (is_string($where)) {
665 33
      $WHERE = $this->escape($where, false);
666 33
    } elseif (is_array($where)) {
667
      $WHERE = $this->_parseArrayPair($where, 'AND');
668 33
    } else {
669 33
      $WHERE = '';
670
    }
671
672
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
673 24
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
674
    }
675
676 33
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
677
678
    return $this->query($sql);
679
  }
680 5
681
  /**
682
   * Ends a transaction and commits if no errors, then ends autocommit.
683 33
   *
684
   * @return bool <p>This will return true or false indicating success of transactions.</p>
685
   */
686 View Code Duplication
  public function endTransaction()
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...
687 3
  {
688 3
    if ($this->_in_transaction === false) {
689
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
690
      return false;
691 1
    }
692 1
693
    if (!$this->errors()) {
694 1
      $return = \mysqli_commit($this->link);
695 1
    } else {
696
      $this->rollback();
697
      $return = false;
698 1
    }
699 1
700
    \mysqli_autocommit($this->link, true);
701 1
    $this->_in_transaction = false;
702 1
703
    return $return;
704 1
  }
705
706
  /**
707 1
   * Get all errors from "$this->_errors".
708
   *
709
   * @return array|false <p>false === on errors</p>
710
   */
711 33
  public function errors()
712
  {
713
    $errors = $this->_debug->getErrors();
714 3
715 3
    return count($errors) > 0 ? $errors : false;
716 3
  }
717 3
718 33
  /**
719
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
720
   *
721
   * @param mixed     $var           boolean: convert into "integer"<br />
722 33
   *                                 int: int (don't change it)<br />
723
   *                                 float: float (don't change it)<br />
724 33
   *                                 null: null (don't change it)<br />
725 9
   *                                 array: run escape() for every key => value<br />
726 9
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
727
   * @param bool      $stripe_non_utf8
728 33
   * @param bool      $html_entity_decode
729
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
730 1
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
731 1
   *                                 <strong>null</strong> => Convert the array into null, every time.
732
   *
733 33
   * @return mixed
734
   */
735 33
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
736
  {
737 33
    if ($var === '') {
738
      return '';
739
    }
740
741 3
    if ($var === null) {
742
      return null;
743
    }
744
745
    // save the current value as int (for later usage)
746 3
    if (!is_object($var)) {
747
      $varInt = (int)$var;
748
    }
749
750
    /** @noinspection TypeUnsafeComparisonInspection */
751
    if (
752 2
        is_int($var)
753
        ||
754
        is_bool($var)
755
        ||
756
        (
757
            isset($varInt, $var[0])
758
            &&
759
            $var[0] != '0'
760
            &&
761 35
            "$varInt" == $var
0 ignored issues
show
Bug introduced by
The variable $varInt does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
762
        )
763 35
    ) {
764
765
      // "int" || int || bool
766
767
      return (int)$var;
768
    }
769
770
    if (is_float($var)) {
771 22
772
      // float
773 22
774
      return $var;
775
    }
776
777
    if (is_array($var)) {
778
779
      // array
780
781 8
      if ($convert_array === null) {
782
        return null;
783 8
      }
784
785
      $varCleaned = array();
786
      foreach ((array)$var as $key => $value) {
787
788
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
789
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
790
791
        /** @noinspection OffsetOperationsInspection */
792
        $varCleaned[$key] = $value;
793
      }
794
795
      if ($convert_array === true) {
796
        $varCleaned = implode(',', $varCleaned);
797
798 9
        return $varCleaned;
799
      }
800 9
801 1
      return (array)$varCleaned;
802
    }
803
804 1
    if (
805
        is_string($var)
806
        ||
807
        (
808
            is_object($var)
809 1
            &&
810
            method_exists($var, '__toString')
811
        )
812 1
    ) {
813 1
814
      // "string"
815
816 1
      $var = (string)$var;
817
818
      if ($stripe_non_utf8 === true) {
819 8
        $var = UTF8::cleanup($var);
820
      }
821
822 8
      if ($html_entity_decode === true) {
823
        // use no-html-entity for db
824 8
        $var = UTF8::html_entity_decode($var);
825
      }
826
827
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
828
829
      $var = \mysqli_real_escape_string($this->getLink(), $var);
830
831
      return (string)$var;
832
833
    }
834 3
835
    if ($var instanceof \DateTime) {
836 3
837
      // "DateTime"-object
838 3
839 2
      try {
840 2
        return $this->escape($var->format('Y-m-d H:i:s'), false);
841
      } catch (\Exception $e) {
842 3
        return null;
843 3
      }
844 3
845 3
    } else {
846
      return false;
847 3
    }
848
  }
849
850
  /**
851
   * Execute select/insert/update/delete sql-queries.
852
   *
853
   * @param string $query    sql-query
854
   * @param bool   $useCache use cache?
855
   * @param int    $cacheTTL cache-ttl in seconds
856 3
   * @param DB     $db
857
   *
858
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
859 3
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
860 3
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
861 3
   *               "true" by e.g. "DROP"-queries<br />
862 3
   *               "false" on error
863
   *
864
   * @throws QueryException
865 3
   */
866
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
867
  {
868
    // init
869
    $cacheKey = null;
870
    if (!$db) {
871
      $db = self::getInstance();
872
    }
873
874 View Code Duplication
    if ($useCache === true) {
875
      $cache = new Cache(null, null, false, $useCache);
876
      $cacheKey = 'sql-' . md5($query);
877
878
      if (
879
          $cache->getCacheIsReady() === true
880
          &&
881
          $cache->existsItem($cacheKey)
882
      ) {
883
        return $cache->getItem($cacheKey);
884
      }
885
886 3
    } else {
887
      $cache = false;
888
    }
889 3
890 3
    $result = $db->query($query);
891
892 3
    if ($result instanceof Result) {
893 1
894 1
      $return = $result->fetchAllArray();
895
896
      // save into the cache
897 1 View Code Duplication
      if (
898 1
          $cacheKey !== null
899 1
          &&
900 1
          $useCache === true
901 1
          &&
902
          $cache instanceof Cache
903
          &&
904 1
          $cache->getCacheIsReady() === true
905 3
      ) {
906
        $cache->setItem($cacheKey, $return, $cacheTTL);
907
      }
908 3
909
    } else {
910 3
      $return = $result;
911
    }
912 1
913
    return $return;
914
  }
915
916
  /**
917 1
   * Get all table-names via "SHOW TABLES".
918
   *
919 1
   * @return array
920
   */
921 1
  public function getAllTables()
922 1
  {
923 1
    $query = 'SHOW TABLES';
924 1
    $result = $this->query($query);
925 1
926
    return $result->fetchAllArray();
927 1
  }
928 2
929
  /**
930
   * @return Debug
931 3
   */
932
  public function getDebugger()
933
  {
934
    return $this->_debug;
935
  }
936
937
  /**
938
   * Get errors from "$this->_errors".
939 1
   *
940
   * @return array
941 1
   */
942
  public function getErrors()
943
  {
944
    return $this->_debug->getErrors();
945
  }
946
947
  /**
948
   * getInstance()
949
   *
950
   * @param string      $hostname
951
   * @param string      $username
952
   * @param string      $password
953
   * @param string      $database
954
   * @param int|string  $port          <p>default is (int)3306</p>
955
   * @param string      $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
956
   * @param bool|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
957
   * @param bool|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
958
   * @param string      $logger_class_name
959
   * @param string      $logger_level
960
   * @param array       $extra_config    <p>
961
   *                                     'session_to_db' => false|true<br>
962
   *                                     'socket' => 'string (path)'<br>
963
   *                                     'ssl' => 'bool'<br>
964
   *                                     'clientkey' => 'string (path)'<br>
965
   *                                     'clientcert' => 'string (path)'<br>
966
   *                                     'cacert' => 'string (path)'<br>
967
   *                                     </p>
968
   *
969
   * @return \voku\db\DB
970
   */
971
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $extra_config = array())
972
  {
973
    /**
974
     * @var $instance DB[]
975
     */
976
    static $instance = array();
977
978
    /**
979
     * @var $firstInstance DB
980
     */
981
    static $firstInstance = null;
982
983
    if (
984
        $hostname . $username . $password . $database . $port . $charset == ''
985
        &&
986
        null !== $firstInstance
987
    ) {
988
      return $firstInstance;
989
    }
990
991
    $extra_config_string = '';
992
    if (is_array($extra_config) === true) {
993
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
994
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
995
      }
996 7
    } else {
997
      // only for backward compatibility
998 7
      $extra_config_string = (int)$extra_config;
999 7
    }
1000 5
1001 5
    $connection = md5(
1002 7
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1003 5
    );
1004 5
1005
    if (!isset($instance[$connection])) {
1006 7
      $instance[$connection] = new self(
1007
          $hostname,
1008 7
          $username,
1009
          $password,
1010
          $database,
1011 7
          $port,
1012
          $charset,
1013
          $exit_on_error,
1014 7
          $echo_on_error,
1015
          $logger_class_name,
1016 7
          $logger_level,
1017
          $extra_config
1018
      );
1019
1020
      if (null === $firstInstance) {
1021
        $firstInstance = $instance[$connection];
1022
      }
1023
    }
1024
1025
    return $instance[$connection];
1026 1
  }
1027
1028 1
  /**
1029 1
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1030
   *
1031
   * @return \mysqli
1032
   */
1033
  public function getLink()
1034
  {
1035
    return $this->link;
1036 1
  }
1037
1038 1
  /**
1039 1
   * Get the current charset.
1040
   *
1041 1
   * @return string
1042
   */
1043
  public function get_charset()
1044
  {
1045
    return $this->charset;
1046
  }
1047
1048
  /**
1049
   * Check if we are in a transaction.
1050
   *
1051
   * @return bool
1052
   */
1053
  public function inTransaction()
1054
  {
1055
    return $this->_in_transaction;
1056 1
  }
1057
1058 1
  /**
1059
   * Execute a "insert"-query.
1060
   *
1061
   * @param string      $table
1062 1
   * @param array       $data
1063 1
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1064
   *
1065 1
   * @return false|int <p>false on error</p>
1066
   *
1067
   * @throws QueryException
1068 1
   */
1069 1
  public function insert($table, array $data = array(), $databaseName = null)
1070 1
  {
1071
    // init
1072 1
    $table = trim($table);
1073
1074 1
    if ($table === '') {
1075 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1076 1
1077
      return false;
1078 1
    }
1079
1080 1
    if (count($data) === 0) {
1081 1
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1082 1
1083 1
      return false;
1084 1
    }
1085
1086
    $SET = $this->_parseArrayPair($data);
1087 1
1088 1
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1089 1
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1090
    }
1091
1092
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1093 1
1094
    return $this->query($sql);
1095 1
  }
1096
1097
  /**
1098
   * Returns the auto generated id used in the last query.
1099
   *
1100
   * @return int|string
1101
   */
1102
  public function insert_id()
1103
  {
1104
    return \mysqli_insert_id($this->link);
1105
  }
1106
1107
  /**
1108 1
   * Check if db-connection is ready.
1109 1
   *
1110
   * @return boolean
1111
   */
1112 1
  public function isReady()
1113 1
  {
1114
    return $this->connected ? true : false;
1115
  }
1116
1117
  /**
1118
   * Get the last sql-error.
1119
   *
1120
   * @return string|false <p>false === there was no error</p>
1121
   */
1122 1
  public function lastError()
1123
  {
1124 1
    $errors = $this->_debug->getErrors();
1125 1
1126
    return count($errors) > 0 ? end($errors) : false;
1127
  }
1128
1129
  /**
1130
   * Execute a sql-multi-query.
1131
   *
1132 4
   * @param string $sql
1133
   *
1134 4
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1135
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1136 4
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1137 1
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1138
   *
1139 1
   * @throws QueryException
1140
   */
1141
  public function multi_query($sql)
1142 4
  {
1143
    if (!$this->isReady()) {
1144
      return false;
1145
    }
1146
1147 View Code Duplication
    if (!$sql || $sql === '') {
1148 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1149 4
      return false;
1150
    }
1151 4
1152
    $query_start_time = microtime(true);
1153
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1154
    $query_duration = microtime(true) - $query_start_time;
1155
    $this->_debug->logQuery($sql, $query_duration, 0);
1156
1157
    $returnTheResult = false;
1158
    $result = array();
1159 4
1160
    if ($resultTmp) {
1161 4
      do {
1162
1163
        $resultTmpInner = \mysqli_store_result($this->link);
1164
        if ($resultTmpInner instanceof \mysqli_result) {
1165
          $returnTheResult = true;
1166
          $result[] = new Result($sql, $resultTmpInner);
1167
        } else {
1168
          // is the query successful
1169 4
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1170
            $result[] = true;
1171 4
          } else {
1172
            $result[] = false;
1173
          }
1174
        }
1175
1176
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1177
    } else {
1178
      // log the error query
1179 2
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1180
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1181
    }
1182 2
1183 1
    // return the result only if there was a "SELECT"-query
1184 1
    if ($returnTheResult === true) {
1185 1
      return $result;
1186 1
    }
1187 1
1188
    if (
1189
        count($result) > 0
1190 2
        &&
1191 2
        in_array(false, $result, true) === false
1192
    ) {
1193 2
      return true;
1194
    }
1195
1196
    return false;
1197
  }
1198
1199
  /**
1200
   * Pings a server connection, or tries to reconnect
1201 2
   * if the connection has gone down.
1202
   *
1203 2
   * @return boolean
1204
   */
1205 2
  public function ping()
1206
  {
1207
    if (
1208
        $this->link
1209
        &&
1210
        $this->link instanceof \mysqli
1211 2
    ) {
1212
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1213
      /** @noinspection UsageOfSilenceOperatorInspection */
1214 2
      return (bool)@\mysqli_ping($this->link);
1215
    }
1216 2
1217 2
    return false;
1218 2
  }
1219 2
1220 2
  /**
1221
   * Selects a different database than the one specified on construction.
1222 2
   *
1223
   * @param string $database <p>Database name to switch to.</p>
1224
   *
1225
   * @return bool <p>Boolean true on success, false otherwise.</p>
1226
   */
1227
  public function select_db($database)
1228
  {
1229
    if (!$this->isReady()) {
1230
      return false;
1231
    }
1232
1233
    return mysqli_select_db($this->link, $database);
1234
  }
1235
1236 21
  /**
1237
   * Get a new "Prepare"-Object for your sql-query.
1238
   *
1239 21
   * @param string $query
1240
   *
1241 21
   * @return Prepare
1242 2
   */
1243
  public function prepare($query)
1244 2
  {
1245
    return new Prepare($this, $query);
1246
  }
1247 20
1248 3
  /**
1249
   * Execute a sql-query and return the result-array for select-statements.
1250 3
   *
1251
   * @param string $query
1252
   * @param DB     $db
1253 18
   *
1254
   * @return mixed
1255 18
   * @deprecated
1256
   * @throws \Exception
1257
   */
1258
  public static function qry($query, DB $db = null)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1259 18
  {
1260
    if (!$db) {
1261 18
      $db = self::getInstance();
1262
    }
1263
1264
    $args = func_get_args();
1265
    /** @noinspection SuspiciousAssignmentsInspection */
1266
    $query = array_shift($args);
1267
    $query = str_replace('?', '%s', $query);
1268
    $args = array_map(
1269
        array(
1270
            $db,
1271
            'escape',
1272
        ),
1273
        $args
1274 23
    );
1275
    array_unshift($args, $query);
1276
    $query = call_user_func_array('sprintf', $args);
1277 23
    $result = $db->query($query);
1278
1279
    if ($result instanceof Result) {
1280 23
      return $result->fetchAllArray();
1281
    }
1282
1283
    return $result;
1284 23
  }
1285 23
1286 23
  /**
1287 23
   * Execute a sql-query.
1288 23
   *
1289
   * @param string        $sql            <p>The sql query-string.</p>
1290 23
   *
1291 2
   * @param array|boolean $params         <p>
1292 2
   *                                      "array" of sql-query-parameters<br/>
1293
   *                                      "false" if you don't need any parameter (default)<br/>
1294 23
   *                                      </p>
1295 1
   *
1296 1
   * @return bool|int|Result              <p>
1297
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1298 23
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1299 1
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1300 1
   *                                      "true" by e.g. "DROP"-queries<br />
1301
   *                                      "false" on error
1302 23
   *                                      </p>
1303 1
   *
1304 1
   * @throws QueryException
1305
   */
1306 23
  public function query($sql = '', $params = false)
1307 1
  {
1308 1
    if (!$this->isReady()) {
1309
      return false;
1310 23
    }
1311 1
1312 1 View Code Duplication
    if (!$sql || $sql === '') {
1313
      $this->_debug->displayError('Can not execute an empty query.', false);
1314 23
      return false;
1315 1
    }
1316 1
1317
    if (
1318 23
        $params !== false
1319 2
        &&
1320 2
        is_array($params)
1321
        &&
1322 23
        count($params) > 0
1323 2
    ) {
1324 2
      $sql = $this->_parseQueryParams($sql, $params);
1325
    }
1326 23
1327 4
    $query_start_time = microtime(true);
1328 4
    $query_result = \mysqli_real_query($this->link, $sql);
1329
    $query_duration = microtime(true) - $query_start_time;
1330 23
1331 1
    $this->query_count++;
1332 1
1333
    $mysqli_field_count = \mysqli_field_count($this->link);
1334 23
    if ($mysqli_field_count) {
1335 4
      $result = \mysqli_store_result($this->link);
1336 4
    } else {
1337
      $result = $query_result;
1338 23
    }
1339 1
1340 1
    if ($result instanceof \mysqli_result) {
1341
      // log the select query
1342 23
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1343 1
      // return query result object
1344 1
      return new Result($sql, $result);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Result::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1345
    }
1346 23
1347 2
    if ($query_result === true) {
1348 2
1349
      // "INSERT" || "REPLACE"
1350 23 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1351 1
        $insert_id = (int)$this->insert_id();
1352 1
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1353
        return $insert_id;
1354 23
      }
1355 2
1356 2
      // "UPDATE" || "DELETE"
1357 2 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1358
        $affected_rows = (int)$this->affected_rows();
1359 2
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1360 1
        return $affected_rows;
1361 2
      }
1362 1
1363 1
      // log the ? query
1364
      $this->_debug->logQuery($sql, $query_duration, 0);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1365 2
      return true;
1366 23
    }
1367
1368
    // log the error query
1369 23
    $this->_debug->logQuery($sql, $query_duration, 0, true);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1370 23
1371 23
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1324 can also be of type array<integer,string>; however, voku\db\DB::queryErrorHandling() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1372
  }
1373 23
1374 23
  /**
1375 23
   * Error-handling for the sql-query.
1376 23
   *
1377
   * @param string     $errorMessage
1378 23
   * @param int        $errorNumber
1379 23
   * @param string     $sql
1380 23
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1381
   * @param bool       $sqlMultiQuery
1382 23
   *
1383 23
   * @throws QueryException
1384 23
   * @throws DBGoneAwayException
1385
   *
1386 23
   * @return bool
1387 23
   */
1388 23
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1389
  {
1390 23
    $errorNumber = (int)$errorNumber;
1391 23
    if (
1392
        $errorMessage === 'DB server has gone away'
1393 23
        ||
1394
        $errorMessage === 'MySQL server has gone away'
1395 23
        ||
1396
        $errorNumber === 2006
1397 23
    ) {
1398 1
      static $RECONNECT_COUNTER;
1399 23
1400 23
      // exit if we have more then 3 "DB server has gone away"-errors
1401 23
      if ($RECONNECT_COUNTER > 3) {
1402
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql, 5);
1403 23
        throw new DBGoneAwayException($errorMessage);
1404 1
      }
1405 1
1406
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1407 23
1408 23
      // reconnect
1409 23
      $RECONNECT_COUNTER++;
1410
      $this->reconnect(true);
1411 23
1412 2
      // re-run the current (non multi) query
1413 2
      if ($sqlMultiQuery === false) {
1414
        return $this->query($sql, $sqlParams);
1415 23
      }
1416 23
1417
      return false;
1418 23
    }
1419
1420
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1421
1422
    // this query returned an error, we must display it (only for dev) !!!
1423
    $this->_debug->displayError($errorMessage . '(' . $errorNumber. ') ' . ' | ' . $sql);
1424
1425
    return false;
1426
  }
1427
1428 26
  /**
1429
   * Quote && Escape e.g. a table name string.
1430 26
   *
1431 26
   * @param string $str
1432 26
   *
1433 26
   * @return string
1434 26
   */
1435
  public function quote_string($str)
1436 26
  {
1437 26
    $str = str_replace(
1438
        '`',
1439 26
        '``',
1440
        trim(
1441
            $this->escape($str, false),
1442
            '`'
1443
        )
1444
    );
1445
1446
    return '`' . $str . '`';
1447 1
  }
1448
1449 1
  /**
1450
   * Reconnect to the MySQL-Server.
1451
   *
1452
   * @param bool $checkViaPing
1453
   *
1454
   * @return bool
1455
   */
1456
  public function reconnect($checkViaPing = false)
1457
  {
1458
    $ping = false;
1459
1460
    if ($checkViaPing === true) {
1461
      $ping = $this->ping();
1462
    }
1463 1
1464
    if ($ping !== true) {
1465
      $this->connected = false;
1466 1
      $this->connect();
1467
    }
1468 1
1469 1
    return $this->isReady();
1470
  }
1471 1
1472
  /**
1473
   * Execute a "replace"-query.
1474 1
   *
1475 1
   * @param string      $table
1476
   * @param array       $data
1477 1
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1478
   *
1479
   * @return false|int <p>false on error</p>
1480
   *
1481 1
   * @throws QueryException
1482 1
   */
1483
  public function replace($table, array $data = array(), $databaseName = null)
1484 1
  {
1485 1
    // init
1486
    $table = trim($table);
1487 1
1488
    if ($table === '') {
1489
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1490 1
1491
      return false;
1492 1
    }
1493 1
1494 1
    if (count($data) === 0) {
1495
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1496 1
1497
      return false;
1498
    }
1499
1500 1
    // extracting column names
1501
    $columns = array_keys($data);
1502 1
    foreach ($columns as $k => $_key) {
1503
      /** @noinspection AlterInForeachInspection */
1504
      $columns[$k] = $this->quote_string($_key);
1505
    }
1506
1507
    $columns = implode(',', $columns);
1508
1509
    // extracting values
1510
    foreach ($data as $k => $_value) {
1511
      /** @noinspection AlterInForeachInspection */
1512
      $data[$k] = $this->secure($_value);
1513
    }
1514
    $values = implode(',', $data);
1515
1516
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1517 6
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1518
    }
1519
1520 6
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1521
1522 6
    return $this->query($sql);
1523 1
  }
1524
1525 1
  /**
1526
   * Rollback in a transaction and end the transaction.
1527
   *
1528 6
   * @return bool <p>Boolean true on success, false otherwise.</p>
1529 2
   */
1530 View Code Duplication
  public function rollback()
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...
1531 2
  {
1532
    if ($this->_in_transaction === false) {
1533
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1534 6
      return false;
1535
    }
1536 6
1537 2
    $return = \mysqli_rollback($this->link);
1538 6
    \mysqli_autocommit($this->link, true);
1539 4
    $this->_in_transaction = false;
1540 4
1541 1
    return $return;
1542
  }
1543
1544 6
1545
  /**
1546
   * Commits the current transaction and end the transaction.
1547
   *
1548 6
   * @return bool <p>Boolean true on success, false otherwise.</p>
1549
   */
1550 6 View Code Duplication
  public function commit()
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...
1551
  {
1552
    if ($this->_in_transaction === false) {
1553
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1554
      return false;
1555
    }
1556
1557
    $return = mysqli_commit($this->link);
1558
    \mysqli_autocommit($this->link, true);
1559
    $this->_in_transaction = false;
1560
1561
    return $return;
1562
  }
1563
1564 2
  /**
1565
   * Execute a callback inside a transaction.
1566
   *
1567 2
   * @param callback $callback The callback to run inside the transaction
1568
   *
1569 2
   * @return bool Boolean true on success, false otherwise
1570 1
   */
1571
  public function transact($callback)
1572 1
  {
1573
    try {
1574
      $this->beginTransaction();
1575 2
      call_user_func($callback, $this);
1576 1
      return $this->commit();
1577 2
    } catch (\Exception $e) {
1578 2
      $this->rollback();
1579 2
      return false;
1580 1
    }
1581
  }
1582
1583 2
  /**
1584
   * Try to secure a variable, so can you use it in sql-queries.
1585
   *
1586
   * <p>
1587 2
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1588
   * 1. parse into "int"
1589 2
   * </p><br />
1590
   *
1591
   * <p>
1592
   * <strong>float:</strong><br />
1593
   * 1. return "float"
1594
   * </p><br />
1595
   *
1596
   * <p>
1597
   * <strong>string:</strong><br />
1598
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1599
   * 2. trim whitespace<br />
1600
   * 3. trim '<br />
1601
   * 4. escape the string (and remove non utf-8 chars)<br />
1602
   * 5. trim ' again (because we maybe removed some chars)<br />
1603 20
   * 6. add ' around the new string<br />
1604
   * </p><br />
1605
   *
1606 20
   * <p>
1607
   * <strong>array:</strong><br />
1608 20
   * 1. return null
1609 1
   * </p><br />
1610
   *
1611 1
   * <p>
1612
   * <strong>object:</strong><br />
1613
   * 1. return false
1614 20
   * </p><br />
1615 5
   *
1616 20
   * <p>
1617 16
   * <strong>null:</strong><br />
1618 16
   * 1. return null
1619 1
   * </p>
1620
   *
1621
   * @param mixed $var
1622 20
   *
1623
   * @return mixed
1624
   */
1625
  public function secure($var)
1626 20
  {
1627
    if (
1628 20
        $var === ''
1629
        ||
1630
        ($this->_convert_null_to_empty_string === true && $var === null)
1631
    ) {
1632
      return "''";
1633
    }
1634
1635
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1636 1
      return $var;
1637
    }
1638 1
1639
    if (is_string($var)) {
1640 1
      $var = trim(trim($var), "'");
1641
    }
1642
1643
    $var = $this->escape($var, false, false, null);
1644
1645
    if (is_string($var)) {
1646 9
      $var = "'" . trim($var, "'") . "'";
1647
    }
1648 9
1649
    return $var;
1650
  }
1651
1652
  /**
1653
   * Execute a "select"-query.
1654
   *
1655
   * @param string       $table
1656
   * @param string|array $where
1657
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1658
   *
1659
   * @return false|Result <p>false on error</p>
1660
   *
1661
   * @throws QueryException
1662
   */
1663 View Code Duplication
  public function select($table, $where = '1=1', $databaseName = null)
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...
1664
  {
1665
    // init
1666 2
    $table = trim($table);
1667
1668 2
    if ($table === '') {
1669
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1670 2
1671 2
      return false;
1672 2
    }
1673 2
1674
    if (is_string($where)) {
1675
      $WHERE = $this->escape($where, false);
1676
    } elseif (is_array($where)) {
1677
      $WHERE = $this->_parseArrayPair($where, 'AND');
1678
    } else {
1679
      $WHERE = '';
1680
    }
1681
1682
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1683
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1684
    }
1685
1686
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1687
1688
    return $this->query($sql);
1689 2
  }
1690
1691 2
  /**
1692 2
   * Set the current charset.
1693
   *
1694
   * @param string $charset
1695
   *
1696
   * @return bool
1697
   */
1698
  public function set_charset($charset)
1699
  {
1700
    $charsetLower = strtolower($charset);
1701
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1702
      $charset = 'utf8';
1703
    }
1704
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1705
      $charset = 'utf8mb4';
1706
    }
1707
1708
    $this->charset = (string)$charset;
1709
1710
    $return = mysqli_set_charset($this->link, $charset);
1711
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1712
    /** @noinspection UsageOfSilenceOperatorInspection */
1713
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1714
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1715
    /** @noinspection UsageOfSilenceOperatorInspection */
1716
    @\mysqli_query($this->link, "SET NAMES '" . $charset . "'");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1717
1718
    return $return;
1719
  }
1720
1721
  /**
1722
   * Set the option to convert null to "''" (empty string).
1723
   *
1724
   * Used in secure() => select(), insert(), update(), delete()
1725
   *
1726
   * @param $bool
1727
   */
1728
  public function set_convert_null_to_empty_string($bool)
1729
  {
1730
    $this->_convert_null_to_empty_string = (bool)$bool;
1731
  }
1732
1733
  /**
1734
   * Enables or disables internal report functions
1735
   *
1736
   * @link http://php.net/manual/en/function.mysqli-report.php
1737
   *
1738
   * @param int $flags <p>
1739
   *                   <table>
1740
   *                   Supported flags
1741
   *                   <tr valign="top">
1742
   *                   <td>Name</td>
1743
   *                   <td>Description</td>
1744
   *                   </tr>
1745
   *                   <tr valign="top">
1746
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1747
   *                   <td>Turns reporting off</td>
1748
   *                   </tr>
1749
   *                   <tr valign="top">
1750
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1751
   *                   <td>Report errors from mysqli function calls</td>
1752
   *                   </tr>
1753
   *                   <tr valign="top">
1754
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1755
   *                   <td>
1756
   *                   Throw <b>mysqli_sql_exception</b> for errors
1757
   *                   instead of warnings
1758
   *                   </td>
1759
   *                   </tr>
1760
   *                   <tr valign="top">
1761
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1762
   *                   <td>Report if no index or bad index was used in a query</td>
1763
   *                   </tr>
1764
   *                   <tr valign="top">
1765
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1766
   *                   <td>Set all options (report all)</td>
1767
   *                   </tr>
1768
   *                   </table>
1769
   *                   </p>
1770
   *
1771
   * @return bool
1772
   */
1773
  public function set_mysqli_report($flags)
1774
  {
1775
    return \mysqli_report($flags);
1776
  }
1777
1778
  /**
1779
   * Show config errors by throw exceptions.
1780
   *
1781
   * @return bool
1782
   *
1783
   * @throws \InvalidArgumentException
1784
   */
1785
  public function showConfigError()
1786
  {
1787
1788
    if (
1789
        !$this->hostname
1790
        ||
1791
        !$this->username
1792
        ||
1793
        !$this->database
1794
    ) {
1795
1796
      if (!$this->hostname) {
1797
        throw new \InvalidArgumentException('no-sql-hostname');
1798
      }
1799
1800
      if (!$this->username) {
1801
        throw new \InvalidArgumentException('no-sql-username');
1802
      }
1803
1804
      if (!$this->database) {
1805
        throw new \InvalidArgumentException('no-sql-database');
1806
      }
1807
1808
      return false;
1809
    }
1810
1811
    return true;
1812
  }
1813
1814
  /**
1815
   * alias: "beginTransaction()"
1816
   */
1817
  public function startTransaction()
1818
  {
1819
    $this->beginTransaction();
1820
  }
1821
1822
  /**
1823
   * Execute a "update"-query.
1824
   *
1825
   * @param string       $table
1826
   * @param array        $data
1827
   * @param array|string $where
1828
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1829
   *
1830
   * @return false|int <p>false on error</p>
1831
   *
1832
   * @throws QueryException
1833
   */
1834
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1835
  {
1836
    // init
1837
    $table = trim($table);
1838
1839
    if ($table === '') {
1840
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1841
1842
      return false;
1843
    }
1844
1845
    if (count($data) === 0) {
1846
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1847
1848
      return false;
1849
    }
1850
1851
    $SET = $this->_parseArrayPair($data);
1852
1853
    if (is_string($where)) {
1854
      $WHERE = $this->escape($where, false);
1855
    } elseif (is_array($where)) {
1856
      $WHERE = $this->_parseArrayPair($where, 'AND');
1857
    } else {
1858
      $WHERE = '';
1859
    }
1860
1861
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1862
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1863
    }
1864
1865
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1866
1867
    return $this->query($sql);
1868
  }
1869
1870
}
1871