Completed
Push — master ( c3ae3f...099e03 )
by Lars
01:45
created

DB::update()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 0
cts 0
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 19
nc 8
nop 4
crap 42
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
  /**
95
   * The path name to the key file
96
   *
97
   * @var string
98
   */
99
  private $_clientkey;
100
101
  /**
102
   * The path name to the certificate file
103
   *
104
   * @var string
105
   */
106
  private $_clientcert;
107
108
  /**
109 10
   * The path name to the certificate authority file
110
   *
111 10
   * @var string
112
   */
113 10
  private $_cacert;
114
115 10
  /**
116 10
   * @var Debug
117 10
   */
118 10
  private $_debug;
119 10
120 10
  /**
121 10
   * __construct()
122 10
   *
123 10
   * @param string         $hostname
124 10
   * @param string         $username
125 10
   * @param string         $password
126
   * @param string         $database
127 10
   * @param int            $port
128
   * @param string         $charset
129 7
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
130
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
131 4
   * @param string         $logger_class_name
132
   * @param string         $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
133 4
   * @param array          $extra_config <p>
134
   *                                     'session_to_db' => false|true<br>
135 4
   *                                     'socket' => 'string (path)'<br>
136
   *                                     'ssl' => 'bool'<br>
137 4
   *                                     'clientkey' => 'string (path)'<br>
138
   *                                     'clientcert' => 'string (path)'<br>
139 4
   *                                     'cacert' => 'string (path)'<br>
140
   *                                     </p>
141 4
   */
142
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config = array())
143 4
  {
144
    $this->connected = false;
145 4
146
    $this->_debug = new Debug($this);
147 4
148
    $this->_loadConfig(
149 4
        $hostname,
150
        $username,
151 4
        $password,
152
        $database,
153 4
        $port,
154
        $charset,
155 4
        $exit_on_error,
156
        $echo_on_error,
157 4
        $logger_class_name,
158
        $logger_level,
159 4
        $extra_config
160
    );
161
162
    $this->connect();
163
164
    $this->mysqlDefaultTimeFunctions = array(
165
      // Returns the current date.
166
      'CURDATE()',
167
      // CURRENT_DATE	| Synonyms for CURDATE()
168
      'CURRENT_DATE()',
169
      // CURRENT_TIME	| Synonyms for CURTIME()
170
      'CURRENT_TIME()',
171
      // CURRENT_TIMESTAMP | Synonyms for NOW()
172
      'CURRENT_TIMESTAMP()',
173
      // Returns the current time.
174
      'CURTIME()',
175
      // Synonym for NOW()
176
      'LOCALTIME()',
177
      // Synonym for NOW()
178 10
      'LOCALTIMESTAMP()',
179
      // Returns the current date and time.
180 10
      'NOW()',
181 10
      // Returns the time at which the function executes.
182 10
      'SYSDATE()',
183 10
      // Returns a UNIX timestamp.
184
      'UNIX_TIMESTAMP()',
185 10
      // Returns the current UTC date.
186 4
      'UTC_DATE()',
187 4
      // Returns the current UTC time.
188
      'UTC_TIME()',
189 10
      // Returns the current UTC date and time.
190 4
      'UTC_TIMESTAMP()',
191 4
    );
192
  }
193
194 7
  /**
195
   * Prevent the instance from being cloned.
196
   *
197
   * @return void
198 10
   */
199
  private function __clone()
200
  {
201
  }
202 10
203
  /**
204 10
   * __destruct
205 10
   *
206
   */
207 10
  public function __destruct()
208 10
  {
209 10
    // close the connection only if we don't save PHP-SESSION's in DB
210
    if ($this->session_to_db === false) {
211 10
      $this->close();
212 10
    }
213 10
  }
214
215 10
  /**
216 10
   * __wakeup
217
   *
218 10
   * @return void
219
   */
220 10
  public function __wakeup()
221
  {
222
    $this->reconnect();
223
  }
224
225
  /**
226
   * Load the config from the constructor.
227
   *
228
   * @param string         $hostname
229
   * @param string         $username
230 10
   * @param string         $password
231
   * @param string         $database
232
   * @param int|string     $port          <p>default is (int)3306</p>
233
   * @param string         $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
234 10
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
235 10
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
236 9
   * @param string         $logger_class_name
237 9
   * @param string         $logger_level
238 8
   * @param array          $extra_config <p>
239 10
   *                                     'session_to_db' => false|true<br>
240
   *                                     'socket' => 'string (path)'<br>
241 3
   *                                     'ssl' => 'bool'<br>
242 1
   *                                     'clientkey' => 'string (path)'<br>
243
   *                                     'clientcert' => 'string (path)'<br>
244
   *                                     'cacert' => 'string (path)'<br>
245 2
   *                                     </p>
246 1
   *
247
   * @return bool
248
   */
249 1
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
250 1
  {
251
    $this->hostname = (string)$hostname;
252
    $this->username = (string)$username;
253
    $this->password = (string)$password;
254
    $this->database = (string)$database;
255
256 7
    if ($charset) {
257
      $this->charset = (string)$charset;
258
    }
259
260
    if ($port) {
261
      $this->port = (int)$port;
262
    } else {
263
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
264
      /** @noinspection UsageOfSilenceOperatorInspection */
265
      $this->port = (int)@ini_get('mysqli.default_port');
266 9
    }
267
268 9
    // fallback
269 1
    if (!$this->port) {
270
      $this->port = 3306;
271
    }
272 9
273
    if (!$this->socket) {
274 9
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
275
      $this->socket = @ini_get('mysqli.default_socket');
276 9
    }
277 9
278 9
    if ($exit_on_error === true || $exit_on_error === false) {
279
      $this->_debug->setExitOnError($exit_on_error);
280
    }
281 9
282 9
    if ($echo_on_error === true || $echo_on_error === false) {
283 9
      $this->_debug->setEchoOnError($echo_on_error);
284 9
    }
285 9
286 9
    $this->_debug->setLoggerClassName($logger_class_name);
287 9
    $this->_debug->setLoggerLevel($logger_level);
288 9
289 9
    if (is_array($extra_config) === true) {
290 9
291 3
      if (isset($extra_config['session_to_db'])) {
292 3
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
293 3
      }
294
295 6
      if (isset($extra_config['socket'])) {
296
        $this->socket = $extra_config['socket'];
297 6
      }
298
299
      if (isset($extra_config['ssl'])) {
300
        $this->_ssl = $extra_config['ssl'];
301
      }
302
303
      if (isset($extra_config['clientkey'])) {
304 6
        $this->_clientkey = $extra_config['clientkey'];
305
      }
306 6
307
      if (isset($extra_config['clientcert'])) {
308
        $this->_clientcert = $extra_config['clientcert'];
309
      }
310
311
      if (isset($extra_config['cacert'])) {
312
        $this->_cacert = $extra_config['cacert'];
313
      }
314 45
315
    } else {
316 45
      // only for backward compatibility
317
      $this->session_to_db = (boolean)$extra_config;
318
    }
319
320
    return $this->showConfigError();
321
  }
