Completed
Push — master ( af652f...7d670a )
by Lars
01:53
created

DB::__invoke()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

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

    return array();
}

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

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

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
594
595
      if (Helper::isMysqlndIsUsed() === true) {
596
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
597 26
      }
598
599
      if ($this->_ssl === true) {
600
601 26
        if (empty($this->clientcert)) {
0 ignored issues
show
Bug introduced by
The property clientcert does not seem to exist. Did you mean _clientcert?

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

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

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

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

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

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

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

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

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

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

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

    return array();
}

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

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

Loading history...
1370 23
1371 23
        return $insert_id;
1372
      }
1373 23
1374 23
      // "UPDATE" || "DELETE"
1375 23 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1376 23
        $affected_rows = (int)$this->affected_rows();
1377
        $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 1338 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...
1378 23
1379 23
        return $affected_rows;
1380 23
      }
1381
1382 23
      // log the ? query
1383 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 1338 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...
1384 23
1385
      return true;
1386 23
    }
1387 23
1388 23
    // log the error query
1389
    $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 1338 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...
1390 23
1391 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 1338 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...
1392
  }
1393 23
1394
  /**
1395 23
   * Error-handling for the sql-query.
1396
   *
1397 23
   * @param string     $errorMessage
1398 1
   * @param int        $errorNumber
1399 23
   * @param string     $sql
1400 23
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1401 23
   * @param bool       $sqlMultiQuery
1402
   *
1403 23
   * @throws QueryException
1404 1
   * @throws DBGoneAwayException
1405 1
   *
1406
   * @return bool
1407 23
   */
1408 23
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1409 23
  {
1410
    $errorNumber = (int)$errorNumber;
1411 23
1412 2
    if (
1413 2
        $errorMessage === 'DB server has gone away'
1414
        ||
1415 23
        $errorMessage === 'MySQL server has gone away'
1416 23
        ||
1417
        $errorNumber === 2006
1418 23
    ) {
1419
      static $RECONNECT_COUNTER;
1420
1421
      // exit if we have more then 3 "DB server has gone away"-errors
1422
      if ($RECONNECT_COUNTER > 3) {
1423
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql, 5);
1424
        throw new DBGoneAwayException($errorMessage);
1425
      }
1426
1427
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1428 26
1429
      // reconnect
1430 26
      $RECONNECT_COUNTER++;
1431 26
      $this->reconnect(true);
1432 26
1433 26
      // re-run the current (non multi) query
1434 26
      if ($sqlMultiQuery === false) {
1435
        return $this->query($sql, $sqlParams);
1436 26
      }
1437 26
1438
      return false;
1439 26
    }
1440
1441
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1442
1443
    // this query returned an error, we must display it (only for dev) !!!
1444
    $this->_debug->displayError($errorMessage . '(' . $errorNumber. ') ' . ' | ' . $sql);
1445
1446
    return false;
1447 1
  }
1448
1449 1
  /**
1450
   * Quote && Escape e.g. a table name string.
1451
   *
1452
   * @param string $str
1453
   *
1454
   * @return string
1455
   */
1456
  public function quote_string($str)
1457
  {
1458
    $str = str_replace(
1459
        '`',
1460
        '``',
1461
        trim(
1462
            $this->escape($str, false),
1463 1
            '`'
1464
        )
1465
    );
1466 1
1467
    return '`' . $str . '`';
1468 1
  }
1469 1
1470
  /**
1471 1
   * Reconnect to the MySQL-Server.
1472
   *
1473
   * @param bool $checkViaPing
1474 1
   *
1475 1
   * @return bool
1476
   */
1477 1
  public function reconnect($checkViaPing = false)
1478
  {
1479
    $ping = false;
1480
1481 1
    if ($checkViaPing === true) {
1482 1
      $ping = $this->ping();
1483
    }
1484 1
1485 1
    if ($ping !== true) {
1486
      $this->connected = false;
1487 1
      $this->connect();
1488
    }
1489
1490 1
    return $this->isReady();
1491
  }
1492 1
1493 1
  /**
1494 1
   * Execute a "replace"-query.
1495
   *
1496 1
   * @param string      $table
1497
   * @param array       $data
1498
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1499
   *
1500 1
   * @return false|int <p>false on error</p>
1501
   *
1502 1
   * @throws QueryException
1503
   */
1504
  public function replace($table, array $data = array(), $databaseName = null)
1505
  {
1506
    // init
1507
    $table = trim($table);
1508
1509
    if ($table === '') {
1510
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1511
1512
      return false;
1513
    }
1514
1515
    if (count($data) === 0) {
1516
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1517 6
1518
      return false;
1519
    }
1520 6
1521
    // extracting column names
1522 6
    $columns = array_keys($data);
1523 1
    foreach ($columns as $k => $_key) {
1524
      /** @noinspection AlterInForeachInspection */
1525 1
      $columns[$k] = $this->quote_string($_key);
1526
    }
1527
1528 6
    $columns = implode(',', $columns);
1529 2
1530
    // extracting values
1531 2
    foreach ($data as $k => $_value) {
1532
      /** @noinspection AlterInForeachInspection */
1533
      $data[$k] = $this->secure($_value);
1534 6
    }
1535
    $values = implode(',', $data);
1536 6
1537 2
    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...
1538 6
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1539 4
    }
