Completed
Push — master ( 0fccc4...58c13f )
by Lars
79:58 queued 64:56
created

DB::execSQL()   C

Complexity

Conditions 9
Paths 7

Size

Total Lines 47
Code Lines 28

Duplication

Lines 26
Ratio 55.32 %

Code Coverage

Tests 16
CRAP Score 9.0164

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
480 28
  {
481 28
    if ($this->_in_transaction === true) {
482 24
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
483
      return false;
484
    }
485 33
486
    $this->clearErrors(); // needed for "$this->endTransaction()"
487
    $this->_in_transaction = true;
488 27
    $return = \mysqli_autocommit($this->link, false);
489
    if ($return === false) {
490
      $this->_in_transaction = false;
491 27
    }
492
493
    return $return;
494
  }
495 25
496
497
  /**
498 23
   * Clear the errors in "_debug->_errors".
499 22
   *
500 22
   * @return bool
501
   */
502 22
  public function clearErrors()
503
  {
504
    return $this->_debug->clearErrors();
505
  }
506 8
507 8
  /**
508 8
   * Closes a previously opened database connection.
509
   *
510 8
   * @return bool
511
   */
512
  public function close()
513
  {
514
    $this->connected = false;
515
    if (!$this->link) {
516
      return false;
517
    }
518
519
    if (\mysqli_close($this->link)) {
520 8
      $this->link = null;
521
      return true;
522 8
    }
523
524
    return false;
525
  }
526
527
  /**
528
   * Open a new connection to the MySQL server.
529
   *
530
   * @return bool
531
   *
532
   * @throws DBConnectException
533 3
   */
534
  public function connect()
535
  {
536 3
    if ($this->isReady()) {
537
      return true;
538
    }
539
540 3
    $flags = null;
541 3
542 3
    \mysqli_report(MYSQLI_REPORT_STRICT);
543
    try {
544 3
      $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...
545 3
546 3
      if (Helper::isMysqlndIsUsed() === true) {
547 3
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
548 3
      }
549 3
550 3
      if ($this->_ssl === true) {
551
552 3
        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...
553
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
554
        }
555
556
        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...
557
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
558
        }
559
560
        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...
561
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
562
        }
563
564
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
565
        \mysqli_ssl_set(
566
            $this->link,
567
            $this->_clientkey,
568
            $this->_clientcert,
569
            $this->_cacert,
570
            null,
571
            null
572
        );
573
        $flags = MYSQLI_CLIENT_SSL;
574
      }
575
576
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
577
      $this->connected = @\mysqli_real_connect(
578
          $this->link,
579
          $this->hostname,
580
          $this->username,
581
          $this->password,
582
          $this->database,
583
          $this->port,
584
          $this->socket,
585
          $flags
586
      );
587
    } catch (\Exception $e) {
588
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
589
      $this->_debug->displayError($error, false);
590
      throw new DBConnectException($error, 100, $e);
591
    }
592
    \mysqli_report(MYSQLI_REPORT_OFF);
593
594
    $errno = mysqli_connect_errno();
595
    if (!$this->connected || $errno) {
596
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
597 26
      $this->_debug->displayError($error, false);
598
      throw new DBConnectException($error, 101);
599
    }
600
601 26
    $this->set_charset($this->charset);
602 26
603 26
    return $this->isReady();
604 1
  }
605
606
  /**
607 26
   * Execute a "delete"-query.
608 1
   *
609
   * @param string       $table
610
   * @param string|array $where
611 26
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
612 22
   *
613 22
   * @return false|int <p>false on error</p>
614
   *
615 26
   *    * @throws QueryException
616
   */
617 26 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...
618 22
  {
619 22
    // init
620
    $table = trim($table);
621 26
622
    if ($table === '') {
623
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
624
625
      return false;
626
    }
627
628
    if (is_string($where)) {
629
      $WHERE = $this->escape($where, false);
630
    } elseif (is_array($where)) {
631
      $WHERE = $this->_parseArrayPair($where, 'AND');
632
    } else {
633
      $WHERE = '';
634
    }
635
636
    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...
637
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
638
    }
639
640
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
641 33
642
    return $this->query($sql);
643 33
  }
644 2
645
  /**
646
   * Ends a transaction and commits if no errors, then ends autocommit.
647 33
   *
648 2
   * @return bool <p>This will return true or false indicating success of transactions.</p>
649
   */
650 View Code Duplication
  public function endTransaction()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
