Completed
Push — master ( d6adcd...059279 )
by Lars
97:47 queued 96:10
created

DB::getErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 1
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace voku\db;
4
5
use voku\cache\Cache;
6
use voku\db\exceptions\DBConnectException;
7
use voku\db\exceptions\DBGoneAwayException;
8
use voku\db\exceptions\QueryException;
9
use voku\helper\UTF8;
10
11
/**
12
 * DB: this handles DB queries via MySQLi
13
 *
14
 * @package voku\db
15
 */
16
final class DB
17
{
18
19
  /**
20
   * @var int
21
   */
22
  public $query_count = 0;
23
24
  /**
25
   * @var \mysqli
26
   */
27
  private $link = false;
28
29
  /**
30
   * @var bool
31
   */
32
  private $connected = false;
33
34
  /**
35
   * @var array
36
   */
37
  private $mysqlDefaultTimeFunctions;
38
39
  /**
40
   * @var string
41
   */
42
  private $hostname = '';
43
44
  /**
45
   * @var string
46
   */
47
  private $username = '';
48
49
  /**
50
   * @var string
51
   */
52
  private $password = '';
53
54
  /**
55
   * @var string
56
   */
57
  private $database = '';
58
59
  /**
60
   * @var int
61
   */
62
  private $port = 3306;
63
64
  /**
65
   * @var string
66
   */
67
  private $charset = 'utf8';
68
69
  /**
70
   * @var string
71
   */
72
  private $socket = '';
73
74
  /**
75
   * @var bool
76
   */
77
  private $session_to_db = false;
78
79
  /**
80
   * @var bool
81
   */
82
  private $_in_transaction = false;
83
84
  /**
85
   * @var bool
86
   */
87
  private $_convert_null_to_empty_string = false;
88
89
  /**
90
   * @var bool
91
   */
92
  private $_ssl = false;
93
94
  /**
95
   * The path name to the key file
96
   *
97
   * @var string
98
   */
99
  private $_clientkey;
100
101
  /**
102
   * The path name to the certificate file
103
   *
104
   * @var string
105
   */
106
  private $_clientcert;
107
108
  /**
109 10
   * The path name to the certificate authority file
110
   *
111 10
   * @var string
112
   */
113 10
  private $_cacert;
114
115 10
  /**
116 10
   * @var Debug
117 10
   */
118 10
  private $_debug;
119 10
120 10
  /**
121 10
   * __construct()
122 10
   *
123 10
   * @param string         $hostname
124 10
   * @param string         $username
125 10
   * @param string         $password
126
   * @param string         $database
127 10
   * @param int            $port
128
   * @param string         $charset
129 7
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
130
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
131 4
   * @param string         $logger_class_name
132
   * @param string         $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
133 4
   * @param array          $extra_config  <p>
134
   *                                      'session_to_db' => false|true<br>
135 4
   *                                      'socket' => 'string (path)'<br>
136
   *                                      '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
   * @param null|string $sql
217
   * @param array       $bindings
218 10
   *
219
   * @return bool|int|Result|DB           <p>
220 10
   *                                      "DB" by "$sql" === null<br />
221
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
222
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
223
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
224
   *                                      "true" by e.g. "DROP"-queries<br />
225
   *                                      "false" on error
226
   *                                      </p>
227
   */
228
  public function __invoke($sql = null, array $bindings = array())
229
  {
230 10
    return isset($sql) ? $this->query($sql, $bindings) : $this;
231
  }
232
233
  /**
234 10
   * __wakeup
235 10
   *
236 9
   * @return void
237 9
   */
238 8
  public function __wakeup()
239 10
  {
240
    $this->reconnect();
241 3
  }
242 1
243
  /**
244
   * Load the config from the constructor.
245 2
   *
246 1
   * @param string         $hostname
247
   * @param string         $username
248
   * @param string         $password
249 1
   * @param string         $database
250 1
   * @param int|string     $port          <p>default is (int)3306</p>
251
   * @param string         $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
252
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
253
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
254
   * @param string         $logger_class_name
255
   * @param string         $logger_level
256 7
   * @param array          $extra_config  <p>
257
   *                                      'session_to_db' => false|true<br>
258
   *                                      'socket' => 'string (path)'<br>
259
   *                                      'ssl' => 'bool'<br>
260
   *                                      'clientkey' => 'string (path)'<br>
261
   *                                      'clientcert' => 'string (path)'<br>
262
   *                                      'cacert' => 'string (path)'<br>
263
   *                                      </p>
264
   *
265
   * @return bool
266 9
   */
267
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
268 9
  {
269 1
    $this->hostname = (string)$hostname;
270
    $this->username = (string)$username;
271
    $this->password = (string)$password;
272 9
    $this->database = (string)$database;
273
274 9
    if ($charset) {
275
      $this->charset = (string)$charset;
276 9
    }
277 9
278 9
    if ($port) {
279
      $this->port = (int)$port;
280
    } else {
281 9
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
282 9
      /** @noinspection UsageOfSilenceOperatorInspection */
283 9
      $this->port = (int)@ini_get('mysqli.default_port');
284 9
    }
285 9
286 9
    // fallback
287 9
    if (!$this->port) {
288 9
      $this->port = 3306;
289 9
    }
290 9
291 3
    if (!$this->socket) {
292 3
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
293 3
      $this->socket = @ini_get('mysqli.default_socket');
294
    }
295 6
296
    if ($exit_on_error === true || $exit_on_error === false) {
297 6
      $this->_debug->setExitOnError($exit_on_error);
298
    }
299
300
    if ($echo_on_error === true || $echo_on_error === false) {
301
      $this->_debug->setEchoOnError($echo_on_error);
302
    }
303
304 6
    $this->_debug->setLoggerClassName($logger_class_name);
305
    $this->_debug->setLoggerLevel($logger_level);
306 6
307
    if (is_array($extra_config) === true) {
308
309
      if (isset($extra_config['session_to_db'])) {
310
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
311
      }
312
313
      if (isset($extra_config['socket'])) {
314 45
        $this->socket = $extra_config['socket'];
315
      }
316 45
317
      if (isset($extra_config['ssl'])) {
318
        $this->_ssl = $extra_config['ssl'];
319
      }
320
321
      if (isset($extra_config['clientkey'])) {
322
        $this->_clientkey = $extra_config['clientkey'];
323
      }
324
325
      if (isset($extra_config['clientcert'])) {
326 2
        $this->_clientcert = $extra_config['clientcert'];
327
      }
328 2
329
      if (isset($extra_config['cacert'])) {
330
        $this->_cacert = $extra_config['cacert'];
331
      }
332
333
    } else {
334
      // only for backward compatibility
335
      $this->session_to_db = (boolean)$extra_config;
336
    }
337
338
    return $this->showConfigError();
339
  }
340
341
  /**
342
   * Parses arrays with value pairs and generates SQL to use in queries.
343
   *
344
   * @param array  $arrayPair
345
   * @param string $glue <p>This is the separator.</p>
346
   *
347
   * @return string
348
   *
349
   * @internal
350
   */
351
  public function _parseArrayPair($arrayPair, $glue = ',')
352
  {
353
    // init
354
    $sql = '';
355
356
    /** @noinspection IsEmptyFunctionUsageInspection */
357
    if (empty($arrayPair)) {
358
      return '';
359
    }
360
361
    $arrayPairCounter = 0;
362
    foreach ($arrayPair as $_key => $_value) {
363
      $_connector = '=';
364
      $_glueHelper = '';
365
      $_key_upper = strtoupper($_key);
366
367
      if (strpos($_key_upper, ' NOT') !== false) {
368
        $_connector = 'NOT';
369
      }
370
371
      if (strpos($_key_upper, ' IS') !== false) {
372
        $_connector = 'IS';
373
      }
374
375
      if (strpos($_key_upper, ' IS NOT') !== false) {
376
        $_connector = 'IS NOT';
377
      }
378
379
      if (strpos($_key_upper, ' IN') !== false) {
380
        $_connector = 'IN';
381
      }
382
383 57
      if (strpos($_key_upper, ' NOT IN') !== false) {
384
        $_connector = 'NOT IN';
385
      }
386
387
      if (strpos($_key_upper, ' BETWEEN') !== false) {
388 57
        $_connector = 'BETWEEN';
389
      }
390
391
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
392
        $_connector = 'NOT BETWEEN';
393 57
      }
394
395
      if (strpos($_key_upper, ' LIKE') !== false) {
396 57
        $_connector = 'LIKE';
397 57
      }
398 11
399 57
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
400 11
        $_connector = 'NOT LIKE';
401
      }
402
403 57 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
404 57
        $_connector = '>';
405 57
      }