322
323
  /**
324
   * Parses arrays with value pairs and generates SQL to use in queries.
325
   *
326 2
   * @param array  $arrayPair
327
   * @param string $glue <p>This is the separator.</p>
328 2
   *
329
   * @return string
330
   *
331
   * @internal
332
   */
333
  public function _parseArrayPair($arrayPair, $glue = ',')
334
  {
335
    // init
336
    $sql = '';
337
338
    /** @noinspection IsEmptyFunctionUsageInspection */
339
    if (empty($arrayPair)) {
340
      return '';
341
    }
342
343
    $arrayPairCounter = 0;
344
    foreach ($arrayPair as $_key => $_value) {
345
      $_connector = '=';
346
      $_glueHelper = '';
347
      $_key_upper = strtoupper($_key);
348
349
      if (strpos($_key_upper, ' NOT') !== false) {
350
        $_connector = 'NOT';
351
      }
352
353
      if (strpos($_key_upper, ' IS') !== false) {
354
        $_connector = 'IS';
355
      }
356
357
      if (strpos($_key_upper, ' IS NOT') !== false) {
358
        $_connector = 'IS NOT';
359
      }
360
361
      if (strpos($_key_upper, ' IN') !== false) {
362
        $_connector = 'IN';
363
      }
364
365
      if (strpos($_key_upper, ' NOT IN') !== false) {
366
        $_connector = 'NOT IN';
367
      }
368
369
      if (strpos($_key_upper, ' BETWEEN') !== false) {
370
        $_connector = 'BETWEEN';
371
      }
372
373
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
374
        $_connector = 'NOT BETWEEN';
375
      }
376
377
      if (strpos($_key_upper, ' LIKE') !== false) {
378
        $_connector = 'LIKE';
379
      }
380
381
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
382
        $_connector = 'NOT LIKE';
383 57
      }
384
385 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
386
        $_connector = '>';
387
      }
388 57
389 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
390
        $_connector = '<';
391
      }
392
393 57
      if (strpos($_key_upper, ' >=') !== false) {
394
        $_connector = '>=';
395
      }
396 57
397 57
      if (strpos($_key_upper, ' <=') !== false) {
398 11
        $_connector = '<=';
399 57
      }
400 11
401
      if (strpos($_key_upper, ' <>') !== false) {
402
        $_connector = '<>';
403 57
      }
404 57
405 57
      if (strpos($_key_upper, ' OR') !== false) {
406
        $_glueHelper = 'OR';
407 57
      }
408 10
409 10
      if (strpos($_key_upper, ' AND') !== false) {
410 10
        $_glueHelper = 'AND';
411 10
      }
412 10
413 10
      if (is_array($_value) === true) {
414 10
        foreach ($_value as $oldKey => $oldValue) {
415 10
          $_value[$oldKey] = $this->secure($oldValue);
416 10
        }
417 10
418 10
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
419
          $_value = '(' . implode(',', $_value) . ')';
420 10
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
421
          $_value = '(' . implode(' AND ', $_value) . ')';
422 4
        }
423 1
424 1
      } else {
425 4
        $_value = $this->secure($_value);
426
      }
427 57
428
      $quoteString = $this->quote_string(
429
          trim(
430
              str_ireplace(
431
                  array(
432
                      $_connector,
433
                      $_glueHelper,
434
                  ),
435
                  '',
436
                  $_key
437
              )
438
          )
439
      );
440
441
      if (!is_array($_value)) {
442
        $_value = array($_value);
443
      }
444
445
      if (!$_glueHelper) {
446
        $_glueHelper = $glue;
447
      }
448
449
      $tmpCounter = 0;
450 35
      foreach ($_value as $valueInner) {
451
452 35
        $_glueHelperInner = $_glueHelper;
453
454
        if ($arrayPairCounter === 0) {
455
456 35
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
457 4
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
458
          } elseif ($tmpCounter === 0) {
459 4
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
460
          }
461
462
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
463
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
464 33
        }
465 3
466 33
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
467 3
        $tmpCounter++;
468 33
      }
469 3
470 3
      if ($_glueHelper === 'OR') {
471
        $sql .= ' ) ';
472 33
      }
473 33
474 33
      $arrayPairCounter++;
475
    }
476 33
477
    return $sql;
478 33
  }
479 33
480 28
  /**
481 28
   * _parseQueryParams
482 24
   *
483
   * @param string $sql
484
   * @param array  $params
485 33
   *
486
   * @return string
487
   */
488 27
  private function _parseQueryParams($sql, array $params)
489
  {
490
    // is there anything to parse?
491 27
    if (strpos($sql, '?') === false) {
492
      return $sql;
493
    }
494
495 25
    if (count($params) > 0) {
496
      $parseKey = md5(uniqid((string)mt_rand(), true));
497
      $sql = str_replace('?', $parseKey, $sql);
498 23
499 22
      $k = 0;
500 22
      while (strpos($sql, $parseKey) !== false) {
501
        $value = $this->secure($params[$k]);
502 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 502 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...
503
        $k++;
504
      }
505
    }
506 8
507 8
    return $sql;
508 8
  }