651
  {
652 33
    if ($this->_in_transaction === false) {
653 33
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
654 33
      return false;
655
    }
656
657
    if (!$this->errors()) {
658 33
      $return = \mysqli_commit($this->link);
659
    } else {
660 33
      $this->rollback();
661 33
      $return = false;
662
    }
663 33
664 33
    \mysqli_autocommit($this->link, true);
665 33
    $this->_in_transaction = false;
666 33
667
    return $return;
668 33
  }
669 33
670
  /**
671
   * Get all errors from "$this->_errors".
672
   *
673 24
   * @return array|false <p>false === on errors</p>
674
   */
675
  public function errors()
676 33
  {
677
    $errors = $this->_debug->getErrors();
678
679
    return count($errors) > 0 ? $errors : false;
680 5
  }
681
682
  /**
683 33
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
684
   *
685
   * @param mixed     $var           boolean: convert into "integer"<br />
686
   *                                 int: int (don't change it)<br />
687 3
   *                                 float: float (don't change it)<br />
688 3
   *                                 null: null (don't change it)<br />
689
   *                                 array: run escape() for every key => value<br />
690
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
691 1
   * @param bool      $stripe_non_utf8
692 1
   * @param bool      $html_entity_decode
693
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
694 1
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
695 1
   *                                 <strong>null</strong> => Convert the array into null, every time.
696
   *
697
   * @return mixed
698 1
   */
699 1
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
700
  {
701 1
    if ($var === '') {
702 1
      return '';
703
    }
704 1
705
    if ($var === null) {
706
      return null;
707 1
    }
708
709
    // save the current value as int (for later usage)
710
    if (!is_object($var)) {
711 33
      $varInt = (int)$var;
712
    }
713
714 3
    /** @noinspection TypeUnsafeComparisonInspection */
715 3
    if (
716 3
        is_int($var)
717 3
        ||
718 33
        is_bool($var)
719
        ||
720
        (
721
            isset($varInt, $var[0])
722 33
            &&
723
            $var[0] != '0'
724 33
            &&
725 9
            "$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...
726 9
        )
727
    ) {
728 33
729
      // "int" || int || bool
730 1
731 1
      return (int)$var;
732
    }
733 33
734
    if (is_float($var)) {
735 33
736
      // float
737 33
738
      return $var;
739
    }
740
741 3
    if (is_array($var)) {
742
743
      // array
744
745
      if ($convert_array === null) {
746 3
        return null;
747
      }
748
749
      $varCleaned = array();
750
      foreach ((array)$var as $key => $value) {
751
752 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
753
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
754
755
        /** @noinspection OffsetOperationsInspection */
756
        $varCleaned[$key] = $value;
757
      }
758
759
      if ($convert_array === true) {
760
        $varCleaned = implode(',', $varCleaned);
761 35
762
        return $varCleaned;
763 35
      }
764
765
      return (array)$varCleaned;
766
    }
767
768
    if (
769
        is_string($var)
770
        ||
771 22
        (
772
            is_object($var)
773 22
            &&
774
            method_exists($var, '__toString')
775
        )
776
    ) {
777
778
      // "string"
779
780
      $var = (string)$var;
781 8
782
      if ($stripe_non_utf8 === true) {
783 8
        $var = UTF8::cleanup($var);
784
      }
785
786
      if ($html_entity_decode === true) {
787
        // use no-html-entity for db
788
        $var = UTF8::html_entity_decode($var);
789
      }
790
791
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
792
793
      $var = \mysqli_real_escape_string($this->getLink(), $var);
794
795
      return (string)$var;
796
797
    }
798 9
799
    if ($var instanceof \DateTime) {
800 9
801 1
      // "DateTime"-object
802
803
      try {
804 1
        return $this->escape($var->format('Y-m-d H:i:s'), false);
805
      } catch (\Exception $e) {
806
        return null;
807
      }
808
809 1
    } else {
810
      return false;
811
    }
812 1
  }
813 1
814
  /**
815
   * Execute select/insert/update/delete sql-queries.
816 1
   *
817
   * @param string $query    sql-query
818
   * @param bool   $useCache use cache?
819 8
   * @param int    $cacheTTL cache-ttl in seconds
820
   *
821
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
822 8
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
823
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
824 8
   *               "true" by e.g. "DROP"-queries<br />
825
   *               "false" on error
826
   *
827
   * @throws QueryException
828
   */