406
407 57 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
408 10
        $_connector = '<';
409 10
      }
410 10
411 10
      if (strpos($_key_upper, ' >=') !== false) {
412 10
        $_connector = '>=';
413 10
      }
414 10
415 10
      if (strpos($_key_upper, ' <=') !== false) {
416 10
        $_connector = '<=';
417 10
      }
418 10
419
      if (strpos($_key_upper, ' <>') !== false) {
420 10
        $_connector = '<>';
421
      }
422 4
423 1
      if (strpos($_key_upper, ' OR') !== false) {
424 1
        $_glueHelper = 'OR';
425 4
      }
426
427 57
      if (strpos($_key_upper, ' AND') !== false) {
428
        $_glueHelper = 'AND';
429
      }
430
431
      if (is_array($_value) === true) {
432
        foreach ($_value as $oldKey => $oldValue) {
433
          $_value[$oldKey] = $this->secure($oldValue);
434
        }
435
436
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
437
          $_value = '(' . implode(',', $_value) . ')';
438
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
439
          $_value = '(' . implode(' AND ', $_value) . ')';
440
        }
441
442
      } else {
443
        $_value = $this->secure($_value);
444
      }
445
446
      $quoteString = $this->quote_string(
447
          trim(
448
              str_ireplace(
449
                  array(
450 35
                      $_connector,
451
                      $_glueHelper,
452 35
                  ),
453
                  '',
454
                  $_key
455
              )
456 35
          )
457 4
      );
458
459 4
      if (!is_array($_value)) {
460
        $_value = array($_value);
461
      }
462
463
      if (!$_glueHelper) {
464 33
        $_glueHelper = $glue;
465 3
      }
466 33
467 3
      $tmpCounter = 0;
468 33
      foreach ($_value as $valueInner) {
469 3
470 3
        $_glueHelperInner = $_glueHelper;
471
472 33
        if ($arrayPairCounter === 0) {
473 33
474 33
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
475
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
476 33
          } elseif ($tmpCounter === 0) {
477
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
478 33
          }
479 33
480 28
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
481 28
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
482 24
        }
483
484
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
485 33
        $tmpCounter++;
486
      }
487
488 27
      if ($_glueHelper === 'OR') {
489
        $sql .= ' ) ';
490
      }
491 27
492
      $arrayPairCounter++;
493
    }
494
495 25
    return $sql;
496
  }
497
498 23
  /**
499 22
   * _parseQueryParams
500 22
   *
501
   * @param string $sql
502 22
   * @param array  $params
503
   *
504
   * @return string
505
   */
506 8
  private function _parseQueryParams($sql, array $params)
507 8
  {
508 8
    // is there anything to parse?
509
    if (strpos($sql, '?') === false) {
510 8
      return $sql;
511
    }
512
513
    if (count($params) > 0) {
514
      $parseKey = md5(uniqid((string)mt_rand(), true));
515
      $sql = str_replace('?', $parseKey, $sql);
516
517
      $k = 0;
518
      while (strpos($sql, $parseKey) !== false) {
519
        $value = $this->secure($params[$k]);
520 8
        $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 520 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...
521
        $k++;
522 8
      }
523
    }
524
525
    return $sql;
526
  }
527
528
  /**
529
   * Gets the number of affected rows in a previous MySQL operation.
530
   *
531
   * @return int
532
   */