509
510 8
  /**
511
   * Gets the number of affected rows in a previous MySQL operation.
512
   *
513
   * @return int
514
   */
515
  public function affected_rows()
516
  {
517
    return \mysqli_affected_rows($this->link);
518
  }
519
520 8
  /**
521
   * Begins a transaction, by turning off auto commit.
522 8
   *
523
   * @return bool <p>This will return true or false indicating success of transaction</p>
524
   */
525
  public function beginTransaction()
526
  {
527
    if ($this->_in_transaction === true) {
528
      $this->_debug->displayError('Error mysql server already in transaction!', false);
529
530
      return false;
531
    }
532
533 3
    $this->clearErrors(); // needed for "$this->endTransaction()"
534
535
    $this->_in_transaction = true;
536 3
    $return = \mysqli_autocommit($this->link, false);
537
    if ($return === false) {
538
      $this->_in_transaction = false;
539
    }
540 3
541 3
    return $return;
542 3
  }
543
544 3
  /**
545 3
   * Clear the errors in "_debug->_errors".
546 3
   *
547 3
   * @return bool
548 3
   */
549 3
  public function clearErrors()
550 3
  {
551
    return $this->_debug->clearErrors();
552 3
  }
553
554
  /**
555
   * Closes a previously opened database connection.
556
   *
557
   * @return bool
558
   */
559
  public function close()
560
  {
561
    $this->connected = false;
562
563
    if (!$this->link) {
564
      return false;
565
    }
566
567
    if (\mysqli_close($this->link)) {
568
      $this->link = null;
569
      return true;
570
    }
571
572
    return false;
573
  }
574
575
  /**
576
   * Open a new connection to the MySQL server.
577
   *
578
   * @return bool
579
   *
580
   * @throws DBConnectException
581
   */
582
  public function connect()
583
  {
584
    if ($this->isReady()) {
585
      return true;
586
    }
587
588
    $flags = null;
589
590
    \mysqli_report(MYSQLI_REPORT_STRICT);
591
    try {
592
593
      $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...
594
595
      if (Helper::isMysqlndIsUsed() === true) {
596
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
597 26
      }
598
599
      if ($this->_ssl === true) {
600
601 26
        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...
602 26
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
603 26
        }
604 1
605
        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...
606
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
607 26
        }
608 1
609
        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...
610
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
611 26
        }
612 22
613 22
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
614
615 26
        \mysqli_ssl_set(
616
            $this->link,
617 26
            $this->_clientkey,
618 22
            $this->_clientcert,
619 22
            $this->_cacert,
620
            null,
621 26
            null
622
        );
623
624
        $flags = MYSQLI_CLIENT_SSL;
625
      }
626
627
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
628
      $this->connected = @\mysqli_real_connect(
629
          $this->link,
630
          $this->hostname,
631
          $this->username,
632
          $this->password,
633
          $this->database,
634
          $this->port,
635
          $this->socket,
636
          $flags
637
      );
638
639
    } catch (\Exception $e) {
640
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
641 33
      $this->_debug->displayError($error, false);
642
      throw new DBConnectException($error, 100, $e);
643 33
    }
644 2
    \mysqli_report(MYSQLI_REPORT_OFF);
645
646
    $errno = mysqli_connect_errno();
647 33
    if (!$this->connected || $errno) {
648 2
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
649
      $this->_debug->displayError($error, false);
650
      throw new DBConnectException($error, 101);
651
    }
652 33
653 33
    $this->set_charset($this->charset);
654 33
655
    return $this->isReady();
656
  }
657
658 33
  /**
659
   * Execute a "delete"-query.
660 33
   *
661 33
   * @param string       $table
662
   * @param string|array $where
663 33
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
664 33
   *
665 33
   * @return false|int <p>false on error</p>
666 33
   *
667
   *    * @throws QueryException
668 33
   */
669 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...
670
  {
671
    // init
672
    $table = trim($table);
673 24
674
    if ($table === '') {
675
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
676 33
677
      return false;
678
    }
679
680 5
    if (is_string($where)) {
681
      $WHERE = $this->escape($where, false);
682
    } elseif (is_array($where)) {
683 33
      $WHERE = $this->_parseArrayPair($where, 'AND');
684
    } else {
685
      $WHERE = '';
686
    }
687 3
688 3
    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...
689
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
690
    }
691 1
692 1
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
693
694 1
    return $this->query($sql);
695 1
  }
696
697
  /**
698 1
   * Ends a transaction and commits if no errors, then ends autocommit.
699 1
   *
700
   * @return bool <p>This will return true or false indicating success of transactions.</p>
701 1
   */
702 1
  public function endTransaction()
703
  {
704 1
    if (!$this->errors()) {
705
      $return = \mysqli_commit($this->link);
706
    } else {
707 1
      $this->rollback();
708
      $return = false;
709
    }
710
711 33
    \mysqli_autocommit($this->link, true);
712
    $this->_in_transaction = false;
713
714 3
    return $return;
715 3
  }
716 3
717 3
  /**
718 33
   * Get all errors from "$this->_errors".
719
   *
720
   * @return array|false <p>false === on errors</p>
721
   */
722 33
  public function errors()
723
  {
724 33
    $errors = $this->_debug->getErrors();
725 9
726 9
    return count($errors) > 0 ? $errors : false;
727
  }
728 33
729
  /**
730 1
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
731 1
   *
732
   * @param mixed     $var           boolean: convert into "integer"<br />
733 33
   *                                 int: int (don't change it)<br />
734
   *                                 float: float (don't change it)<br />
735 33
   *                                 null: null (don't change it)<br />
736
   *                                 array: run escape() for every key => value<br />
737 33
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
738
   * @param bool      $stripe_non_utf8
739
   * @param bool      $html_entity_decode
740
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
741 3
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
742
   *                                 <strong>null</strong> => Convert the array into null, every time.
743
   *
744
   * @return mixed
745
   */