829
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600)
830
  {
831
    // init
832
    $cacheKey = null;
833
    $db = self::getInstance();
834 3
835 View Code Duplication
    if ($useCache === true) {
836 3
      $cache = new Cache(null, null, false, $useCache);
837
      $cacheKey = 'sql-' . md5($query);
838 3
839 2
      if (
840 2
          $cache->getCacheIsReady() === true
841
          &&
842 3
          $cache->existsItem($cacheKey)
843 3
      ) {
844 3
        return $cache->getItem($cacheKey);
845 3
      }
846
847 3
    } else {
848
      $cache = false;
849
    }
850
851
    $result = $db->query($query);
852
853
    if ($result instanceof Result) {
854
855
      $return = $result->fetchAllArray();
856 3
857
      // save into the cache
858 View Code Duplication
      if (
859 3
          $cacheKey !== null
860 3
          &&
861 3
          $useCache === true
862 3
          &&
863
          $cache instanceof Cache
864
          &&
865 3
          $cache->getCacheIsReady() === true
866
      ) {
867
        $cache->setItem($cacheKey, $return, $cacheTTL);
868
      }
869
870
    } else {
871
      $return = $result;
872
    }
873
874
    return $return;
875
  }
876
877
  /**
878
   * Get all table-names via "SHOW TABLES".
879
   *
880
   * @return array
881
   */
882
  public function getAllTables()
883
  {
884
    $query = 'SHOW TABLES';
885
    $result = $this->query($query);
886 3
887
    return $result->fetchAllArray();
888
  }
889 3
890 3
  /**
891
   * @return Debug
892 3
   */
893 1
  public function getDebugger()
894 1
  {
895
    return $this->_debug;
896
  }
897 1
898 1
  /**
899 1
   * Get errors from "$this->_errors".
900 1
   *
901 1
   * @return array
902
   */
903
  public function getErrors()
904 1
  {
905 3
    return $this->_debug->getErrors();
906
  }
907
908 3
  /**
909
   * getInstance()
910 3
   *
911
   * @param string      $hostname
912 1
   * @param string      $username
913
   * @param string      $password
914
   * @param string      $database
915
   * @param string      $port          default is (int)3306
916
   * @param string      $charset       default is 'utf8' or 'utf8mb4' (if supported)
917 1
   * @param bool|string $exit_on_error use a empty string "" or false to disable it
918
   * @param bool|string $echo_on_error use a empty string "" or false to disable it
919 1
   * @param string      $logger_class_name
920
   * @param string      $logger_level
921 1
   * @param bool|string $session_to_db use a empty string "" or false to disable it
922 1
   *
923 1
   * @return \voku\db\DB
924 1
   */
925 1
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $session_to_db = '')
926
  {
927 1
    /**
928 2
     * @var $instance DB[]
929
     */
930
    static $instance = array();
931 3
932
    /**
933
     * @var $firstInstance DB
934
     */
935
    static $firstInstance = null;
936
937
    if (
938
        $hostname . $username . $password . $database . $port . $charset == ''
939 1
        &&
940
        null !== $firstInstance
941 1
    ) {
942
      return $firstInstance;
943
    }
944
945
    $connection = md5(
946
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . (int)$session_to_db
947
    );
948
949
    if (!isset($instance[$connection])) {
950
      $instance[$connection] = new self(
951
          $hostname,
952
          $username,
953
          $password,
954
          $database,
955
          $port,
956
          $charset,
957
          $exit_on_error,
958
          $echo_on_error,
959
          $logger_class_name,
960
          $logger_level,
961
          $session_to_db
962
      );
963
964
      if (null === $firstInstance) {
965
        $firstInstance = $instance[$connection];
966
      }
967
    }
968
969
    return $instance[$connection];
970
  }
971
972
  /**
973
   * Get the mysqli-link (link identifier returned by mysqli-connect).
974
   *
975
   * @return \mysqli
976
   */
977
  public function getLink()
978
  {
979
    return $this->link;
980
  }
981
982
  /**
983
   * Get the current charset.
984
   *
985
   * @return string
986
   */
987
  public function get_charset()
988
  {
989
    return $this->charset;
990
  }
991
992
  /**
993
   * Check if we are in a transaction.
994
   *
995
   * @return bool
996 7
   */
997
  public function inTransaction()
998 7
  {
999 7
    return $this->_in_transaction;
1000 5
  }
1001 5
1002 7
  /**
1003 5
   * Execute a "insert"-query.
1004 5
   *
1005
   * @param string      $table
1006 7
   * @param array       $data
1007
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1008 7
   *
1009
   * @return false|int <p>false on error</p>
1010
   *
1011 7
   * @throws QueryException
1012
   */
1013
  public function insert($table, array $data = array(), $databaseName = null)
1014 7
  {
1015
    // init
1016 7
    $table = trim($table);
1017
1018
    if ($table === '') {
1019
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1020
1021
      return false;
1022
    }
1023
1024
    if (count($data) === 0) {
1025
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1026 1
1027
      return false;
1028 1
    }
1029 1
1030
    $SET = $this->_parseArrayPair($data);
1031
1032
    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...
1033
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1034
    }