533 3
  public function affected_rows()
534
  {
535
    return \mysqli_affected_rows($this->link);
536 3
  }
537
538
  /**
539
   * Begins a transaction, by turning off auto commit.
540 3
   *
541 3
   * @return bool <p>This will return true or false indicating success of transaction</p>
542 3
   */
543 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...
544 3
  {
545 3
    if ($this->_in_transaction === true) {
546 3
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
547 3
548 3
      return false;
549 3
    }
550 3
551
    $this->clearErrors(); // needed for "$this->endTransaction()"
552 3
    $this->_in_transaction = true;
553
    $return = \mysqli_autocommit($this->link, false);
554
    if ($return === false) {
555
      $this->_in_transaction = false;
556
    }
557
558
    return $return;
559
  }
560
561
  /**
562
   * Clear the errors in "_debug->_errors".
563
   *
564
   * @return bool
565
   */
566
  public function clearErrors()
567
  {
568
    return $this->_debug->clearErrors();
569
  }
570
571
  /**
572
   * Closes a previously opened database connection.
573
   */
574
  public function close()
575
  {
576
    $this->connected = false;
577
578
    if ($this->link) {
579
      \mysqli_close($this->link);
580
    }
581
  }
582
583
  /**
584
   * Commits the current transaction and end the transaction.
585
   *
586
   * @return bool <p>Boolean true on success, false otherwise.</p>
587
   */
588 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...
589
  {
590
    if ($this->_in_transaction === false) {
591
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
592
593
      return false;
594
    }
595
596
    $return = mysqli_commit($this->link);
597 26
    \mysqli_autocommit($this->link, true);
598
    $this->_in_transaction = false;
599
600
    return $return;
601 26
  }
602 26
603 26
  /**
604 1
   * Open a new connection to the MySQL server.
605
   *
606
   * @return bool
607 26
   *
608 1
   * @throws DBConnectException
609
   */
610
  public function connect()
611 26
  {
612 22
    if ($this->isReady()) {
613 22
      return true;
614
    }
615 26
616
    $flags = null;
617 26
618 22
    \mysqli_report(MYSQLI_REPORT_STRICT);
619 22
    try {
620
      $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...
621 26
622
      if (Helper::isMysqlndIsUsed() === true) {
623
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
624
      }
625
626
      if ($this->_ssl === true) {
627
628
        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...
629
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
630
        }
631
632
        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...
633
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
634
        }
635
636
        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...
637
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
638
        }
639
640
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
641 33
642
        \mysqli_ssl_set(
643 33
            $this->link,
644 2
            $this->_clientkey,
645
            $this->_clientcert,
646
            $this->_cacert,
647 33
            null,
648 2
            null
649
        );
650
651
        $flags = MYSQLI_CLIENT_SSL;
652 33
      }
653 33
654 33
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
655
      $this->connected = @\mysqli_real_connect(
656
          $this->link,
657
          $this->hostname,
658 33
          $this->username,
659
          $this->password,
660 33
          $this->database,
661 33
          $this->port,
662
          $this->socket,
663 33
          $flags
664 33
      );
665 33
666 33
    } catch (\Exception $e) {
667
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
668 33
      $this->_debug->displayError($error, false);
669 33
      throw new DBConnectException($error, 100, $e);
670
    }
671
    \mysqli_report(MYSQLI_REPORT_OFF);
672
673 24
    $errno = mysqli_connect_errno();
674
    if (!$this->connected || $errno) {
675
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
676 33
      $this->_debug->displayError($error, false);
677
      throw new DBConnectException($error, 101);
678
    }
679
680 5
    $this->set_charset($this->charset);
681
682
    return $this->isReady();
683 33
  }
684
685
  /**
686
   * Execute a "delete"-query.
687 3
   *
688 3
   * @param string       $table
689
   * @param string|array $where
690
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
691 1
   *
692 1
   * @return false|int <p>false on error</p>
693
   *
694 1
   *    * @throws QueryException
695 1
   */
696 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...
697
  {
698 1
    // init
699 1
    $table = trim($table);
700
701 1
    if ($table === '') {
702 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
703
704 1
      return false;
705
    }
706
707 1
    if (is_string($where)) {
708
      $WHERE = $this->escape($where, false);
709
    } elseif (is_array($where)) {
710
      $WHERE = $this->_parseArrayPair($where, 'AND');
711 33
    } else {
712
      $WHERE = '';
713
    }
714 3
715 3
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
716 3
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
717 3
    }
718 33
719
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
720
721
    return $this->query($sql);
722 33
  }
723
724 33
  /**
725 9
   * Ends a transaction and commits if no errors, then ends autocommit.
726 9
   *
727
   * @return bool <p>This will return true or false indicating success of transactions.</p>
728 33
   */
729 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...
730 1
  {
731 1
    if ($this->_in_transaction === false) {
732
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
733 33
734
      return false;
735 33
    }
736
737 33
    if (!$this->errors()) {
738
      $return = \mysqli_commit($this->link);
739
    } else {
740
      $this->rollback();
741 3
      $return = false;
742
    }
743
744
    \mysqli_autocommit($this->link, true);
745
    $this->_in_transaction = false;
746 3
747
    return $return;
748
  }
749
750
  /**
751
   * Get all errors from "$this->_errors".
752 2
   *
753
   * @return array|false <p>false === on errors</p>
754
   */
755
  public function errors()
756
  {
757
    $errors = $this->_debug->getErrors();
758
759
    return count($errors) > 0 ? $errors : false;
760
  }
761 35
762
  /**
763 35
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
764
   *
765
   * @param mixed     $var           boolean: convert into "integer"<br />
766
   *                                 int: int (don't change it)<br />
767
   *                                 float: float (don't change it)<br />
768
   *                                 null: null (don't change it)<br />
769
   *                                 array: run escape() for every key => value<br />
770
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
771 22
   * @param bool      $stripe_non_utf8
772
   * @param bool      $html_entity_decode
773 22
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
774
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
775
   *                                 <strong>null</strong> => Convert the array into null, every time.
776
   *
777
   * @return mixed
778
   */