746 3
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
747
  {
748
    if ($var === '') {
749
      return '';
750
    }
751
752 2
    if ($var === null) {
753
      return null;
754
    }
755
756
    // save the current value as int (for later usage)
757
    if (!is_object($var)) {
758
      $varInt = (int)$var;
759
    }
760
761 35
    /** @noinspection TypeUnsafeComparisonInspection */
762
    if (
763 35
        is_int($var)
764
        ||
765
        is_bool($var)
766
        ||
767
        (
768
            isset($varInt, $var[0])
769
            &&
770
            $var[0] != '0'
771 22
            &&
772
            "$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...
773 22
        )
774
    ) {
775
776
      // "int" || int || bool
777
778
      return (int)$var;
779
    }
780
781 8
    if (is_float($var)) {
782
783 8
      // float
784
785
      return $var;
786
    }
787
788
    if (is_array($var)) {
789
790
      // array
791
792
      if ($convert_array === null) {
793
        return null;
794
      }
795
796
      $varCleaned = array();
797
      foreach ((array)$var as $key => $value) {
798 9
799
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
800 9
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
801 1
802
        /** @noinspection OffsetOperationsInspection */
803
        $varCleaned[$key] = $value;
804 1
      }
805
806
      if ($convert_array === true) {
807
        $varCleaned = implode(',', $varCleaned);
808
809 1
        return $varCleaned;
810
      }
811
812 1
      return (array)$varCleaned;
813 1
    }
814
815
    if (
816 1
        is_string($var)
817
        ||
818
        (
819 8
            is_object($var)
820
            &&
821
            method_exists($var, '__toString')
822 8
        )
823
    ) {
824 8
825
      // "string"
826
827
      $var = (string)$var;
828
829
      if ($stripe_non_utf8 === true) {
830
        $var = UTF8::cleanup($var);
831
      }
832
833
      if ($html_entity_decode === true) {
834 3
        // use no-html-entity for db
835
        $var = UTF8::html_entity_decode($var);
836 3
      }
837
838 3
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
839 2
840 2
      $var = \mysqli_real_escape_string($this->getLink(), $var);
841
842 3
      return (string)$var;
843 3
844 3
    }
845 3
846
    if ($var instanceof \DateTime) {
847 3
848
      // "DateTime"-object
849
850
      try {
851
        return $this->escape($var->format('Y-m-d H:i:s'), false);
852
      } catch (\Exception $e) {
853
        return null;
854
      }
855
856 3
    } else {
857
      return false;
858
    }
859 3
  }
860 3
861 3
  /**
862 3
   * Execute select/insert/update/delete sql-queries.
863
   *
864
   * @param string $query    sql-query
865 3
   * @param bool   $useCache use cache?
866
   * @param int    $cacheTTL cache-ttl in seconds
867
   *
868
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
869
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
870
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
871
   *               "true" by e.g. "DROP"-queries<br />
872
   *               "false" on error
873
   *
874
   * @throws QueryException
875
   */
876
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600)
877
  {
878
    // init
879
    $cacheKey = null;
880
    $db = self::getInstance();
881
882 View Code Duplication
    if ($useCache === true) {
883
      $cache = new Cache(null, null, false, $useCache);
884
      $cacheKey = 'sql-' . md5($query);
885
886 3
      if (
887
          $cache->getCacheIsReady() === true
888
          &&
889 3
          $cache->existsItem($cacheKey)
890 3
      ) {
891
        return $cache->getItem($cacheKey);
892 3
      }
893 1
894 1
    } else {
895
      $cache = false;
896
    }
897 1
898 1
    $result = $db->query($query);
899 1
900 1
    if ($result instanceof Result) {
901 1
902
      $return = $result->fetchAllArray();
903
904 1
      // save into the cache
905 3 View Code Duplication
      if (
906
          $cacheKey !== null
907
          &&
908 3
          $useCache === true
909
          &&
910 3
          $cache instanceof Cache
911
          &&
912 1
          $cache->getCacheIsReady() === true
913
      ) {
914
        $cache->setItem($cacheKey, $return, $cacheTTL);
915
      }
916
917 1
    } else {
918
      $return = $result;
919 1
    }
920
921 1
    return $return;
922 1
  }
923 1
924 1
  /**
925 1
   * Get all table-names via "SHOW TABLES".
926
   *
927 1
   * @return array
928 2
   */
929
  public function getAllTables()
930
  {
931 3
    $query = 'SHOW TABLES';
932
    $result = $this->query($query);
933
934
    return $result->fetchAllArray();
935
  }
936
937
  /**
938
   * @return Debug
939 1
   */
940
  public function getDebugger()
941 1
  {
942
    return $this->_debug;
943
  }
944
945
  /**
946
   * Get errors from "$this->_errors".
947
   *
948
   * @return array
949
   */
950
  public function getErrors()
951
  {
952
    return $this->_debug->getErrors();
953
  }
954
955
  /**
956
   * getInstance()
957
   *
958
   * @param string      $hostname
959
   * @param string      $username
960
   * @param string      $password
961
   * @param string      $database
962
   * @param int|string  $port          <p>default is (int)3306</p>
963
   * @param string      $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
964
   * @param bool|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
965
   * @param bool|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
966
   * @param string      $logger_class_name
967
   * @param string      $logger_level
968
   * @param array       $extra_config    <p>
969
   *                                     'session_to_db' => false|true<br>
970
   *                                     'socket' => 'string (path)'<br>
971
   *                                     'ssl' => 'bool'<br>
972
   *                                     'clientkey' => 'string (path)'<br>
973
   *                                     'clientcert' => 'string (path)'<br>
974
   *                                     'cacert' => 'string (path)'<br>
975
   *                                     </p>
976
   *
977
   * @return \voku\db\DB
978
   */
979
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $extra_config = array())
980
  {
981
    /**
982
     * @var $instance DB[]
983
     */
984
    static $instance = array();
985
986
    /**
987
     * @var $firstInstance DB
988
     */
989
    static $firstInstance = null;
990
991
    if (
992
        $hostname . $username . $password . $database . $port . $charset == ''
993
        &&
994
        null !== $firstInstance
995
    ) {
996 7
      return $firstInstance;
997
    }
998 7
999 7
    $extra_config_string = '';
1000 5
    if (is_array($extra_config) === true) {
1001 5
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1002 7
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1003 5
      }
1004 5
    } else {
1005
      // only for backward compatibility
1006 7
      $extra_config_string = (int)$extra_config;
1007
    }