1035
1036 1
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1037
1038 1
    return $this->query($sql);
1039 1
  }
1040
1041 1
  /**
1042
   * Returns the auto generated id used in the last query.
1043
   *
1044
   * @return int|string
1045
   */
1046
  public function insert_id()
1047
  {
1048
    return \mysqli_insert_id($this->link);
1049
  }
1050
1051
  /**
1052
   * Check if db-connection is ready.
1053
   *
1054
   * @return boolean
1055
   */
1056 1
  public function isReady()
1057
  {
1058 1
    return $this->connected ? true : false;
1059
  }
1060
1061
  /**
1062 1
   * Get the last sql-error.
1063 1
   *
1064
   * @return string|false <p>false === there was no error</p>
1065 1
   */
1066
  public function lastError()
1067
  {
1068 1
    $errors = $this->_debug->getErrors();
1069 1
1070 1
    return count($errors) > 0 ? end($errors) : false;
1071
  }
1072 1
1073
  /**
1074 1
   * Execute a sql-multi-query.
1075 1
   *
1076 1
   * @param string $sql
1077
   *
1078 1
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1079
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1080 1
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1081 1
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1082 1
   *
1083 1
   * @throws QueryException
1084 1
   */
1085
  public function multi_query($sql)
1086
  {
1087 1
    if (!$this->isReady()) {
1088 1
      return false;
1089 1
    }
1090
1091 View Code Duplication
    if (!$sql || $sql === '') {
1092
      $this->_debug->displayError('Can not execute an empty query.', false);
1093 1
1094
      return false;
1095 1
    }
1096
1097
    $query_start_time = microtime(true);
1098
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1099
    $query_duration = microtime(true) - $query_start_time;
1100
1101
    $this->_debug->logQuery($sql, $query_duration, 0);
1102
1103
    $returnTheResult = false;
1104
    $result = array();
1105
    if ($resultTmp) {
1106
      do {
1107
        $resultTmpInner = \mysqli_store_result($this->link);
1108 1
1109 1
        if ($resultTmpInner instanceof \mysqli_result) {
1110
          $returnTheResult = true;
1111
          $result[] = new Result($sql, $resultTmpInner);
1112 1
        } else {
1113 1
          $errorMsg = \mysqli_error($this->link);
1114
1115
          // is the query successful
1116
          if ($resultTmpInner === true || !$errorMsg) {
1117
            $result[] = true;
1118
          } else {
1119
            $result[] = $this->queryErrorHandling($errorMsg, $sql);
1120
          }
1121
        }
1122 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1123
1124 1
    } else {
1125 1
1126
      $errorMsg = \mysqli_error($this->link);
1127
1128
      if ($this->_debug->checkForDev() === true) {
1129
        echo "Info: maybe you have to increase your 'max_allowed_packet = 30M' in the config: 'my.conf' \n<br />";
1130
        echo 'Error:' . $errorMsg;
1131
      }
1132 4
1133
      $this->_debug->mailToAdmin('SQL-Error in mysqli_multi_query', $errorMsg . ":\n<br />" . $sql);
1134 4
    }
1135
1136 4
    // return the result only if there was a "SELECT"-query
1137 1
    if ($returnTheResult === true) {
1138
      return $result;
1139 1
    }
1140
1141
    if (in_array(false, $result, true) === false) {
1142 4
      return true;
1143
    }
1144
1145
    return false;
1146
  }
1147
1148 4
  /**
1149 4
   * Pings a server connection, or tries to reconnect
1150
   * if the connection has gone down.
1151 4
   *
1152
   * @return boolean
1153
   */
1154
  public function ping()
1155
  {
1156
    if (
1157
        $this->link
1158
        &&
1159 4
        $this->link instanceof \mysqli
1160
    ) {
1161 4
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1162
      /** @noinspection UsageOfSilenceOperatorInspection */
1163
      return (bool)@\mysqli_ping($this->link);
1164
    }
1165
1166
    return false;
1167
  }
1168
1169 4
  /**
1170
   * Selects a different database than the one specified on construction.
1171 4
   *
1172
   * @param string $database <p>Database name to switch to.</p>
1173
   *
1174
   * @return bool <p>Boolean true on success, false otherwise.</p>
1175
   */
1176
  public function select_db($database)
1177
  {
1178
    if (!$this->isReady()) {
1179 2
      return false;
1180
    }
1181
1182 2
    return mysqli_select_db($this->link, $database);
1183 1
  }
1184 1
1185 1
  /**
1186 1
   * Get a new "Prepare"-Object for your sql-query.
1187 1
   *
1188
   * @param string $query
1189
   *
1190 2
   * @return Prepare
1191 2
   */
1192
  public function prepare($query)
1193 2
  {
1194
    return new Prepare($this, $query);
1195
  }
1196
1197
  /**
1198
   * Execute a sql-query and return the result-array for select-statements.
1199
   *
1200
   * @param $query
1201 2
   *
1202
   * @return mixed
1203 2
   * @deprecated
1204
   * @throws \Exception
1205 2
   */
1206
  public static function qry($query)
1207
  {
1208
    $db = self::getInstance();
1209
1210
    $args = func_get_args();
1211 2
    /** @noinspection SuspiciousAssignmentsInspection */
1212
    $query = array_shift($args);
1213
    $query = str_replace('?', '%s', $query);
1214 2
    $args = array_map(
1215
        array(
1216 2
            $db,
1217 2
            'escape',
1218 2
        ),
1219 2
        $args
1220 2
    );
1221
    array_unshift($args, $query);
1222 2
    $query = call_user_func_array('sprintf', $args);
1223
    $result = $db->query($query);
1224
1225
    if ($result instanceof Result) {
1226
      return $result->fetchAllArray();
1227
    }
1228
1229
    return $result;
1230
  }
1231
1232
  /**
1233
   * Execute a sql-query.
1234
   *
1235
   * @param string        $sql            <p>The sql query-string.</p>
1236 21
   *
1237
   * @param array|boolean $params         <p>
1238
   *                                      "array" of sql-query-parameters<br/>
1239 21
   *                                      "false" if you don't need any parameter (default)<br/>
1240
   *                                      </p>
1241 21
   *
1242 2
   * @return bool|int|Result              <p>
1243
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1244 2
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1245
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1246
   *                                      "true" by e.g. "DROP"-queries<br />
1247 20
   *                                      "false" on error
1248 3
   *                                      </p>
1249
   *
1250 3
   * @throws QueryException
1251
   */
1252
  public function query($sql = '', $params = false)
1253 18
  {
1254
    if (!$this->isReady()) {
1255 18
      return false;
1256
    }
1257
1258 View Code Duplication
    if (!$sql || $sql === '') {
1259 18
      $this->_debug->displayError('Can not execute an empty query.', false);
1260
1261 18
      return false;
1262
    }
1263
1264
    if (
1265
        $params !== false
1266
        &&
1267
        is_array($params)
1268
        &&
1269
        count($params) > 0
1270
    ) {
1271
      $sql = $this->_parseQueryParams($sql, $params);
1272
    }
1273
1274 23
    $query_start_time = microtime(true);
1275
    $query_result = \mysqli_real_query($this->link, $sql);
1276
    $query_duration = microtime(true) - $query_start_time;
1277 23
1278
    $this->query_count++;
1279
1280 23
    $mysqli_field_count = \mysqli_field_count($this->link);
1281
    if ($mysqli_field_count) {
1282
      $result = \mysqli_store_result($this->link);
1283
    } else {
1284 23
      $result = $query_result;
1285 23
    }
1286 23
1287 23
    if ($result instanceof \mysqli_result) {
1288 23
1289
      // log the select query
1290 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 1271 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...
1291 2
1292 2
      // return query result object
1293
      return new Result($sql, $result);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1271 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...
1294 23
1295 1
    }
1296 1
1297
    if ($query_result === true) {
1298 23
1299 1
      // "INSERT" || "REPLACE"
1300 1 View Code Duplication
      if (preg_match('/^\s*"?(INSERT|REPLACE)\s+/i', $sql)) {
1301
        $insert_id = (int)$this->insert_id();
1302 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 1271 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...
1303 1
1304 1
        return $insert_id;
1305
      }
1306 23
1307 1
      // "UPDATE" || "DELETE"
1308 1 View Code Duplication
      if (preg_match('/^\s*"?(UPDATE|DELETE)\s+/i', $sql)) {
1309
        $affected_rows = (int)$this->affected_rows();
1310 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 1271 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...
1311 1
1312 1
        return $affected_rows;
1313
      }
1314 23
1315 1
      // log the ? query
1316 1
      $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 1271 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...
1317
1318 23
      return true;
1319 2
    }
1320 2
1321
    // log the error query
1322 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 1271 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...
1323 2
1324 2
    return $this->queryErrorHandling(\mysqli_error($this->link), $sql, $params);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1271 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...
1325
  }
1326 23
1327 4
  /**
1328 4
   * Error-handling for the sql-query.
1329
   *
1330 23
   * @param string     $errorMsg
1331 1
   * @param string     $sql
1332 1
   * @param array|bool $sqlParams false if there wasn't any parameter
1333
   *
1334 23
   * @throws QueryException
1335 4
   * @throws DBGoneAwayException
1336 4
   *
1337
   * @return bool
1338 23
   */
1339 1 View Code Duplication
  protected function queryErrorHandling($errorMsg, $sql, $sqlParams = false)
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...
1340 1
  {
1341
    if ($errorMsg === 'DB server has gone away' || $errorMsg === 'MySQL server has gone away') {
1342 23
      static $RECONNECT_COUNTER;
1343 1
1344 1
      // exit if we have more then 3 "DB server has gone away"-errors
1345
      if ($RECONNECT_COUNTER > 3) {
1346 23
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMsg . ":\n<br />" . $sql, 5);
1347 2
        throw new DBGoneAwayException($errorMsg);
1348 2
      }
1349
1350 23
      $this->_debug->mailToAdmin('DB-Error', $errorMsg . ":\n<br />" . $sql);
1351 1
1352 1
      // reconnect
1353
      $RECONNECT_COUNTER++;
1354 23
      $this->reconnect(true);
1355 2
1356 2
      // re-run the current query
1357 2
      return $this->query($sql, $sqlParams);
1358
    }
1359 2
1360 1
    $this->_debug->mailToAdmin('SQL-Error', $errorMsg . ":\n<br />" . $sql);
1361 2
1362 1
    // this query returned an error, we must display it (only for dev) !!!
1363 1
    $this->_debug->displayError($errorMsg . ' | ' . $sql);
1364
1365 2
    return false;
1366 23
  }