779
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
780
  {
781 8
    if ($var === '') {
782
      return '';
783 8
    }
784
785
    if ($var === null) {
786
      return null;
787
    }
788
789
    // save the current value as int (for later usage)
790
    if (!is_object($var)) {
791
      $varInt = (int)$var;
792
    }
793
794
    /** @noinspection TypeUnsafeComparisonInspection */
795
    if (
796
        is_int($var)
797
        ||
798 9
        is_bool($var)
799
        ||
800 9
        (
801 1
            isset($varInt, $var[0])
802
            &&
803
            $var[0] != '0'
804 1
            &&
805
            "$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...
806
        )
807
    ) {
808
809 1
      // "int" || int || bool
810
811
      return (int)$var;
812 1
    }
813 1
814
    if (is_float($var)) {
815
816 1
      // float
817
818
      return $var;
819 8
    }
820
821
    if (is_array($var)) {
822 8
823
      // array
824 8
825
      if ($convert_array === null) {
826
        return null;
827
      }
828
829
      $varCleaned = array();
830
      foreach ((array)$var as $key => $value) {
831
832
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
833
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
834 3
835
        /** @noinspection OffsetOperationsInspection */
836 3
        $varCleaned[$key] = $value;
837
      }
838 3
839 2
      if ($convert_array === true) {
840 2
        $varCleaned = implode(',', $varCleaned);
841
842 3
        return $varCleaned;
843 3
      }
844 3
845 3
      return (array)$varCleaned;
846
    }
847 3
848
    if (
849
        is_string($var)
850
        ||
851
        (
852
            is_object($var)
853
            &&
854
            method_exists($var, '__toString')
855
        )
856 3
    ) {
857
858
      // "string"
859 3
860 3
      $var = (string)$var;
861 3
862 3
      if ($stripe_non_utf8 === true) {
863
        $var = UTF8::cleanup($var);
864
      }
865 3
866
      if ($html_entity_decode === true) {
867
        // use no-html-entity for db
868
        $var = UTF8::html_entity_decode($var);
869
      }
870
871
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
872
873
      $var = \mysqli_real_escape_string($this->getLink(), $var);
874
875
      return (string)$var;
876
877
    }
878
879
    if ($var instanceof \DateTime) {
880
881
      // "DateTime"-object
882
883
      try {
884
        return $this->escape($var->format('Y-m-d H:i:s'), false);
885
      } catch (\Exception $e) {
886 3
        return null;
887
      }
888
889 3
    } else {
890 3
      return false;
891
    }
892 3
  }
893 1
894 1
  /**
895
   * Execute select/insert/update/delete sql-queries.
896
   *
897 1
   * @param string $query    <p>sql-query</p>
898 1
   * @param bool   $useCache <p>use cache?</p>
899 1
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
900 1
   * @param DB     $db       optional <p>the database connection</p>
901 1
   *
902
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
903
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
904 1
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
905 3
   *               "true" by e.g. "DROP"-queries<br />
906
   *               "false" on error
907
   *
908 3
   * @throws QueryException
909
   */
910 3
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
911
  {
912 1
    // init
913
    $cacheKey = null;
914
    if (!$db) {
915
      $db = self::getInstance();
916
    }
917 1
918 View Code Duplication
    if ($useCache === true) {
919 1
      $cache = new Cache(null, null, false, $useCache);
920
      $cacheKey = 'sql-' . md5($query);
921 1
922 1
      if (
923 1
          $cache->getCacheIsReady() === true
924 1
          &&
925 1
          $cache->existsItem($cacheKey)
926
      ) {
927 1
        return $cache->getItem($cacheKey);
928 2
      }
929
930
    } else {
931 3
      $cache = false;
932
    }
933
934
    $result = $db->query($query);
935
936
    if ($result instanceof Result) {
937
938
      $return = $result->fetchAllArray();
939 1
940
      // save into the cache
941 1 View Code Duplication
      if (
942
          $cacheKey !== null
943
          &&
944
          $useCache === true
945
          &&
946
          $cache instanceof Cache
947
          &&
948
          $cache->getCacheIsReady() === true
949
      ) {
950
        $cache->setItem($cacheKey, $return, $cacheTTL);
951
      }
952
953
    } else {
954
      $return = $result;
955
    }
956
957
    return $return;
958
  }
959
960
  /**
961
   * Get all table-names via "SHOW TABLES".
962
   *
963
   * @return array
964
   */
965
  public function getAllTables()
966
  {
967
    $query = 'SHOW TABLES';
968
    $result = $this->query($query);
969
970
    return $result->fetchAllArray();
971
  }
972
973
  /**
974
   * @return Debug
975
   */
976
  public function getDebugger()
977
  {
978
    return $this->_debug;
979
  }
980
981
  /**
982
   * Get errors from "$this->_errors".
983
   *
984
   * @return array
985
   */
986
  public function getErrors()
987
  {
988
    return $this->_debug->getErrors();
989
  }
990
991
  /**
992
   * getInstance()
993
   *
994
   * @param string      $hostname
995
   * @param string      $username
996 7
   * @param string      $password
997
   * @param string      $database
998 7
   * @param int|string  $port            <p>default is (int)3306</p>
999 7
   * @param string      $charset         <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1000 5
   * @param bool|string $exit_on_error   <p>Use a empty string "" or false to disable it.</p>
1001 5
   * @param bool|string $echo_on_error   <p>Use a empty string "" or false to disable it.</p>
1002 7
   * @param string      $logger_class_name
1003 5
   * @param string      $logger_level
1004 5
   * @param array       $extra_config    <p>
1005
   *                                     'session_to_db' => false|true<br>
1006 7
   *                                     'socket' => 'string (path)'<br>
1007
   *                                     'ssl' => 'bool'<br>
1008 7
   *                                     'clientkey' => 'string (path)'<br>
1009
   *                                     'clientcert' => 'string (path)'<br>
1010
   *                                     'cacert' => 'string (path)'<br>
1011 7
   *                                     </p>
1012
   *
1013
   * @return \voku\db\DB
1014 7
   */
