Completed
Push — master ( 87c372...9db816 )
by Lars
01:48
created

DB::getDebugger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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