1367
1368
  /**
1369 23
   * Quote && Escape e.g. a table name string.
1370 23
   *
1371 23
   * @param string $str
1372
   *
1373 23
   * @return string
1374 23
   */
1375 23
  public function quote_string($str)
1376 23
  {
1377
    $str = str_replace(
1378 23
        '`',
1379 23
        '``',
1380 23
        trim(
1381
            $this->escape($str, false),
1382 23
            '`'
1383 23
        )
1384 23
    );
1385
1386 23
    return '`' . $str . '`';
1387 23
  }
1388 23
1389
  /**
1390 23
   * Reconnect to the MySQL-Server.
1391 23
   *
1392
   * @param bool $checkViaPing
1393 23
   *
1394
   * @return bool
1395 23
   */
1396
  public function reconnect($checkViaPing = false)
1397 23
  {
1398 1
    $ping = false;
1399 23
1400 23
    if ($checkViaPing === true) {
1401 23
      $ping = $this->ping();
1402
    }
1403 23
1404 1
    if ($ping !== true) {
1405 1
      $this->connected = false;
1406
      $this->connect();
1407 23
    }
1408 23
1409 23
    return $this->isReady();
1410
  }
1411 23
1412 2
  /**
1413 2
   * Execute a "replace"-query.
1414
   *
1415 23
   * @param string      $table
1416 23
   * @param array       $data
1417
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1418 23
   *
1419
   * @return false|int <p>false on error</p>
1420
   *
1421
   * @throws QueryException
1422
   */