1015
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $extra_config = array())
1016 7
  {
1017
    /**
1018
     * @var $instance DB[]
1019
     */
1020
    static $instance = array();
1021
1022
    /**
1023
     * @var $firstInstance DB
1024
     */
1025
    static $firstInstance = null;
1026 1
1027
    if (
1028 1
        $hostname . $username . $password . $database . $port . $charset == ''
1029 1
        &&
1030
        null !== $firstInstance
1031
    ) {
1032
      return $firstInstance;
1033
    }
1034
1035
    $extra_config_string = '';
1036 1
    if (is_array($extra_config) === true) {
1037
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1038 1
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1039 1
      }
1040
    } else {
1041 1
      // only for backward compatibility
1042
      $extra_config_string = (int)$extra_config;
1043
    }
1044
1045
    $connection = md5(
1046
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1047
    );
1048
1049
    if (!isset($instance[$connection])) {
1050
      $instance[$connection] = new self(
1051
          $hostname,
1052
          $username,
1053
          $password,
1054
          $database,
1055
          $port,
1056 1
          $charset,
1057
          $exit_on_error,
1058 1
          $echo_on_error,
1059
          $logger_class_name,
1060
          $logger_level,
1061
          $extra_config
1062 1
      );
1063 1
1064
      if (null === $firstInstance) {
1065 1
        $firstInstance = $instance[$connection];
1066
      }
1067
    }
1068 1
1069 1
    return $instance[$connection];
1070 1
  }
1071
1072 1
  /**
1073
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1074 1
   *
1075 1
   * @return \mysqli
1076 1
   */
1077
  public function getLink()
1078 1
  {
1079
    return $this->link;
1080 1
  }
1081 1
1082 1
  /**
1083 1
   * Get the current charset.
1084 1
   *
1085
   * @return string
1086
   */
1087 1
  public function get_charset()
1088 1
  {
1089 1
    return $this->charset;
1090
  }
1091
1092
  /**
1093 1
   * Check if we are in a transaction.
1094
   *
1095 1
   * @return bool
1096
   */
1097
  public function inTransaction()
1098
  {
1099
    return $this->_in_transaction;
1100
  }
1101
1102
  /**
1103
   * Execute a "insert"-query.
1104
   *
1105
   * @param string      $table
1106
   * @param array       $data
1107
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1108 1
   *
1109 1
   * @return false|int <p>false on error</p>
1110
   *
1111
   * @throws QueryException
1112 1
   */
1113 1
  public function insert($table, array $data = array(), $databaseName = null)
1114
  {
1115
    // init
1116
    $table = trim($table);
1117
1118
    if ($table === '') {
1119
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1120
1121
      return false;
1122 1
    }
1123
1124 1
    if (count($data) === 0) {
1125 1
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1126
1127
      return false;
1128
    }
1129
1130
    $SET = $this->_parseArrayPair($data);
1131
1132 4
    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...
1133
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1134 4
    }
1135
1136 4
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1137 1
1138
    return $this->query($sql);
1139 1
  }
1140
1141
  /**
1142 4
   * Returns the auto generated id used in the last query.
1143
   *
1144
   * @return int|string
1145
   */
1146
  public function insert_id()
1147
  {
1148 4
    return \mysqli_insert_id($this->link);
1149 4
  }
1150
1151 4
  /**
1152
   * Check if db-connection is ready.
1153
   *
1154
   * @return boolean
1155
   */
1156
  public function isReady()
1157
  {
1158
    return $this->connected ? true : false;
1159 4
  }
1160
1161 4
  /**
1162
   * Get the last sql-error.
1163
   *
1164
   * @return string|false <p>false === there was no error</p>
1165
   */
1166
  public function lastError()
1167
  {
1168
    $errors = $this->_debug->getErrors();
1169 4
1170
    return count($errors) > 0 ? end($errors) : false;
1171 4
  }
1172
1173
  /**
1174
   * Execute a sql-multi-query.
1175
   *
1176
   * @param string $sql
1177
   *
1178
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1179 2
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1180
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1181
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1182 2
   *
1183 1
   * @throws QueryException
1184 1
   */
1185 1
  public function multi_query($sql)
1186 1
  {
1187 1
    if (!$this->isReady()) {
1188
      return false;
1189
    }
1190 2
1191 2 View Code Duplication
    if (!$sql || $sql === '') {
1192
      $this->_debug->displayError('Can not execute an empty query.', false);
1193 2
1194
      return false;
1195
    }
1196
1197
    $query_start_time = microtime(true);
1198
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1199
    $query_duration = microtime(true) - $query_start_time;
1200
1201 2
    $this->_debug->logQuery($sql, $query_duration, 0);
1202
1203 2
    $returnTheResult = false;
1204
    $result = array();
1205 2
1206
    if ($resultTmp) {
1207
      do {
1208
1209
        $resultTmpInner = \mysqli_store_result($this->link);
1210
1211 2
        if ($resultTmpInner instanceof \mysqli_result) {
1212
1213
          $returnTheResult = true;
1214 2
          $result[] = new Result($sql, $resultTmpInner);
1215
1216 2
        } else {
1217 2
1218 2
          // is the query successful
1219 2
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1220 2
            $result[] = true;
1221
          } else {
1222 2
            $result[] = false;
1223
          }
1224
1225
        }
1226
1227
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1228
1229
    } else {
1230
1231
      // log the error query
1232
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1233
1234
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1235
    }
1236 21
1237
    // return the result only if there was a "SELECT"-query
1238
    if ($returnTheResult === true) {
1239 21
      return $result;
1240
    }
1241 21
1242 2
    if (
1243
        count($result) > 0
1244 2
        &&
1245
        in_array(false, $result, true) === false
1246
    ) {
1247 20
      return true;
1248 3
    }
1249
1250 3
    return false;
1251
  }
1252
1253 18
  /**
1254
   * Pings a server connection, or tries to reconnect
1255 18
   * if the connection has gone down.
1256
   *
1257
   * @return boolean
1258
   */