1008 7
1009
    $connection = md5(
1010
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1011 7
    );
1012
1013
    if (!isset($instance[$connection])) {
1014 7
      $instance[$connection] = new self(
1015
          $hostname,
1016 7
          $username,
1017
          $password,
1018
          $database,
1019
          $port,
1020
          $charset,
1021
          $exit_on_error,
1022
          $echo_on_error,
1023
          $logger_class_name,
1024
          $logger_level,
1025
          $extra_config
1026 1
      );
1027
1028 1
      if (null === $firstInstance) {
1029 1
        $firstInstance = $instance[$connection];
1030
      }
1031
    }
1032
1033
    return $instance[$connection];
1034
  }
1035
1036 1
  /**
1037
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1038 1
   *
1039 1
   * @return \mysqli
1040
   */
1041 1
  public function getLink()
1042
  {
1043
    return $this->link;
1044
  }
1045
1046
  /**
1047
   * Get the current charset.
1048
   *
1049
   * @return string
1050
   */
1051
  public function get_charset()
1052
  {
1053
    return $this->charset;
1054
  }
1055
1056 1
  /**
1057
   * Check if we are in a transaction.
1058 1
   *
1059
   * @return bool
1060
   */
1061
  public function inTransaction()
1062 1
  {
1063 1
    return $this->_in_transaction;
1064
  }
1065 1
1066
  /**
1067
   * Execute a "insert"-query.
1068 1
   *
1069 1
   * @param string      $table
1070 1
   * @param array       $data
1071
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1072 1
   *
1073
   * @return false|int <p>false on error</p>
1074 1
   *
1075 1
   * @throws QueryException
1076 1
   */
1077
  public function insert($table, array $data = array(), $databaseName = null)
1078 1
  {
1079
    // init
1080 1
    $table = trim($table);
1081 1
1082 1
    if ($table === '') {
1083 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1084 1
1085
      return false;
1086
    }
1087 1
1088 1
    if (count($data) === 0) {
1089 1
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1090
1091
      return false;
1092
    }
1093 1
1094
    $SET = $this->_parseArrayPair($data);
1095 1
1096
    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...
1097
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1098
    }
1099
1100
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1101
1102
    return $this->query($sql);
1103
  }
1104
1105
  /**
1106
   * Returns the auto generated id used in the last query.
1107
   *
1108 1
   * @return int|string
1109 1
   */
1110
  public function insert_id()
1111
  {
1112 1
    return \mysqli_insert_id($this->link);
1113 1
  }
1114
1115
  /**
1116
   * Check if db-connection is ready.
1117
   *
1118
   * @return boolean
1119
   */
1120
  public function isReady()
1121
  {
1122 1
    return $this->connected ? true : false;
1123
  }
1124 1
1125 1
  /**
1126
   * Get the last sql-error.
1127
   *
1128
   * @return string|false <p>false === there was no error</p>
1129
   */
1130
  public function lastError()
1131
  {
1132 4
    $errors = $this->_debug->getErrors();
1133
1134 4
    return count($errors) > 0 ? end($errors) : false;
1135
  }
1136 4
1137 1
  /**
1138
   * Execute a sql-multi-query.
1139 1
   *
1140
   * @param string $sql
1141
   *
1142 4
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1143
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1144
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1145
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1146
   *
1147
   * @throws QueryException
1148 4
   */
1149 4
  public function multi_query($sql)
1150
  {
1151 4
    if (!$this->isReady()) {
1152
      return false;
1153
    }
1154
1155 View Code Duplication
    if (!$sql || $sql === '') {
1156
      $this->_debug->displayError('Can not execute an empty query.', false);
1157
1158
      return false;
1159 4
    }
1160
1161 4
    $query_start_time = microtime(true);
1162
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1163
    $query_duration = microtime(true) - $query_start_time;
1164
1165
    $this->_debug->logQuery($sql, $query_duration, 0);
1166
1167
    $returnTheResult = false;
1168
    $result = array();
1169 4
    if ($resultTmp) {
1170
      do {
1171 4
        $resultTmpInner = \mysqli_store_result($this->link);
1172
1173
        if ($resultTmpInner instanceof \mysqli_result) {
1174
          $returnTheResult = true;
1175
1176
          $result[] = new Result($sql, $resultTmpInner);
1177
1178
        } else {
1179 2
1180
          // is the query successful
1181
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1182 2
            $result[] = true;
1183 1
          } else {
1184 1
            $result[] = false;
1185 1
          }
1186 1
        }
1187 1
1188
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1189
1190 2
    } else {
1191 2
1192
      // log the error query
1193 2
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1194
1195
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1196
    }
1197
1198
    // return the result only if there was a "SELECT"-query
1199
    if ($returnTheResult === true) {
1200
      return $result;
1201 2
    }
1202
1203 2
    if (
1204
        count($result) > 0
1205 2
        &&
1206
        in_array(false, $result, true) === false
1207
    ) {
1208
      return true;
1209
    }
1210
1211 2
    return false;
1212
  }
1213
1214 2
  /**
1215
   * Pings a server connection, or tries to reconnect
1216 2
   * if the connection has gone down.
1217 2
   *
1218 2
   * @return boolean
1219 2
   */
1220 2
  public function ping()
1221
  {
1222 2
    if (
1223
        $this->link
1224
        &&
1225
        $this->link instanceof \mysqli
1226
    ) {
1227
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1228
      /** @noinspection UsageOfSilenceOperatorInspection */
1229
      return (bool)@\mysqli_ping($this->link);
1230
    }
1231
1232
    return false;
1233
  }
1234
1235
  /**
1236 21
   * Selects a different database than the one specified on construction.
1237
   *
1238
   * @param string $database <p>Database name to switch to.</p>
1239 21
   *
1240
   * @return bool <p>Boolean true on success, false otherwise.</p>
1241 21
   */