1423
  public function replace($table, array $data = array(), $databaseName = null)
1424
  {
1425
    // init
1426
    $table = trim($table);
1427
1428 26
    if ($table === '') {
1429
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1430 26
1431 26
      return false;
1432 26
    }
1433 26
1434 26
    if (count($data) === 0) {
1435
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1436 26
1437 26
      return false;
1438
    }
1439 26
1440
    // extracting column names
1441
    $columns = array_keys($data);
1442
    foreach ($columns as $k => $_key) {
1443
      /** @noinspection AlterInForeachInspection */
1444
      $columns[$k] = $this->quote_string($_key);
1445
    }
1446
1447 1
    $columns = implode(',', $columns);
1448
1449 1
    // extracting values
1450
    foreach ($data as $k => $_value) {
1451
      /** @noinspection AlterInForeachInspection */
1452
      $data[$k] = $this->secure($_value);
1453
    }
1454
    $values = implode(',', $data);
1455
1456
    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...
1457
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1458
    }
1459
1460
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1461
1462
    return $this->query($sql);
1463 1
  }
1464
1465
  /**
1466 1
   * Rollback in a transaction and end the transaction.
1467
   *
1468 1
   * @return bool <p>Boolean true on success, false otherwise.</p>
1469 1
   */
1470 View Code Duplication
  public function rollback()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1471 1
  {
1472
    if ($this->_in_transaction === false) {
1473
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1474 1
      return false;
1475 1
    }
1476
1477 1
    $return = \mysqli_rollback($this->link);
1478
    \mysqli_autocommit($this->link, true);
1479
    $this->_in_transaction = false;
1480
1481 1
    return $return;
1482 1
  }