1259 18
  public function ping()
1260
  {
1261 18
    if (
1262
        $this->link
1263
        &&
1264
        $this->link instanceof \mysqli
1265
    ) {
1266
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1267
      /** @noinspection UsageOfSilenceOperatorInspection */
1268
      return (bool)@\mysqli_ping($this->link);
1269
    }
1270
1271
    return false;
1272
  }
1273
1274 23
  /**
1275
   * Get a new "Prepare"-Object for your sql-query.
1276
   *
1277 23
   * @param string $query
1278
   *
1279
   * @return Prepare
1280 23
   */
1281
  public function prepare($query)
1282
  {
1283
    return new Prepare($this, $query);
1284 23
  }
1285 23
1286 23
  /**
1287 23
   * Execute a sql-query and return the result-array for select-statements.
1288 23
   *
1289
   * @param string $query
1290 23
   *
1291 2
   * @return mixed
1292 2
   * @deprecated
1293
   * @throws \Exception
1294 23
   */
1295 1
  public static function qry($query)
1296 1
  {
1297
    $db = self::getInstance();
1298 23
1299 1
    $args = func_get_args();
1300 1
    /** @noinspection SuspiciousAssignmentsInspection */
1301
    $query = array_shift($args);
1302 23
    $query = str_replace('?', '%s', $query);
1303 1
    $args = array_map(
1304 1
        array(
1305
            $db,
1306 23
            'escape',
1307 1
        ),
1308 1
        $args
1309
    );
1310 23
    array_unshift($args, $query);
1311 1
    $query = call_user_func_array('sprintf', $args);
1312 1
    $result = $db->query($query);
1313
1314 23
    if ($result instanceof Result) {
1315 1
      return $result->fetchAllArray();
1316 1
    }
1317
1318 23
    return $result;
1319 2
  }
1320 2
1321
  /**
1322 23
   * Execute a sql-query.
1323 2
   *
1324 2
   * @param string        $sql            <p>The sql query-string.</p>
1325
   *
1326 23
   * @param array|boolean $params         <p>
1327 4
   *                                      "array" of sql-query-parameters<br/>
1328 4
   *                                      "false" if you don't need any parameter (default)<br/>
1329
   *                                      </p>
1330 23
   *
1331 1
   * @return bool|int|Result              <p>
1332 1
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1333
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1334 23
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1335 4
   *                                      "true" by e.g. "DROP"-queries<br />
1336 4
   *                                      "false" on error
1337
   *                                      </p>
1338 23
   *
1339 1
   * @throws QueryException
1340 1
   */
1341
  public function query($sql = '', $params = false)
1342 23
  {
1343 1
    if (!$this->isReady()) {
1344 1
      return false;
1345
    }
1346 23
1347 2 View Code Duplication
    if (!$sql || $sql === '') {
1348 2
      $this->_debug->displayError('Can not execute an empty query.', false);
1349
1350 23
      return false;
1351 1
    }
1352 1
1353
    if (
1354 23
        $params !== false
1355 2
        &&
1356 2
        is_array($params)
1357 2
        &&
1358
        count($params) > 0
1359 2
    ) {
1360 1
      $sql = $this->_parseQueryParams($sql, $params);
1361 2
    }
1362 1
1363 1
    $query_start_time = microtime(true);
1364
    $query_result = \mysqli_real_query($this->link, $sql);
1365 2
    $query_duration = microtime(true) - $query_start_time;
1366 23
1367
    $this->query_count++;
1368
1369 23
    $mysqli_field_count = \mysqli_field_count($this->link);
1370 23
    if ($mysqli_field_count) {
1371 23
      $result = \mysqli_store_result($this->link);
1372
    } else {
1373 23
      $result = $query_result;
1374 23
    }
1375 23
1376 23
    if ($result instanceof \mysqli_result) {
1377
1378 23
      // log the select query
1379 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 1360 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...
1380 23
1381
      // return query result object
1382 23
      return new Result($sql, $result);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1360 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...
1383 23
    }
1384 23
1385
    if ($query_result === true) {
1386 23
1387 23
      // "INSERT" || "REPLACE"
1388 23 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1389
        $insert_id = (int)$this->insert_id();
1390 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 1360 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...
1391 23
1392
        return $insert_id;
1393 23
      }
1394
1395 23
      // "UPDATE" || "DELETE"
1396 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1397 23
        $affected_rows = (int)$this->affected_rows();
1398 1
        $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 1360 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...
1399 23
1400 23
        return $affected_rows;
1401 23
      }
1402
1403 23
      // log the ? query
1404 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 1360 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...
1405 1
1406
      return true;
1407 23
    }
1408 23
1409 23
    // log the error query
1410
    $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 1360 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...
1411 23
1412 2
    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 1360 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...
1413 2
  }
1414
1415 23
  /**
1416 23
   * Error-handling for the sql-query.
1417
   *
1418 23
   * @param string     $errorMessage
1419
   * @param int        $errorNumber
1420
   * @param string     $sql
1421
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1422
   * @param bool       $sqlMultiQuery
1423
   *
1424
   * @throws QueryException
1425
   * @throws DBGoneAwayException
1426
   *
1427
   * @return bool
1428 26
   */
1429
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1430 26
  {
1431 26
    $errorNumber = (int)$errorNumber;
1432 26
1433 26
    if (
1434 26
        $errorMessage === 'DB server has gone away'
1435
        ||
1436 26
        $errorMessage === 'MySQL server has gone away'
1437 26
        ||
1438
        $errorNumber === 2006
1439 26
    ) {
1440
      static $RECONNECT_COUNTER;
1441
1442
      // exit if we have more then 3 "DB server has gone away"-errors
1443
      if ($RECONNECT_COUNTER > 3) {
1444
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1445
        throw new DBGoneAwayException($errorMessage);
1446
      }
1447 1
1448
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1449 1
1450
      // reconnect
1451
      $RECONNECT_COUNTER++;
1452
      $this->reconnect(true);
1453
1454
      // re-run the current (non multi) query
1455
      if ($sqlMultiQuery === false) {
1456
        return $this->query($sql, $sqlParams);
1457
      }
1458
1459
      return false;
1460
    }
1461
1462
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1463 1
1464
    // this query returned an error, we must display it (only for dev) !!!
1465
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql);
1466 1
1467
    return false;