1540 4
1541 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1542
1543
    return $this->query($sql);
1544 6
  }
1545
1546
  /**
1547
   * Rollback in a transaction and end the transaction.
1548 6
   *
1549
   * @return bool <p>Boolean true on success, false otherwise.</p>
1550 6
   */
1551
  public function rollback()
1552
  {
1553
    if ($this->_in_transaction === false) {
1554
      return false;
1555
    }
1556
1557
    $return = \mysqli_rollback($this->link);
1558
    \mysqli_autocommit($this->link, true);
1559
    $this->_in_transaction = false;
1560
1561
    return $return;
1562
  }
1563
1564 2
  /**
1565
   * Commits the current transaction and end the transaction.
1566
   *
1567 2
   * @return bool <p>Boolean true on success, false otherwise.</p>
1568
   */
1569 2
  public function commit()
1570 1
  {
1571
    if ($this->_in_transaction === false) {
1572 1
      return false;
1573
    }
1574
1575 2
    if (mysqli_commit($this->link)) {
1576 1
      $this->_in_transaction = false;
1577 2
      return true;
1578 2
    }
1579 2
1580 1
    return false;
1581
  }
1582
1583 2
  /**
1584
   * Execute a callback inside a transaction.
1585
   *
1586
   * @param callback $callback The callback to run inside the transaction
1587 2
   *
1588
   * @return bool Boolean true on success, false otherwise
1589 2
   */
1590
  public function transact($callback)
1591
  {
1592
    try {
1593
      $this->beginTransaction();
1594
      call_user_func($callback, $this);
1595
      return $this->commit();
1596
    } catch (\Exception $e) {
1597
      $this->rollback();
1598
      return false;
1599
    }
1600
  }
1601
1602
  /**
1603 20
   * Try to secure a variable, so can you use it in sql-queries.
1604
   *
1605
   * <p>
1606 20
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1607
   * 1. parse into "int"
1608 20
   * </p><br />
1609 1
   *
1610
   * <p>
1611 1
   * <strong>float:</strong><br />
1612
   * 1. return "float"
1613
   * </p><br />
1614 20
   *
1615 5
   * <p>
1616 20
   * <strong>string:</strong><br />
1617 16
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1618 16
   * 2. trim whitespace<br />
1619 1
   * 3. trim '<br />
1620
   * 4. escape the string (and remove non utf-8 chars)<br />
1621
   * 5. trim ' again (because we maybe removed some chars)<br />
1622 20
   * 6. add ' around the new string<br />
1623
   * </p><br />
1624
   *
1625
   * <p>
1626 20
   * <strong>array:</strong><br />
1627
   * 1. return null
1628 20
   * </p><br />
1629
   *
1630
   * <p>
1631
   * <strong>object:</strong><br />
1632
   * 1. return false
1633
   * </p><br />
1634
   *
1635
   * <p>
1636 1
   * <strong>null:</strong><br />
1637
   * 1. return null
1638 1
   * </p>
1639
   *
1640 1
   * @param mixed $var
1641
   *
1642
   * @return mixed
1643
   */
1644
  public function secure($var)
1645
  {
1646 9
    if (
1647
        $var === ''
1648 9
        ||
1649
        ($this->_convert_null_to_empty_string === true && $var === null)
1650
    ) {
1651
      return "''";
1652
    }
1653
1654
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1655
      return $var;
1656
    }
1657
1658
    if (is_string($var)) {
1659
      $var = trim(trim($var), "'");
1660
    }
1661
1662
    $var = $this->escape($var, false, false, null);
1663
1664
    if (is_string($var)) {
1665
      $var = "'" . trim($var, "'") . "'";
1666 2
    }
1667
1668 2
    return $var;
1669
  }
1670 2
1671 2
  /**
1672 2
   * Execute a "select"-query.
1673 2
   *
1674
   * @param string       $table
1675
   * @param string|array $where
1676
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1677
   *
1678
   * @return false|Result <p>false on error</p>
1679
   *
1680
   * @throws QueryException
1681
   */
1682 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...
1683
  {
1684
    // init
1685
    $table = trim($table);
1686
1687
    if ($table === '') {
1688
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1689 2
1690
      return false;
1691 2
    }
1692 2
1693
    if (is_string($where)) {
1694
      $WHERE = $this->escape($where, false);
1695
    } elseif (is_array($where)) {
1696
      $WHERE = $this->_parseArrayPair($where, 'AND');
1697
    } else {
1698
      $WHERE = '';
1699
    }
1700
1701
    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...
1702
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1703
    }
1704
1705
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1706
1707
    return $this->query($sql);
1708
  }
1709
1710
  /**
1711
   * Set the current charset.
1712
   *
1713
   * @param string $charset
1714
   *
1715
   * @return bool
1716
   */
1717
  public function set_charset($charset)