1483
1484 1
1485 1
  /**
1486
   * Commits the current transaction and end the transaction.
1487 1
   *
1488
   * @return bool <p>Boolean true on success, false otherwise.</p>
1489
   */
1490 1 View Code Duplication
  public function commit()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1491
  {
1492 1
    if ($this->_in_transaction === false) {
1493 1
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1494 1
      return false;
1495
    }
1496 1
1497
    $return = mysqli_commit($this->link);
1498
    \mysqli_autocommit($this->link, true);
1499
    $this->_in_transaction = false;
1500 1
1501
    return $return;
1502 1
  }
1503
1504
  /**
1505
   * Execute a callback inside a transaction.
1506
   *
1507
   * @param callback $callback The callback to run inside the transaction
1508
   *
1509
   * @return bool Boolean true on success, false otherwise
1510
   */
1511
  public function transact($callback)
1512
  {
1513
    try {
1514
      $this->beginTransaction();
1515
      call_user_func($callback, $this);
1516
      return $this->commit();
1517 6
    } catch (\Exception $e) {
1518
      $this->rollback();
1519
      return false;
1520 6
    }
1521
  }
1522 6
1523 1
  /**
1524
   * Try to secure a variable, so can you use it in sql-queries.
1525 1
   *
1526
   * <p>
1527
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1528 6
   * 1. parse into "int"
1529 2
   * </p><br />
1530
   *
1531 2
   * <p>
1532
   * <strong>float:</strong><br />
1533
   * 1. return "float"
1534 6
   * </p><br />
1535
   *
1536 6
   * <p>
1537 2
   * <strong>string:</strong><br />
1538 6
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1539 4
   * 2. trim whitespace<br />
1540 4
   * 3. trim '<br />
1541 1
   * 4. escape the string (and remove non utf-8 chars)<br />
1542
   * 5. trim ' again (because we maybe removed some chars)<br />
1543
   * 6. add ' around the new string<br />
1544 6
   * </p><br />
1545
   *
1546
   * <p>
1547
   * <strong>array:</strong><br />
1548 6
   * 1. return null
1549
   * </p><br />
1550 6
   *
1551
   * <p>
1552
   * <strong>object:</strong><br />
1553
   * 1. return false
1554
   * </p><br />
1555
   *
1556
   * <p>
1557
   * <strong>null:</strong><br />
1558
   * 1. return null
1559
   * </p>
1560
   *
1561
   * @param mixed $var
1562
   *
1563
   * @return mixed
1564 2
   */
1565
  public function secure($var)
1566
  {
1567 2
    if (
1568
        $var === ''
1569 2
        ||
1570 1
        ($this->_convert_null_to_empty_string === true && $var === null)
1571
    ) {
1572 1
      return "''";
1573
    }
1574
1575 2
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1576 1
      return $var;
1577 2
    }
1578 2
1579 2
    if (is_string($var)) {
1580 1
      $var = trim(trim($var), "'");
1581
    }
1582
1583 2
    $var = $this->escape($var, false, false, null);
1584
1585
    if (is_string($var)) {
1586
      $var = "'" . trim($var, "'") . "'";
1587 2
    }
1588
1589 2
    return $var;
1590
  }
1591
1592
  /**
1593
   * Execute a "select"-query.
1594
   *
1595
   * @param string       $table
1596
   * @param string|array $where
1597
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1598
   *
1599
   * @return false|Result <p>false on error</p>
1600
   *
1601
   * @throws QueryException
1602
   */
1603 20 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...
1604
  {
1605
    // init
1606 20
    $table = trim($table);
1607
1608 20
    if ($table === '') {
1609 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1610
1611 1
      return false;
1612
    }
1613
1614 20
    if (is_string($where)) {
1615 5
      $WHERE = $this->escape($where, false);
1616 20
    } elseif (is_array($where)) {
1617 16
      $WHERE = $this->_parseArrayPair($where, 'AND');
1618 16
    } else {
1619 1
      $WHERE = '';
1620
    }
1621
1622 20
    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...
1623
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1624
    }
1625
1626 20
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1627
1628 20
    return $this->query($sql);
1629
  }
1630
1631
  /**
1632
   * Set the current charset.
1633
   *
1634
   * @param string $charset
1635
   *
1636 1
   * @return bool
1637
   */
1638 1
  public function set_charset($charset)