1242 2
  public function select_db($database)
1243
  {
1244 2
    if (!$this->isReady()) {
1245
      return false;
1246
    }
1247 20
1248 3
    return mysqli_select_db($this->link, $database);
1249
  }
1250 3
1251
  /**
1252
   * Get a new "Prepare"-Object for your sql-query.
1253 18
   *
1254
   * @param string $query
1255 18
   *
1256
   * @return Prepare
1257
   */
1258
  public function prepare($query)
1259 18
  {
1260
    return new Prepare($this, $query);
1261 18
  }
1262
1263
  /**
1264
   * Execute a sql-query and return the result-array for select-statements.
1265
   *
1266
   * @param $query
1267
   *
1268
   * @return mixed
1269
   * @deprecated
1270
   * @throws \Exception
1271
   */
1272
  public static function qry($query)
1273
  {
1274 23
    $db = self::getInstance();
1275
1276
    $args = func_get_args();
1277 23
    /** @noinspection SuspiciousAssignmentsInspection */
1278
    $query = array_shift($args);
1279
    $query = str_replace('?', '%s', $query);
1280 23
    $args = array_map(
1281
        array(
1282
            $db,
1283
            'escape',
1284 23
        ),
1285 23
        $args
1286 23
    );
1287 23
    array_unshift($args, $query);
1288 23
    $query = call_user_func_array('sprintf', $args);
1289
    $result = $db->query($query);
1290 23
1291 2
    if ($result instanceof Result) {
1292 2
      return $result->fetchAllArray();
1293
    }
1294 23
1295 1
    return $result;
1296 1
  }
1297
1298 23
  /**
1299 1
   * Execute a sql-query.
1300 1
   *
1301
   * @param string        $sql            <p>The sql query-string.</p>
1302 23
   *
1303 1
   * @param array|boolean $params         <p>
1304 1
   *                                      "array" of sql-query-parameters<br/>
1305
   *                                      "false" if you don't need any parameter (default)<br/>
1306 23
   *                                      </p>
1307 1
   *
1308 1
   * @return bool|int|Result              <p>
1309
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1310 23
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1311 1
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1312 1
   *                                      "true" by e.g. "DROP"-queries<br />
1313
   *                                      "false" on error
1314 23
   *                                      </p>
1315 1
   *
1316 1
   * @throws QueryException
1317
   */
1318 23
  public function query($sql = '', $params = false)
1319 2
  {
1320 2
    if (!$this->isReady()) {
1321
      return false;
1322 23
    }
1323 2
1324 2 View Code Duplication
    if (!$sql || $sql === '') {
1325
      $this->_debug->displayError('Can not execute an empty query.', false);
1326 23
1327 4
      return false;
1328 4
    }
1329
1330 23
    if (
1331 1
        $params !== false
1332 1
        &&
1333
        is_array($params)
1334 23
        &&
1335 4
        count($params) > 0
1336 4
    ) {
1337
      $sql = $this->_parseQueryParams($sql, $params);
1338 23
    }
1339 1
1340 1
    $query_start_time = microtime(true);
1341
    $query_result = \mysqli_real_query($this->link, $sql);
1342 23
    $query_duration = microtime(true) - $query_start_time;
1343 1
1344 1
    $this->query_count++;
1345
1346 23
    $mysqli_field_count = \mysqli_field_count($this->link);
1347 2
    if ($mysqli_field_count) {
1348 2
      $result = \mysqli_store_result($this->link);
1349
    } else {
1350 23
      $result = $query_result;
1351 1
    }
1352 1
1353
    if ($result instanceof \mysqli_result) {
1354 23
1355 2
      // log the select query
1356 2
      $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 1337 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...
1357 2
1358
      // return query result object
1359 2
      return new Result($sql, $result);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1337 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...
1360 1
    }
1361 2
1362 1
    if ($query_result === true) {
1363 1
1364
      // "INSERT" || "REPLACE"
1365 2 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1366 23
        $insert_id = (int)$this->insert_id();
1367
        $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 1337 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...
1368
1369 23
        return $insert_id;
1370 23
      }
1371 23
1372
      // "UPDATE" || "DELETE"
1373 23 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1374 23
        $affected_rows = (int)$this->affected_rows();
1375 23
        $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 1337 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...
1376 23
1377
        return $affected_rows;
1378 23
      }
1379 23
1380 23
      // log the ? query
1381
      $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 1337 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...
1382 23
1383 23
      return true;
1384 23
    }
1385
1386 23
    // log the error query
1387 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 1337 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...
1388 23
1389
    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 1337 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...
1390 23
  }
1391 23
1392
  /**
1393 23
   * Error-handling for the sql-query.
1394
   *
1395 23
   * @param string     $errorMessage
1396
   * @param int        $errorNumber
1397 23
   * @param string     $sql
1398 1
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1399 23
   * @param bool       $sqlMultiQuery
1400 23
   *
1401 23
   * @throws QueryException
1402
   * @throws DBGoneAwayException
1403 23
   *
1404 1
   * @return bool
1405 1
   */
1406
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1407 23
  {
1408 23
    $errorNumber = (int)$errorNumber;
1409 23
1410
    if (
1411 23
        $errorMessage === 'DB server has gone away'
1412 2
        ||
1413 2
        $errorMessage === 'MySQL server has gone away'
1414
        ||
1415 23
        $errorNumber === 2006
1416 23
    ) {
1417
      static $RECONNECT_COUNTER;
1418 23
1419
      // exit if we have more then 3 "DB server has gone away"-errors
1420
      if ($RECONNECT_COUNTER > 3) {
1421
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql, 5);
1422
        throw new DBGoneAwayException($errorMessage);
1423
      }
1424
1425
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1426
1427
      // reconnect
1428 26
      $RECONNECT_COUNTER++;
1429
      $this->reconnect(true);
1430 26
1431 26
      // re-run the current (non multi) query
1432 26
      if ($sqlMultiQuery === false) {
1433 26
        return $this->query($sql, $sqlParams);
1434 26
      }
1435
1436 26
      return false;
1437 26
    }