1468 1
  }
1469 1
1470
  /**
1471 1
   * Quote && Escape e.g. a table name string.
1472
   *
1473
   * @param string $str
1474 1
   *
1475 1
   * @return string
1476
   */
1477 1
  public function quote_string($str)
1478
  {
1479
    $str = str_replace(
1480
        '`',
1481 1
        '``',
1482 1
        trim(
1483
            $this->escape($str, false),
1484 1
            '`'
1485 1
        )
1486
    );
1487 1
1488
    return '`' . $str . '`';
1489
  }
1490 1
1491
  /**
1492 1
   * Reconnect to the MySQL-Server.
1493 1
   *
1494 1
   * @param bool $checkViaPing
1495
   *
1496 1
   * @return bool
1497
   */
1498
  public function reconnect($checkViaPing = false)
1499
  {
1500 1
    $ping = false;
1501
1502 1
    if ($checkViaPing === true) {
1503
      $ping = $this->ping();
1504
    }
1505
1506
    if ($ping !== true) {
1507
      $this->connected = false;
1508
      $this->connect();
1509
    }
1510
1511
    return $this->isReady();
1512
  }
1513
1514
  /**
1515
   * Execute a "replace"-query.
1516
   *
1517 6
   * @param string      $table
1518
   * @param array       $data
1519
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1520 6
   *
1521
   * @return false|int <p>false on error</p>
1522 6
   *
1523 1
   * @throws QueryException
1524
   */
1525 1
  public function replace($table, array $data = array(), $databaseName = null)
1526
  {
1527
    // init
1528 6
    $table = trim($table);
1529 2
1530
    if ($table === '') {
1531 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1532
1533
      return false;
1534 6
    }
1535
1536 6
    if (count($data) === 0) {
1537 2
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1538 6
1539 4
      return false;
1540 4
    }
1541 1
1542
    // extracting column names
1543
    $columns = array_keys($data);
1544 6
    foreach ($columns as $k => $_key) {
1545
      /** @noinspection AlterInForeachInspection */
1546
      $columns[$k] = $this->quote_string($_key);
1547
    }
1548 6
1549
    $columns = implode(',', $columns);
1550 6
1551
    // extracting values
1552
    foreach ($data as $k => $_value) {
1553
      /** @noinspection AlterInForeachInspection */
1554
      $data[$k] = $this->secure($_value);
1555
    }
1556
    $values = implode(',', $data);
1557
1558
    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...
1559
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1560
    }
1561
1562
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1563
1564 2
    return $this->query($sql);
1565
  }
1566
1567 2
  /**
1568
   * Rollback in a transaction and end the transaction.
1569 2
   *
1570 1
   * @return bool <p>Boolean true on success, false otherwise.</p>
1571
   */
1572 1 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...
1573
  {
1574
    if ($this->_in_transaction === false) {
1575 2
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1576 1
1577 2
      return false;
1578 2
    }
1579 2
1580 1
    $return = \mysqli_rollback($this->link);
1581
    \mysqli_autocommit($this->link, true);
1582
    $this->_in_transaction = false;
1583 2
1584
    return $return;
1585
  }
1586
1587 2
  /**
1588
   * Try to secure a variable, so can you use it in sql-queries.
1589 2
   *
1590
   * <p>
1591
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1592
   * 1. parse into "int"
1593
   * </p><br />
1594
   *
1595
   * <p>
1596
   * <strong>float:</strong><br />
1597
   * 1. return "float"
1598
   * </p><br />
1599
   *
1600
   * <p>
1601
   * <strong>string:</strong><br />
1602
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1603 20
   * 2. trim whitespace<br />
1604
   * 3. trim '<br />
1605
   * 4. escape the string (and remove non utf-8 chars)<br />
1606 20
   * 5. trim ' again (because we maybe removed some chars)<br />
1607
   * 6. add ' around the new string<br />
1608 20
   * </p><br />
1609 1
   *
1610
   * <p>
1611 1
   * <strong>array:</strong><br />
1612
   * 1. return null
1613
   * </p><br />
1614 20
   *
1615 5
   * <p>
1616 20
   * <strong>object:</strong><br />
1617 16
   * 1. return false
1618 16
   * </p><br />
1619 1
   *
1620
   * <p>
1621
   * <strong>null:</strong><br />
1622 20
   * 1. return null
1623
   * </p>
1624
   *
1625
   * @param mixed $var
1626 20
   *
1627
   * @return mixed
1628 20
   */
1629
  public function secure($var)
1630
  {
1631
    if (
1632
        $var === ''
1633
        ||
1634
        ($this->_convert_null_to_empty_string === true && $var === null)
1635
    ) {
1636 1
      return "''";
1637
    }
1638 1
1639
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1640 1
      return $var;
1641
    }
1642
1643
    if (is_string($var)) {
1644
      $var = trim(trim($var), "'");
1645
    }
1646 9
1647
    $var = $this->escape($var, false, false, null);
1648 9
1649
    if (is_string($var)) {
1650
      $var = "'" . trim($var, "'") . "'";
1651
    }
1652
1653
    return $var;
1654
  }
1655
1656
  /**
1657
   * Execute a "select"-query.
1658
   *
1659
   * @param string       $table
1660
   * @param string|array $where
1661
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1662
   *
1663
   * @return false|Result <p>false on error</p>
1664
   *
1665
   * @throws QueryException
1666 2
   */
1667 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...
1668 2
  {
1669
    // init
1670 2
    $table = trim($table);
1671 2
1672 2
    if ($table === '') {
1673 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1674
1675
      return false;
1676
    }
1677
1678
    if (is_string($where)) {
1679
      $WHERE = $this->escape($where, false);
1680
    } elseif (is_array($where)) {
1681
      $WHERE = $this->_parseArrayPair($where, 'AND');
1682
    } else {
1683
      $WHERE = '';
1684
    }
1685
1686
    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...
1687
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1688
    }