1718
  {
1719
    $charsetLower = strtolower($charset);
1720
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1721
      $charset = 'utf8';
1722
    }
1723
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1724
      $charset = 'utf8mb4';
1725
    }
1726
1727
    $this->charset = (string)$charset;
1728
1729
    $return = mysqli_set_charset($this->link, $charset);
1730
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1731
    /** @noinspection UsageOfSilenceOperatorInspection */
1732
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1733
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1734
    /** @noinspection UsageOfSilenceOperatorInspection */
1735
    @\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...
1736
1737
    return $return;
1738
  }
1739
1740
  /**
1741
   * Set the option to convert null to "''" (empty string).
1742
   *
1743
   * Used in secure() => select(), insert(), update(), delete()
1744
   *
1745
   * @param $bool
1746
   */
1747
  public function set_convert_null_to_empty_string($bool)
1748
  {
1749
    $this->_convert_null_to_empty_string = (bool)$bool;
1750
  }
1751
1752
  /**
1753
   * Enables or disables internal report functions
1754
   *
1755
   * @link http://php.net/manual/en/function.mysqli-report.php
1756
   *
1757
   * @param int $flags <p>
1758
   *                   <table>
1759
   *                   Supported flags
1760
   *                   <tr valign="top">
1761
   *                   <td>Name</td>
1762
   *                   <td>Description</td>
1763
   *                   </tr>
1764
   *                   <tr valign="top">
1765
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1766
   *                   <td>Turns reporting off</td>
1767
   *                   </tr>
1768
   *                   <tr valign="top">
1769
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1770
   *                   <td>Report errors from mysqli function calls</td>
1771
   *                   </tr>
1772
   *                   <tr valign="top">
1773
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1774
   *                   <td>
1775
   *                   Throw <b>mysqli_sql_exception</b> for errors
1776
   *                   instead of warnings
1777
   *                   </td>
1778
   *                   </tr>
1779
   *                   <tr valign="top">
1780
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1781
   *                   <td>Report if no index or bad index was used in a query</td>
1782
   *                   </tr>
1783
   *                   <tr valign="top">
1784
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1785
   *                   <td>Set all options (report all)</td>
1786
   *                   </tr>
1787
   *                   </table>
1788
   *                   </p>
1789
   *
1790
   * @return bool
1791
   */
1792
  public function set_mysqli_report($flags)
1793
  {
1794
    return \mysqli_report($flags);
1795
  }
1796
1797
  /**
1798
   * Show config errors by throw exceptions.
1799
   *
1800
   * @return bool
1801
   *
1802
   * @throws \InvalidArgumentException
1803
   */
1804
  public function showConfigError()
1805
  {
1806
1807
    if (
1808
        !$this->hostname
1809
        ||
1810
        !$this->username
1811
        ||
1812
        !$this->database
1813
    ) {
1814
1815
      if (!$this->hostname) {
1816
        throw new \InvalidArgumentException('no-sql-hostname');
1817
      }
1818
1819
      if (!$this->username) {
1820
        throw new \InvalidArgumentException('no-sql-username');
1821
      }
1822
1823
      if (!$this->database) {
1824
        throw new \InvalidArgumentException('no-sql-database');
1825
      }
1826
1827
      return false;
1828
    }
1829
1830
    return true;
1831
  }
1832
1833
  /**
1834
   * alias: "beginTransaction()"
1835
   */
1836
  public function startTransaction()
1837
  {
1838
    $this->beginTransaction();
1839
  }
1840
1841
  /**
1842
   * Execute a "update"-query.
1843
   *
1844
   * @param string       $table
1845
   * @param array        $data
1846
   * @param array|string $where
1847
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1848
   *
1849
   * @return false|int <p>false on error</p>
1850
   *
1851
   * @throws QueryException
1852
   */
1853
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1854
  {
1855
    // init
1856
    $table = trim($table);
1857
1858
    if ($table === '') {
1859
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1860
1861
      return false;
1862
    }
1863
1864
    if (count($data) === 0) {
1865
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1866
1867
      return false;
1868
    }
1869
1870
    $SET = $this->_parseArrayPair($data);
1871
1872
    if (is_string($where)) {
1873
      $WHERE = $this->escape($where, false);
1874
    } elseif (is_array($where)) {
1875
      $WHERE = $this->_parseArrayPair($where, 'AND');
1876
    } else {
1877
      $WHERE = '';
1878
    }
1879
1880
    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...
1881
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1882
    }
1883
1884
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1885
1886
    return $this->query($sql);
1887
  }
1888
1889
  /**
1890
   * @param null|string $sql
1891
   * @param array $bindings
1892
   *
1893
   * @return bool|int|Result|DB           <p>
1894
   *                                      "DB" by "$sql" === null<br />
1895
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1896
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1897
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1898
   *                                      "true" by e.g. "DROP"-queries<br />
1899
   *                                      "false" on error
1900
   *                                      </p>
1901
   */
1902
  //public function __invoke($sql = null, array $bindings = array())
1903
  //{
1904
  //  return isset($sql) ? $this->query($sql, $bindings) : $this;
1905
  //}
1906
1907
}
1908