1438
1439 26
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1440
1441
    // this query returned an error, we must display it (only for dev) !!!
1442
    $this->_debug->displayError($errorMessage . '(' . $errorNumber. ') ' . ' | ' . $sql);
1443
1444
    return false;
1445
  }
1446
1447 1
  /**
1448
   * Quote && Escape e.g. a table name string.
1449 1
   *
1450
   * @param string $str
1451
   *
1452
   * @return string
1453
   */
1454
  public function quote_string($str)
1455
  {
1456
    $str = str_replace(
1457
        '`',
1458
        '``',
1459
        trim(
1460
            $this->escape($str, false),
1461
            '`'
1462
        )
1463 1
    );
1464
1465
    return '`' . $str . '`';
1466 1
  }
1467
1468 1
  /**
1469 1
   * Reconnect to the MySQL-Server.
1470
   *
1471 1
   * @param bool $checkViaPing
1472
   *
1473
   * @return bool
1474 1
   */
1475 1
  public function reconnect($checkViaPing = false)
1476
  {
1477 1
    $ping = false;
1478
1479
    if ($checkViaPing === true) {
1480
      $ping = $this->ping();
1481 1
    }
1482 1
1483
    if ($ping !== true) {
1484 1
      $this->connected = false;
1485 1
      $this->connect();
1486
    }
1487 1
1488
    return $this->isReady();
1489
  }
1490 1
1491
  /**
1492 1
   * Execute a "replace"-query.
1493 1
   *
1494 1
   * @param string      $table
1495
   * @param array       $data
1496 1
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1497
   *
1498
   * @return false|int <p>false on error</p>
1499
   *
1500 1
   * @throws QueryException
1501
   */
1502 1
  public function replace($table, array $data = array(), $databaseName = null)
1503
  {
1504
    // init
1505
    $table = trim($table);
1506
1507
    if ($table === '') {
1508
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1509
1510
      return false;
1511
    }
1512
1513
    if (count($data) === 0) {
1514
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1515
1516
      return false;
1517 6
    }
1518
1519
    // extracting column names
1520 6
    $columns = array_keys($data);
1521
    foreach ($columns as $k => $_key) {
1522 6
      /** @noinspection AlterInForeachInspection */
1523 1
      $columns[$k] = $this->quote_string($_key);
1524
    }
1525 1
1526
    $columns = implode(',', $columns);
1527
1528 6
    // extracting values
1529 2
    foreach ($data as $k => $_value) {
1530
      /** @noinspection AlterInForeachInspection */
1531 2
      $data[$k] = $this->secure($_value);
1532
    }
1533
    $values = implode(',', $data);
1534 6
1535
    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...
1536 6
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1537 2
    }
1538 6
1539 4
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1540 4
1541 1
    return $this->query($sql);
1542
  }
1543
1544 6
  /**
1545
   * Rollback in a transaction and end the transaction.
1546
   *
1547
   * @return bool <p>Boolean true on success, false otherwise.</p>
1548 6
   */
1549
  public function rollback()
1550 6
  {
1551
    if ($this->_in_transaction === false) {
1552
      return false;
1553
    }
1554
1555
    $return = \mysqli_rollback($this->link);
1556
    \mysqli_autocommit($this->link, true);
1557
    $this->_in_transaction = false;
1558
1559
    return $return;
1560
  }
1561
1562
  /**
1563
   * Commits the current transaction and end the transaction.
1564 2
   *
1565
   * @return bool <p>Boolean true on success, false otherwise.</p>
1566
   */
1567 2
  public function commit()
1568
  {
1569 2
    if ($this->_in_transaction === false) {
1570 1
      return false;
1571
    }
1572 1
1573
    if (mysqli_commit($this->link)) {
1574
      $this->_in_transaction = false;
1575 2
      return true;
1576 1
    }
1577 2
1578 2
    return false;
1579 2
  }
1580 1
1581
  /**
1582
   * Execute a callback inside a transaction.
1583 2
   *
1584
   * @param callback $callback The callback to run inside the transaction
1585
   *
1586
   * @return bool Boolean true on success, false otherwise
1587 2
   */
1588
  public function transact($callback)
1589 2
  {
1590
    try {
1591
      $this->beginTransaction();
1592
      call_user_func($callback, $this);
1593
      return $this->commit();
1594
    } catch (\Exception $e) {
1595
      $this->rollback();
1596
      return false;
1597
    }
1598
  }
1599
1600
  /**
1601
   * Try to secure a variable, so can you use it in sql-queries.
1602
   *
1603 20
   * <p>
1604
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1605
   * 1. parse into "int"
1606 20
   * </p><br />
1607
   *
1608 20
   * <p>
1609 1
   * <strong>float:</strong><br />
1610
   * 1. return "float"
1611 1
   * </p><br />
1612
   *
1613
   * <p>
1614 20
   * <strong>string:</strong><br />
1615 5
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1616 20
   * 2. trim whitespace<br />
1617 16
   * 3. trim '<br />
1618 16
   * 4. escape the string (and remove non utf-8 chars)<br />
1619 1
   * 5. trim ' again (because we maybe removed some chars)<br />
1620
   * 6. add ' around the new string<br />
1621
   * </p><br />
1622 20
   *
1623
   * <p>
1624
   * <strong>array:</strong><br />
1625
   * 1. return null
1626 20
   * </p><br />
1627
   *
1628 20
   * <p>
1629
   * <strong>object:</strong><br />
1630
   * 1. return false
1631
   * </p><br />
1632
   *
1633
   * <p>
1634
   * <strong>null:</strong><br />
1635
   * 1. return null
1636 1
   * </p>
1637
   *
1638 1
   * @param mixed $var
1639
   *
1640 1
   * @return mixed
1641
   */
1642
  public function secure($var)
1643
  {
1644
    if (
1645
        $var === ''
1646 9
        ||
1647
        ($this->_convert_null_to_empty_string === true && $var === null)
1648 9
    ) {
1649
      return "''";
1650
    }
1651
1652
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1653
      return $var;
1654
    }