1689 2
1690
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1691 2
1692 2
    return $this->query($sql);
1693
  }
1694
1695
  /**
1696
   * Selects a different database than the one specified on construction.
1697
   *
1698
   * @param string $database <p>Database name to switch to.</p>
1699
   *
1700
   * @return bool <p>Boolean true on success, false otherwise.</p>
1701
   */
1702
  public function select_db($database)
1703
  {
1704
    if (!$this->isReady()) {
1705
      return false;
1706
    }
1707
1708
    return mysqli_select_db($this->link, $database);
1709
  }
1710
1711
  /**
1712
   * Set the current charset.
1713
   *
1714
   * @param string $charset
1715
   *
1716
   * @return bool
1717
   */
1718
  public function set_charset($charset)
1719
  {
1720
    $charsetLower = strtolower($charset);
1721
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1722
      $charset = 'utf8';
1723
    }
1724
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1725
      $charset = 'utf8mb4';
1726
    }
1727
1728
    $this->charset = (string)$charset;
1729
1730
    $return = mysqli_set_charset($this->link, $charset);
1731
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1732
    /** @noinspection UsageOfSilenceOperatorInspection */
1733
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1734
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1735
    /** @noinspection UsageOfSilenceOperatorInspection */
1736
    @\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...
1737
1738
    return $return;
1739
  }
1740
1741
  /**
1742
   * Set the option to convert null to "''" (empty string).
1743
   *
1744
   * Used in secure() => select(), insert(), update(), delete()
1745
   *
1746
   * @param $bool
1747
   */
1748
  public function set_convert_null_to_empty_string($bool)
1749
  {
1750
    $this->_convert_null_to_empty_string = (bool)$bool;
1751
  }
1752
1753
  /**
1754
   * Enables or disables internal report functions
1755
   *
1756
   * @link http://php.net/manual/en/function.mysqli-report.php
1757
   *
1758
   * @param int $flags <p>
1759
   *                   <table>
1760
   *                   Supported flags
1761
   *                   <tr valign="top">
1762
   *                   <td>Name</td>
1763
   *                   <td>Description</td>
1764
   *                   </tr>
1765
   *                   <tr valign="top">
1766
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1767
   *                   <td>Turns reporting off</td>
1768
   *                   </tr>
1769
   *                   <tr valign="top">
1770
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1771
   *                   <td>Report errors from mysqli function calls</td>
1772
   *                   </tr>
1773
   *                   <tr valign="top">
1774
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1775
   *                   <td>
1776
   *                   Throw <b>mysqli_sql_exception</b> for errors
1777
   *                   instead of warnings
1778
   *                   </td>
1779
   *                   </tr>
1780
   *                   <tr valign="top">
1781
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1782
   *                   <td>Report if no index or bad index was used in a query</td>
1783
   *                   </tr>
1784
   *                   <tr valign="top">
1785
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1786
   *                   <td>Set all options (report all)</td>
1787
   *                   </tr>
1788
   *                   </table>
1789
   *                   </p>
1790
   *
1791
   * @return bool
1792
   */
1793
  public function set_mysqli_report($flags)
1794
  {
1795
    return \mysqli_report($flags);
1796
  }
1797
1798
  /**
1799
   * Show config errors by throw exceptions.
1800
   *
1801
   * @return bool
1802
   *
1803
   * @throws \InvalidArgumentException
1804
   */
1805
  public function showConfigError()
1806
  {
1807
1808
    if (
1809
        !$this->hostname
1810
        ||
1811
        !$this->username
1812
        ||
1813
        !$this->database
1814
    ) {
1815
1816
      if (!$this->hostname) {
1817
        throw new \InvalidArgumentException('no-sql-hostname');
1818
      }
1819
1820
      if (!$this->username) {
1821
        throw new \InvalidArgumentException('no-sql-username');
1822
      }
1823
1824
      if (!$this->database) {
1825
        throw new \InvalidArgumentException('no-sql-database');
1826
      }
1827
1828
      return false;
1829
    }
1830
1831
    return true;
1832
  }
1833
1834
  /**
1835
   * alias: "beginTransaction()"
1836
   */
1837
  public function startTransaction()
1838
  {
1839
    $this->beginTransaction();
1840
  }
1841
1842
  /**
1843
   * Execute a callback inside a transaction.
1844
   *
1845
   * @param callback $callback The callback to run inside the transaction
1846
   *
1847
   * @return bool Boolean true on success, false otherwise
1848
   */
1849
  public function transact($callback)
1850
  {
1851
    try {
1852
      $this->beginTransaction();
1853
      call_user_func($callback, $this);
1854
1855
      return $this->commit();
1856
    } catch (\Exception $e) {
1857
      $this->rollback();
1858
1859
      return false;
1860
    }
1861
  }
1862
1863
  /**
1864
   * Execute a "update"-query.
1865
   *
1866
   * @param string       $table
1867
   * @param array        $data
1868
   * @param array|string $where
1869
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1870
   *
1871
   * @return false|int <p>false on error</p>
1872
   *
1873
   * @throws QueryException
1874
   */
1875
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1876
  {
1877
    // init
1878
    $table = trim($table);
1879
1880
    if ($table === '') {
1881
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1882
1883
      return false;
1884
    }
1885
1886
    if (count($data) === 0) {
1887
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1888
1889
      return false;
1890
    }
1891
1892
    $SET = $this->_parseArrayPair($data);
1893
1894
    if (is_string($where)) {
1895
      $WHERE = $this->escape($where, false);
1896
    } elseif (is_array($where)) {
1897
      $WHERE = $this->_parseArrayPair($where, 'AND');
1898
    } else {
1899
      $WHERE = '';
1900
    }
1901
1902
    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...
1903
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1904
    }
1905
1906
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1907
1908
    return $this->query($sql);
1909
  }
1910
1911
}
1912