1639
  {
1640 1
    $charsetLower = strtolower($charset);
1641
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1642
      $charset = 'utf8';
1643
    }
1644
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1645
      $charset = 'utf8mb4';
1646 9
    }
1647
1648 9
    $this->charset = (string)$charset;
1649
1650
    $return = mysqli_set_charset($this->link, $charset);
1651
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1652
    /** @noinspection UsageOfSilenceOperatorInspection */
1653
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1654
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1655
    /** @noinspection UsageOfSilenceOperatorInspection */
1656
    @\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...
1657
1658
    return $return;
1659
  }
1660
1661
  /**
1662
   * Set the option to convert null to "''" (empty string).
1663
   *
1664
   * Used in secure() => select(), insert(), update(), delete()
1665
   *
1666 2
   * @param $bool
1667
   */
1668 2
  public function set_convert_null_to_empty_string($bool)
1669
  {
1670 2
    $this->_convert_null_to_empty_string = (bool)$bool;
1671 2
  }
1672 2
1673 2
  /**
1674
   * Enables or disables internal report functions
1675
   *
1676
   * @link http://php.net/manual/en/function.mysqli-report.php
1677
   *
1678
   * @param int $flags <p>
1679
   *                   <table>
1680
   *                   Supported flags
1681
   *                   <tr valign="top">
1682
   *                   <td>Name</td>
1683
   *                   <td>Description</td>
1684
   *                   </tr>
1685
   *                   <tr valign="top">
1686
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1687
   *                   <td>Turns reporting off</td>
1688
   *                   </tr>
1689 2
   *                   <tr valign="top">
1690
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1691 2
   *                   <td>Report errors from mysqli function calls</td>
1692 2
   *                   </tr>
1693
   *                   <tr valign="top">
1694
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1695
   *                   <td>
1696
   *                   Throw <b>mysqli_sql_exception</b> for errors
1697
   *                   instead of warnings
1698
   *                   </td>
1699
   *                   </tr>
1700
   *                   <tr valign="top">
1701
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1702
   *                   <td>Report if no index or bad index was used in a query</td>
1703
   *                   </tr>
1704
   *                   <tr valign="top">
1705
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1706
   *                   <td>Set all options (report all)</td>
1707
   *                   </tr>
1708
   *                   </table>
1709
   *                   </p>
1710
   *
1711
   * @return bool
1712
   */
1713
  public function set_mysqli_report($flags)
1714
  {
1715
    return \mysqli_report($flags);
1716
  }
1717
1718
  /**
1719
   * Show config errors by throw exceptions.
1720
   *
1721
   * @return bool
1722
   *
1723
   * @throws \InvalidArgumentException
1724
   */
1725
  public function showConfigError()
1726
  {
1727
1728
    if (
1729
        !$this->hostname
1730
        ||
1731
        !$this->username
1732
        ||
1733
        !$this->database
1734
    ) {
1735
1736
      if (!$this->hostname) {
1737
        throw new \InvalidArgumentException('no-sql-hostname');
1738
      }
1739
1740
      if (!$this->username) {
1741
        throw new \InvalidArgumentException('no-sql-username');
1742
      }
1743
1744
      if (!$this->database) {
1745
        throw new \InvalidArgumentException('no-sql-database');
1746
      }
1747
1748
      return false;
1749
    }
1750
1751
    return true;
1752
  }
1753
1754
  /**
1755
   * alias: "beginTransaction()"
1756
   */
1757
  public function startTransaction()
1758
  {
1759
    $this->beginTransaction();
1760
  }
1761
1762
  /**
1763
   * Execute a "update"-query.
1764
   *
1765
   * @param string       $table
1766
   * @param array        $data
1767
   * @param array|string $where
1768
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1769
   *
1770
   * @return false|int <p>false on error</p>
1771
   *
1772
   * @throws QueryException
1773
   */
1774
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1775
  {
1776
    // init
1777
    $table = trim($table);
1778
1779
    if ($table === '') {
1780
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1781
1782
      return false;
1783
    }
1784
1785
    if (count($data) === 0) {
1786
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1787
1788
      return false;
1789
    }
1790
1791
    $SET = $this->_parseArrayPair($data);
1792
1793
    if (is_string($where)) {
1794
      $WHERE = $this->escape($where, false);
1795
    } elseif (is_array($where)) {
1796
      $WHERE = $this->_parseArrayPair($where, 'AND');
1797
    } else {
1798
      $WHERE = '';
1799
    }
1800
1801
    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...
1802
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1803
    }
1804
1805
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1806
1807
    return $this->query($sql);
1808
  }
1809
1810
}
1811