1655
1656
    if (is_string($var)) {
1657
      $var = trim(trim($var), "'");
1658
    }
1659
1660
    $var = $this->escape($var, false, false, null);
1661
1662
    if (is_string($var)) {
1663
      $var = "'" . trim($var, "'") . "'";
1664
    }
1665
1666 2
    return $var;
1667
  }
1668 2
1669
  /**
1670 2
   * Execute a "select"-query.
1671 2
   *
1672 2
   * @param string       $table
1673 2
   * @param string|array $where
1674
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1675
   *
1676
   * @return false|Result <p>false on error</p>
1677
   *
1678
   * @throws QueryException
1679
   */
1680 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...
1681
  {
1682
    // init
1683
    $table = trim($table);
1684
1685
    if ($table === '') {
1686
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1687
1688
      return false;
1689 2
    }
1690
1691 2
    if (is_string($where)) {
1692 2
      $WHERE = $this->escape($where, false);
1693
    } elseif (is_array($where)) {
1694
      $WHERE = $this->_parseArrayPair($where, 'AND');
1695
    } else {
1696
      $WHERE = '';
1697
    }
1698
1699
    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...
1700
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1701
    }
1702
1703
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1704
1705
    return $this->query($sql);
1706
  }
1707
1708
  /**
1709
   * Set the current charset.
1710
   *
1711
   * @param string $charset
1712
   *
1713
   * @return bool
1714
   */
1715
  public function set_charset($charset)
1716
  {
1717
    $charsetLower = strtolower($charset);
1718
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1719
      $charset = 'utf8';
1720
    }
1721
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1722
      $charset = 'utf8mb4';
1723
    }
1724
1725
    $this->charset = (string)$charset;
1726
1727
    $return = mysqli_set_charset($this->link, $charset);
1728
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1729
    /** @noinspection UsageOfSilenceOperatorInspection */
1730
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1731
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1732
    /** @noinspection UsageOfSilenceOperatorInspection */
1733
    @\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...
1734
1735
    return $return;
1736
  }
1737
1738
  /**
1739
   * Set the option to convert null to "''" (empty string).
1740
   *
1741
   * Used in secure() => select(), insert(), update(), delete()
1742
   *
1743
   * @param $bool
1744
   */
1745
  public function set_convert_null_to_empty_string($bool)
1746
  {
1747
    $this->_convert_null_to_empty_string = (bool)$bool;
1748
  }
1749
1750
  /**
1751
   * Enables or disables internal report functions
1752
   *
1753
   * @link http://php.net/manual/en/function.mysqli-report.php
1754
   *
1755
   * @param int $flags <p>
1756
   *                   <table>
1757
   *                   Supported flags
1758
   *                   <tr valign="top">
1759
   *                   <td>Name</td>
1760
   *                   <td>Description</td>
1761
   *                   </tr>
1762
   *                   <tr valign="top">
1763
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1764
   *                   <td>Turns reporting off</td>
1765
   *                   </tr>
1766
   *                   <tr valign="top">
1767
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1768
   *                   <td>Report errors from mysqli function calls</td>
1769
   *                   </tr>
1770
   *                   <tr valign="top">
1771
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1772
   *                   <td>
1773
   *                   Throw <b>mysqli_sql_exception</b> for errors
1774
   *                   instead of warnings
1775
   *                   </td>
1776
   *                   </tr>
1777
   *                   <tr valign="top">
1778
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1779
   *                   <td>Report if no index or bad index was used in a query</td>
1780
   *                   </tr>
1781
   *                   <tr valign="top">
1782
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1783
   *                   <td>Set all options (report all)</td>
1784
   *                   </tr>
1785
   *                   </table>
1786
   *                   </p>
1787
   *
1788
   * @return bool
1789
   */
1790
  public function set_mysqli_report($flags)
1791
  {
1792
    return \mysqli_report($flags);
1793
  }
1794
1795
  /**
1796
   * Show config errors by throw exceptions.
1797
   *
1798
   * @return bool
1799
   *
1800
   * @throws \InvalidArgumentException
1801
   */
1802
  public function showConfigError()
1803
  {
1804
1805
    if (
1806
        !$this->hostname
1807
        ||
1808
        !$this->username
1809
        ||
1810
        !$this->database
1811
    ) {
1812
1813
      if (!$this->hostname) {
1814
        throw new \InvalidArgumentException('no-sql-hostname');
1815
      }
1816
1817
      if (!$this->username) {
1818
        throw new \InvalidArgumentException('no-sql-username');
1819
      }
1820
1821
      if (!$this->database) {
1822
        throw new \InvalidArgumentException('no-sql-database');
1823
      }
1824
1825
      return false;
1826
    }
1827
1828
    return true;
1829
  }
1830
1831
  /**
1832
   * alias: "beginTransaction()"
1833
   */
1834
  public function startTransaction()
1835
  {
1836
    $this->beginTransaction();
1837
  }
1838
1839
  /**
1840
   * Execute a "update"-query.
1841
   *
1842
   * @param string       $table
1843
   * @param array        $data
1844
   * @param array|string $where
1845
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1846
   *
1847
   * @return false|int <p>false on error</p>
1848
   *
1849
   * @throws QueryException
1850
   */
1851
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1852
  {
1853
    // init
1854
    $table = trim($table);
1855
1856
    if ($table === '') {
1857
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1858
1859
      return false;
1860
    }
1861
1862
    if (count($data) === 0) {
1863
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1864
1865
      return false;
1866
    }
1867
1868
    $SET = $this->_parseArrayPair($data);
1869
1870
    if (is_string($where)) {
1871
      $WHERE = $this->escape($where, false);
1872
    } elseif (is_array($where)) {
1873
      $WHERE = $this->_parseArrayPair($where, 'AND');
1874
    } else {
1875
      $WHERE = '';
1876
    }
1877
1878
    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...
1879
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1880
    }
1881
1882
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1883
1884
    return $this->query($sql);
1885
  }
1886
1887
}