Completed
Push — master ( 423a40...855ee8 )
by Lars
03:36
created

DB::num_rows()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 2
nop 1
crap 3.0416
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\cache\Cache;
8
use voku\db\exceptions\DBConnectException;
9
use voku\db\exceptions\DBGoneAwayException;
10
use voku\db\exceptions\QueryException;
11
use voku\helper\UTF8;
12
13
/**
14
 * DB: This class can handle DB queries via MySQLi.
15
 *
16
 * @package voku\db
17
 */
18
final class DB
19
{
20
21
  /**
22
   * @var int
23
   */
24
  public $query_count = 0;
25
26
  /**
27
   * @var \mysqli|null
28
   */
29
  private $link;
30
31
  /**
32
   * @var bool
33
   */
34
  private $connected = false;
35
36
  /**
37
   * @var array
38
   */
39
  private $mysqlDefaultTimeFunctions;
40
41
  /**
42
   * @var string
43
   */
44
  private $hostname = '';
45
46
  /**
47
   * @var string
48
   */
49
  private $username = '';
50
51
  /**
52
   * @var string
53
   */
54
  private $password = '';
55
56
  /**
57
   * @var string
58
   */
59
  private $database = '';
60
61
  /**
62
   * @var int
63
   */
64
  private $port = 3306;
65
66
  /**
67
   * @var string
68
   */
69
  private $charset = 'utf8';
70
71
  /**
72
   * @var string
73
   */
74
  private $socket = '';
75
76
  /**
77
   * @var bool
78
   */
79
  private $session_to_db = false;
80
81
  /**
82
   * @var bool
83
   */
84
  private $_in_transaction = false;
85
86
  /**
87
   * @var bool
88
   */
89
  private $_convert_null_to_empty_string = false;
90
91
  /**
92
   * @var bool
93
   */
94
  private $_ssl = false;
95
96
  /**
97
   * The path name to the key file
98
   *
99
   * @var string
100
   */
101
  private $_clientkey;
102
103
  /**
104
   * The path name to the certificate file
105
   *
106
   * @var string
107
   */
108
  private $_clientcert;
109
110
  /**
111
   * The path name to the certificate authority file
112
   *
113
   * @var string
114
   */
115
  private $_cacert;
116
117
  /**
118
   * @var Debug
119
   */
120
  private $_debug;
121
122
  /**
123
   * __construct()
124
   *
125
   * @param string $hostname
126
   * @param string $username
127
   * @param string $password
128
   * @param string $database
129
   * @param int    $port
130
   * @param string $charset
131
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
132
   *                                      Use false to disable it.</p>
133
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
134
   *                                      Use false to disable it.</p>
135
   * @param string $logger_class_name
136
   * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
137
   * @param array  $extra_config          <p>
138
   *                                      'session_to_db' => bool<br>
139
   *                                      'socket'        => 'string (path)'<br>
140
   *                                      'ssl'           => bool<br>
141
   *                                      'clientkey'     => 'string (path)'<br>
142
   *                                      'clientcert'    => 'string (path)'<br>
143
   *                                      'cacert'        => 'string (path)'<br>
144
   *                                      </p>
145
   */
146 11
  private function __construct(string $hostname, string $username, string $password, string $database, $port, string $charset, bool $exit_on_error, bool $echo_on_error, string $logger_class_name, string $logger_level, array $extra_config = [])
147
  {
148 11
    $this->_debug = new Debug($this);
149
150 11
    $this->_loadConfig(
151 11
        $hostname,
152 11
        $username,
153 11
        $password,
154 11
        $database,
155 11
        $port,
156 11
        $charset,
157 11
        $exit_on_error,
158 11
        $echo_on_error,
159 11
        $logger_class_name,
160 11
        $logger_level,
161 11
        $extra_config
162
    );
163
164 8
    $this->connect();
165
166 5
    $this->mysqlDefaultTimeFunctions = [
167
      // Returns the current date.
168
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176
      'CURTIME()',
177
      // Synonym for NOW()
178
      'LOCALTIME()',
179
      // Synonym for NOW()
180
      'LOCALTIMESTAMP()',
181
      // Returns the current date and time.
182
      'NOW()',
183
      // Returns the time at which the function executes.
184
      'SYSDATE()',
185
      // Returns a UNIX timestamp.
186
      'UNIX_TIMESTAMP()',
187
      // Returns the current UTC date.
188
      'UTC_DATE()',
189
      // Returns the current UTC time.
190
      'UTC_TIME()',
191
      // Returns the current UTC date and time.
192
      'UTC_TIMESTAMP()',
193
    ];
194 5
  }
195
196
  /**
197
   * Prevent the instance from being cloned.
198
   *
199
   * @return void
200
   */
201
  private function __clone()
202
  {
203
  }
204
205
  /**
206
   * __destruct
207
   *
208
   */
209
  public function __destruct()
210
  {
211
    // close the connection only if we don't save PHP-SESSION's in DB
212
    if ($this->session_to_db === false) {
213
      $this->close();
214
    }
215
  }
216
217
  /**
218
   * @param null|string $sql
219
   * @param array       $bindings
220
   *
221
   * @return bool|int|Result|DB           <p>
222
   *                                      "DB" by "$sql" === null<br />
223
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
224
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
225
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
226
   *                                      "true" by e.g. "DROP"-queries<br />
227
   *                                      "false" on error
228
   *                                      </p>
229
   */
230 2
  public function __invoke(string $sql = null, array $bindings = [])
231
  {
232 2
    return null !== $sql ? $this->query($sql, $bindings) : $this;
233
  }
234
235
  /**
236
   * __wakeup
237
   *
238
   * @return void
239
   */
240 2
  public function __wakeup()
241
  {
242 2
    $this->reconnect();
243 2
  }
244
245
  /**
246
   * Load the config from the constructor.
247
   *
248
   * @param string $hostname
249
   * @param string $username
250
   * @param string $password
251
   * @param string $database
252
   * @param int    $port                  <p>default is (int)3306</p>
253
   * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
254
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
255
   *                                      Use false to disable it.</p>
256
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
257
   *                                      Use false to disable it.</p>
258
   * @param string $logger_class_name
259
   * @param string $logger_level
260
   * @param array  $extra_config          <p>
261
   *                                      'session_to_db' => false|true<br>
262
   *                                      'socket' => 'string (path)'<br>
263
   *                                      'ssl' => 'bool'<br>
264
   *                                      'clientkey' => 'string (path)'<br>
265
   *                                      'clientcert' => 'string (path)'<br>
266
   *                                      'cacert' => 'string (path)'<br>
267
   *                                      </p>
268
   *
269
   * @return bool
270
   */
271 11
  private function _loadConfig(
272
      string $hostname,
273
      string $username,
274
      string $password,
275
      string $database,
276
      $port,
277
      string $charset,
278
      bool $exit_on_error,
279
      bool $echo_on_error,
280
      string $logger_class_name,
281
      string $logger_level,
282
      array $extra_config = []
283
  ): bool
284
  {
285 11
    $this->hostname = $hostname;
286 11
    $this->username = $username;
287 11
    $this->password = $password;
288 11
    $this->database = $database;
289
290 11
    if ($charset) {
291 5
      $this->charset = $charset;
292
    }
293
294 11
    if ($port) {
295 5
      $this->port = (int)$port;
296
    } else {
297
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
298 7
      $this->port = (int)@ini_get('mysqli.default_port');
299
    }
300
301
    // fallback
302 11
    if (!$this->port) {
303
      $this->port = 3306;
304
    }
305
306 11
    if (!$this->socket) {
307
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
308 11
      $this->socket = @ini_get('mysqli.default_socket');
309
    }
310
311 11
    $this->_debug->setExitOnError($exit_on_error);
312 11
    $this->_debug->setEchoOnError($echo_on_error);
313
314 11
    $this->_debug->setLoggerClassName($logger_class_name);
315 11
    $this->_debug->setLoggerLevel($logger_level);
316
317 11
    if (\is_array($extra_config) === true) {
318
319 11
      $this->setConfigExtra($extra_config);
320
321
    } else {
322
323
      // only for backward compatibility
324
      $this->session_to_db = (boolean)$extra_config;
325
326
    }
327
328 11
    return $this->showConfigError();
329
  }
330
331
  /**
332
   * @param array $extra_config           <p>
333
   *                                      'session_to_db' => false|true<br>
334
   *                                      'socket' => 'string (path)'<br>
335
   *                                      'ssl' => 'bool'<br>
336
   *                                      'clientkey' => 'string (path)'<br>
337
   *                                      'clientcert' => 'string (path)'<br>
338
   *                                      'cacert' => 'string (path)'<br>
339
   *                                      </p>
340
   */
341 11
  public function setConfigExtra(array $extra_config)
342
  {
343 11
    if (isset($extra_config['session_to_db'])) {
344
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
345
    }
346
347 11
    if (isset($extra_config['socket'])) {
348
      $this->socket = $extra_config['socket'];
349
    }
350
351 11
    if (isset($extra_config['ssl'])) {
352
      $this->_ssl = $extra_config['ssl'];
353
    }
354
355 11
    if (isset($extra_config['clientkey'])) {
356
      $this->_clientkey = $extra_config['clientkey'];
357
    }
358
359 11
    if (isset($extra_config['clientcert'])) {
360
      $this->_clientcert = $extra_config['clientcert'];
361
    }
362
363 11
    if (isset($extra_config['cacert'])) {
364
      $this->_cacert = $extra_config['cacert'];
365
    }
366 11
  }
367
368
  /**
369
   * Parses arrays with value pairs and generates SQL to use in queries.
370
   *
371
   * @param array  $arrayPair
372
   * @param string $glue <p>This is the separator.</p>
373
   *
374
   * @return string
375
   *
376
   * @internal
377
   */
378 30
  public function _parseArrayPair(array $arrayPair, string $glue = ','): string
379
  {
380
    // init
381 30
    $sql = '';
382
383 30
    if (\count($arrayPair) === 0) {
384
      return '';
385
    }
386
387 30
    $arrayPairCounter = 0;
388 30
    foreach ($arrayPair as $_key => $_value) {
389 30
      $_connector = '=';
390 30
      $_glueHelper = '';
391 30
      $_key_upper = \strtoupper($_key);
392
393 30
      if (\strpos($_key_upper, ' NOT') !== false) {
394 2
        $_connector = 'NOT';
395
      }
396
397 30
      if (\strpos($_key_upper, ' IS') !== false) {
398 1
        $_connector = 'IS';
399
      }
400
401 30
      if (\strpos($_key_upper, ' IS NOT') !== false) {
402 1
        $_connector = 'IS NOT';
403
      }
404
405 30
      if (\strpos($_key_upper, ' IN') !== false) {
406 1
        $_connector = 'IN';
407
      }
408
409 30
      if (\strpos($_key_upper, ' NOT IN') !== false) {
410 1
        $_connector = 'NOT IN';
411
      }
412
413 30
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
414 1
        $_connector = 'BETWEEN';
415
      }
416
417 30
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
418 1
        $_connector = 'NOT BETWEEN';
419
      }
420
421 30
      if (\strpos($_key_upper, ' LIKE') !== false) {
422 2
        $_connector = 'LIKE';
423
      }
424
425 30
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
426 2
        $_connector = 'NOT LIKE';
427
      }
428
429 30 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
430 4
        $_connector = '>';
431
      }
432
433 30 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
434 1
        $_connector = '<';
435
      }
436
437 30
      if (\strpos($_key_upper, ' >=') !== false) {
438 4
        $_connector = '>=';
439
      }
440
441 30
      if (\strpos($_key_upper, ' <=') !== false) {
442 1
        $_connector = '<=';
443
      }
444
445 30
      if (\strpos($_key_upper, ' <>') !== false) {
446 1
        $_connector = '<>';
447
      }
448
449 30
      if (\strpos($_key_upper, ' OR') !== false) {
450 2
        $_glueHelper = 'OR';
451
      }
452
453 30
      if (\strpos($_key_upper, ' AND') !== false) {
454 1
        $_glueHelper = 'AND';
455
      }
456
457 30
      if (\is_array($_value) === true) {
458 2
        foreach ($_value as $oldKey => $oldValue) {
459 2
          $_value[$oldKey] = $this->secure($oldValue);
460
        }
461
462 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
463 1
          $_value = '(' . \implode(',', $_value) . ')';
464 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
465 2
          $_value = '(' . \implode(' AND ', $_value) . ')';
466
        }
467
468
      } else {
469 30
        $_value = $this->secure($_value);
470
      }
471
472 30
      $quoteString = $this->quote_string(
473 30
          \trim(
474 30
              \str_ireplace(
475
                  [
476 30
                      $_connector,
477 30
                      $_glueHelper,
478
                  ],
479 30
                  '',
480 30
                  $_key
481
              )
482
          )
483
      );
484
485 30
      $_value = (array)$_value;
486
487 30
      if (!$_glueHelper) {
488 30
        $_glueHelper = $glue;
489
      }
490
491 30
      $tmpCounter = 0;
492 30
      foreach ($_value as $valueInner) {
493
494 30
        $_glueHelperInner = $_glueHelper;
495
496 30
        if ($arrayPairCounter === 0) {
497
498 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
499 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
500 30
          } elseif ($tmpCounter === 0) {
501 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
502
          }
503
504 26
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
505 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
506
        }
507
508 30
        if (\is_string($valueInner) && $valueInner === '') {
509
          $valueInner = "''";
510
        }
511
512 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
513 30
        $tmpCounter++;
514
      }
515
516 30
      if ($_glueHelper === 'OR') {
517 2
        $sql .= ' ) ';
518
      }
519
520 30
      $arrayPairCounter++;
521
    }
522
523 30
    return $sql;
524
  }
525
526
  /**
527
   * _parseQueryParams
528
   *
529
   * @param string $sql
530
   * @param array  $params
531
   *
532
   * @return array <p>with the keys -> 'sql', 'params'</p>
533
   */
534 13
  private function _parseQueryParams(string $sql, array $params = []): array
535
  {
536 13
    $offset = \strpos($sql, '?');
537
538
    // is there anything to parse?
539
    if (
540 13
        $offset === false
541
        ||
542 13
        \count($params) === 0
543
    ) {
544 11
      return ['sql' => $sql, 'params' => $params];
545
    }
546
547 3
    foreach ($params as $key => $param) {
548
549 3
      if ($offset === false) {
550 1
        continue;
551
      }
552
553 3
      $replacement = $this->secure($param);
554
555 3
      unset($params[$key]);
556
557 3
      $sql = \substr_replace($sql, $replacement, $offset, 1);
558 3
      $offset = \strpos($sql, '?', $offset + \strlen((string)$replacement));
559
    }
560
561 3
    return ['sql' => $sql, 'params' => $params];
562
  }
563
564
  /**
565
   * Gets the number of affected rows in a previous MySQL operation.
566
   *
567
   * @return int
568
   */
569 12
  public function affected_rows(): int
570
  {
571 12
    return \mysqli_affected_rows($this->link);
572
  }
573
574
  /**
575
   * Begins a transaction, by turning off auto commit.
576
   *
577
   * @return bool <p>This will return true or false indicating success of transaction</p>
578
   */
579 6 View Code Duplication
  public function beginTransaction(): bool
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...
580
  {
581 6
    if ($this->_in_transaction === true) {
582 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
583
584 2
      return false;
585
    }
586
587 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
588 6
    $this->_in_transaction = true;
589 6
    $return = \mysqli_autocommit($this->link, false);
590 6
    if ($return === false) {
591
      $this->_in_transaction = false;
592
    }
593
594 6
    return $return;
595
  }
596
597
  /**
598
   * Clear the errors in "_debug->_errors".
599
   *
600
   * @return bool
601
   */
602 6
  public function clearErrors(): bool
603
  {
604 6
    return $this->_debug->clearErrors();
605
  }
606
607
  /**
608
   * Closes a previously opened database connection.
609
   */
610 2
  public function close(): bool
611
  {
612 2
    $this->connected = false;
613
614
    if (
615 2
        $this->link
616
        &&
617 2
        $this->link instanceof \mysqli
618
    ) {
619 2
      $result = \mysqli_close($this->link);
620 2
      $this->link = null;
621
622 2
      return $result;
623
    }
624
625 1
    return false;
626
  }
627
628
  /**
629
   * Commits the current transaction and end the transaction.
630
   *
631
   * @return bool <p>Boolean true on success, false otherwise.</p>
632
   */
633 2 View Code Duplication
  public function commit(): bool
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...
634
  {
635 2
    if ($this->_in_transaction === false) {
636
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
637
638
      return false;
639
    }
640
641 2
    $return = \mysqli_commit($this->link);
642 2
    \mysqli_autocommit($this->link, true);
643 2
    $this->_in_transaction = false;
644
645 2
    return $return;
646
  }
647
648
  /**
649
   * Open a new connection to the MySQL server.
650
   *
651
   * @return bool
652
   *
653
   * @throws DBConnectException
654
   */
655 10
  public function connect(): bool
656
  {
657 10
    if ($this->isReady()) {
658 1
      return true;
659
    }
660
661 10
    $flags = null;
662
663 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
664
    try {
665 10
      $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>|null 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...
666
667 10
      if (Helper::isMysqlndIsUsed() === true) {
668 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
669
      }
670
671 10
      if ($this->_ssl === true) {
672
673
        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...
674
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
675
        }
676
677
        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...
678
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
679
        }
680
681
        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...
682
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
683
        }
684
685
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
686
687
        /** @noinspection PhpParamsInspection */
688
        \mysqli_ssl_set(
689
            $this->link,
690
            $this->_clientkey,
691
            $this->_clientcert,
692
            $this->_cacert,
693
            null,
694
            null
695
        );
696
697
        $flags = MYSQLI_CLIENT_SSL;
698
      }
699
700
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
701 10
      $this->connected = @\mysqli_real_connect(
702 10
          $this->link,
703 10
          $this->hostname,
704 10
          $this->username,
705 10
          $this->password,
706 10
          $this->database,
707 10
          $this->port,
708 10
          $this->socket,
709 10
          (int)$flags
710
      );
711
712 3
    } catch (\Exception $e) {
713 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
714 3
      $this->_debug->displayError($error, true);
715
      throw new DBConnectException($error, 100, $e);
716
    }
717 7
    \mysqli_report(MYSQLI_REPORT_OFF);
718
719 7
    $errno = \mysqli_connect_errno();
720 7
    if (!$this->connected || $errno) {
721
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
722
      $this->_debug->displayError($error, true);
723
      throw new DBConnectException($error, 101);
724
    }
725
726 7
    $this->set_charset($this->charset);
727
728 7
    return $this->isReady();
729
  }
730
731
  /**
732
   * Execute a "delete"-query.
733
   *
734
   * @param string       $table
735
   * @param string|array $where
736
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
737
   *
738
   * @return false|int <p>false on error</p>
739
   *
740
   *    * @throws QueryException
741
   */
742 2 View Code Duplication
  public function delete(string $table, $where, string $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...
743
  {
744
    // init
745 2
    $table = \trim($table);
746
747 2
    if ($table === '') {
748 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
749
750 1
      return false;
751
    }
752
753 2
    if (\is_string($where)) {
754 1
      $WHERE = $this->escape($where, false);
755 2
    } elseif (\is_array($where)) {
756 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
757
    } else {
758 1
      $WHERE = '';
759
    }
760
761 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...
762
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
763
    }
764
765 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
766
767 2
    return $this->query($sql);
768
  }
769
770
  /**
771
   * Ends a transaction and commits if no errors, then ends autocommit.
772
   *
773
   * @return bool <p>This will return true or false indicating success of transactions.</p>
774
   */
775 4 View Code Duplication
  public function endTransaction(): bool
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...
776
  {
777 4
    if ($this->_in_transaction === false) {
778
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
779
780
      return false;
781
    }
782
783 4
    if (!$this->errors()) {
784 1
      $return = \mysqli_commit($this->link);
785
    } else {
786 3
      $this->rollback();
787 3
      $return = false;
788
    }
789
790 4
    \mysqli_autocommit($this->link, true);
791 4
    $this->_in_transaction = false;
792
793 4
    return $return;
794
  }
795
796
  /**
797
   * Get all errors from "$this->_errors".
798
   *
799
   * @return array|false <p>false === on errors</p>
800
   */
801 4
  public function errors()
802
  {
803 4
    $errors = $this->_debug->getErrors();
804
805 4
    return \count($errors) > 0 ? $errors : false;
806
  }
807
808
  /**
809
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
810
   *
811
   * @param mixed $sql    <p>The SQL string.</p>
812
   * @param array $params <p>An array of key-value bindings.</p>
813
   *
814
   * @return array <p>with the keys -> 'sql', 'params'</p>
815
   */
816 14
  public function _parseQueryParamsByName(string $sql, array $params = []): array
817
  {
818
    // is there anything to parse?
819
    if (
820 14
        \strpos($sql, ':') === false
821
        ||
822 14
        \count($params) === 0
823
    ) {
824 3
      return ['sql' => $sql, 'params' => $params];
825
    }
826
827 12
    $offset = null;
828 12
    $replacement = null;
829 12
    foreach ($params as $name => $param) {
830
831
      // add ":" if needed
832 12
      if (\strpos($name, ':') !== 0) {
833 2
        $nameTmp = ':' . $name;
834
      } else {
835 10
        $nameTmp = $name;
836
      }
837
838 12
      if ($offset === null) {
839 12
        $offset = \strpos($sql, $nameTmp);
840
      } else {
841 11
        $offset = \strpos($sql, $nameTmp, $offset + \strlen((string)$replacement));
842
      }
843
844 12
      if ($offset === false) {
845 1
        continue;
846
      }
847
848 12
      $replacement = $this->secure($param);
849
850 12
      unset($params[$name]);
851
852 12
      $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
853
    }
854
855 12
    return ['sql' => $sql, 'params' => $params];
856
  }
857
858
  /**
859
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
860
   *
861
   * @param mixed     $var           boolean: convert into "integer"<br />
862
   *                                 int: int (don't change it)<br />
863
   *                                 float: float (don't change it)<br />
864
   *                                 null: null (don't change it)<br />
865
   *                                 array: run escape() for every key => value<br />
866
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
867
   * @param bool      $stripe_non_utf8
868
   * @param bool      $html_entity_decode
869
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
870
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
871
   *                                 <strong>null</strong> => Convert the array into null, every time.
872
   *
873
   * @return mixed
874
   */
875 55
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
876
  {
877
    // [empty]
878 55
    if ($var === '') {
879 2
      return '';
880
    }
881
882
    // ''
883 55
    if ($var === "''") {
884
      return "''";
885
    }
886
887
    // NULL
888 55
    if ($var === null) {
889
      if (
890 2
          $this->_convert_null_to_empty_string === true
891
      ) {
892
        return "''";
893
      }
894
895 2
      return 'NULL';
896
    }
897
898
    // save the current value as int (for later usage)
899 55
    if (!\is_object($var)) {
900 55
      $varInt = (int)$var;
901
    }
902
903
    // "int" || int || bool
904
    if (
905 55
        \is_int($var)
906
        ||
907 51
        \is_bool($var)
908
        ||
909
        (
910 51
            isset($varInt, $var[0])
911
            &&
912 51
            $var[0] != '0'
913
            &&
914 55
            (string)$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...
915
        )
916
    ) {
917 32
      return (int)$var;
918
    }
919
920
    // float
921 51
    if (\is_float($var)) {
922 5
      return $var;
923
    }
924
925
    // array
926 51
    if (\is_array($var)) {
927
928 4
      if ($convert_array === null) {
929 3
        return null;
930
      }
931
932 2
      $varCleaned = [];
933 2
      foreach ((array)$var as $key => $value) {
934
935 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
936 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
937
938
        /** @noinspection OffsetOperationsInspection */
939 2
        $varCleaned[$key] = $value;
940
      }
941
942 2
      if ($convert_array === true) {
943 1
        $varCleaned = \implode(',', $varCleaned);
944
945 1
        return $varCleaned;
946
      }
947
948 2
      return $varCleaned;
949
    }
950
951
    // "string"
952
    if (
953 51
        \is_string($var)
954
        ||
955
        (
956 3
            \is_object($var)
957
            &&
958 51
            \method_exists($var, '__toString')
959
        )
960
    ) {
961 51
      $var = (string)$var;
962
963 51
      if ($stripe_non_utf8 === true) {
964 11
        $var = UTF8::cleanup($var);
965
      }
966
967 51
      if ($html_entity_decode === true) {
968 1
        $var = UTF8::html_entity_decode($var);
969
      }
970
971 51
      $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
972
973 51
      $var = \mysqli_real_escape_string($this->getLink(), $var);
974
975 51
      return (string)$var;
976
977
    }
978
979
    // "DateTime"-object
980 3
    if ($var instanceof \DateTime) {
981 3
      return $this->escape($var->format('Y-m-d H:i:s'), false);
982
    }
983
984 2
    return false;
985
  }
986
987
  /**
988
   * Execute select/insert/update/delete sql-queries.
989
   *
990
   * @param string  $query    <p>sql-query</p>
991
   * @param bool    $useCache optional <p>use cache?</p>
992
   * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
993
   * @param DB|null $db       optional <p>the database connection</p>
994
   *
995
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
996
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
997
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
998
   *               "true" by e.g. "DROP"-queries<br />
999
   *               "false" on error
1000
   *
1001
   * @throws QueryException
1002
   */
1003 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, DB $db = null)
1004
  {
1005
    // init
1006 3
    $cacheKey = null;
1007 3
    if (!$db) {
1008 3
      $db = self::getInstance();
1009
    }
1010
1011 3 View Code Duplication
    if ($useCache === true) {
1012 1
      $cache = new Cache(null, null, false, $useCache);
1013 1
      $cacheKey = 'sql-' . \md5($query);
1014
1015
      if (
1016 1
          $cache->getCacheIsReady() === true
1017
          &&
1018 1
          $cache->existsItem($cacheKey)
1019
      ) {
1020 1
        return $cache->getItem($cacheKey);
1021
      }
1022
1023
    } else {
1024 3
      $cache = false;
1025
    }
1026
1027 3
    $result = $db->query($query);
1028
1029 3
    if ($result instanceof Result) {
1030
1031 1
      $return = $result->fetchAllArray();
1032
1033
      // save into the cache
1034 View Code Duplication
      if (
1035 1
          $cacheKey !== null
1036
          &&
1037 1
          $useCache === true
1038
          &&
1039 1
          $cache instanceof Cache
1040
          &&
1041 1
          $cache->getCacheIsReady() === true
1042
      ) {
1043 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1044
      }
1045
1046
    } else {
1047 2
      $return = $result;
1048
    }
1049
1050 3
    return $return;
1051
  }
1052
1053
  /**
1054
   * Get all table-names via "SHOW TABLES".
1055
   *
1056
   * @return array
1057
   */
1058 1
  public function getAllTables(): array
1059
  {
1060 1
    $query = 'SHOW TABLES';
1061 1
    $result = $this->query($query);
1062
1063 1
    return $result->fetchAllArray();
1064
  }
1065
1066
  /**
1067
   * @return Debug
1068
   */
1069 9
  public function getDebugger(): Debug
1070
  {
1071 9
    return $this->_debug;
1072
  }
1073
1074
  /**
1075
   * Get errors from "$this->_errors".
1076
   *
1077
   * @return array
1078
   */
1079 1
  public function getErrors(): array
1080
  {
1081 1
    return $this->_debug->getErrors();
1082
  }
1083
1084
  /**
1085
   * getInstance()
1086
   *
1087
   * @param string $hostname
1088
   * @param string $username
1089
   * @param string $password
1090
   * @param string $database
1091
   * @param int    $port                 <p>default is (int)3306</p>
1092
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1093
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1094
   *                                     Use false to disable it.</p>
1095
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1096
   *                                     Use false to disable it.</p>
1097
   * @param string $logger_class_name
1098
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1099
   * @param array  $extra_config         <p>
1100
   *                                     're_connect'    => bool<br>
1101
   *                                     'session_to_db' => bool<br>
1102
   *                                     'socket'        => 'string (path)'<br>
1103
   *                                     'ssl'           => bool<br>
1104
   *                                     'clientkey'     => 'string (path)'<br>
1105
   *                                     'clientcert'    => 'string (path)'<br>
1106
   *                                     'cacert'        => 'string (path)'<br>
1107
   *                                     </p>
1108
   *
1109
   * @return self
1110
   */
1111 97
  public static function getInstance(
1112
      string $hostname = '',
1113
      string $username = '',
1114
      string $password = '',
1115
      string $database = '',
1116
      $port = 3306,
1117
      string $charset = 'utf8',
1118
      bool $exit_on_error = true,
1119
      bool $echo_on_error = true,
1120
      string $logger_class_name = '',
1121
      string $logger_level = '',
1122
      array $extra_config = []
1123
  ): self
1124
  {
1125
    /**
1126
     * @var $instance self[]
1127
     */
1128 97
    static $instance = [];
1129
1130
    /**
1131
     * @var $firstInstance self
1132
     */
1133 97
    static $firstInstance = null;
1134
1135
    if (
1136 97
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1137
        &&
1138 97
        null !== $firstInstance
1139
    ) {
1140 14
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1141
        $firstInstance->reconnect(true);
1142
      }
1143
1144 14
      return $firstInstance;
1145
    }
1146
1147 97
    $extra_config_string = '';
1148 97
    if (\is_array($extra_config) === true) {
1149 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1150 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1151
      }
1152
    } else {
1153
      // only for backward compatibility
1154
      $extra_config_string = (int)$extra_config;
1155
    }
1156
1157 97
    $connection = \md5(
1158 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1159
    );
1160
1161 97
    if (!isset($instance[$connection])) {
1162 11
      $instance[$connection] = new self(
1163 11
          $hostname,
1164 11
          $username,
1165 11
          $password,
1166 11
          $database,
1167 11
          $port,
1168 11
          $charset,
1169 11
          $exit_on_error,
1170 11
          $echo_on_error,
1171 11
          $logger_class_name,
1172 11
          $logger_level,
1173 11
          $extra_config
1174
      );
1175
1176 5
      if (null === $firstInstance) {
1177 1
        $firstInstance = $instance[$connection];
1178
      }
1179
    }
1180
1181 97
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1182
      $instance[$connection]->reconnect(true);
1183
    }
1184
1185 97
    return $instance[$connection];
1186
  }
1187
1188
  /**
1189
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1190
   *
1191
   * @return \mysqli
1192
   */
1193 55
  public function getLink(): \mysqli
1194
  {
1195 55
    return $this->link;
1196
  }
1197
1198
  /**
1199
   * Get the current charset.
1200
   *
1201
   * @return string
1202
   */
1203 1
  public function get_charset(): string
1204
  {
1205 1
    return $this->charset;
1206
  }
1207
1208
  /**
1209
   * Check if we are in a transaction.
1210
   *
1211
   * @return bool
1212
   */
1213
  public function inTransaction(): bool
1214
  {
1215
    return $this->_in_transaction;
1216
  }
1217
1218
  /**
1219
   * Execute a "insert"-query.
1220
   *
1221
   * @param string      $table
1222
   * @param array       $data
1223
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1224
   *
1225
   * @return false|int <p>false on error</p>
1226
   *
1227
   * @throws QueryException
1228
   */
1229 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1230
  {
1231
    // init
1232 28
    $table = \trim($table);
1233
1234 28
    if ($table === '') {
1235 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1236
1237 2
      return false;
1238
    }
1239
1240 27
    if (\count($data) === 0) {
1241 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1242
1243 3
      return false;
1244
    }
1245
1246 25
    $SET = $this->_parseArrayPair($data);
1247
1248 25
    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...
1249
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1250
    }
1251
1252 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET";
1253
1254 25
    return $this->query($sql);
1255
  }
1256
1257
  /**
1258
   * Returns the auto generated id used in the last query.
1259
   *
1260
   * @return int|string
1261
   */
1262 58
  public function insert_id()
1263
  {
1264 58
    return \mysqli_insert_id($this->link);
1265
  }
1266
1267
  /**
1268
   * Check if db-connection is ready.
1269
   *
1270
   * @return boolean
1271
   */
1272 106
  public function isReady(): bool
1273
  {
1274 106
    return $this->connected ? true : false;
1275
  }
1276
1277
  /**
1278
   * Get the last sql-error.
1279
   *
1280
   * @return string|false <p>false === there was no error</p>
1281
   */
1282 1
  public function lastError()
1283
  {
1284 1
    $errors = $this->_debug->getErrors();
1285
1286 1
    return \count($errors) > 0 ? end($errors) : false;
1287
  }
1288
1289
  /**
1290
   * Execute a sql-multi-query.
1291
   *
1292
   * @param string $sql
1293
   *
1294
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1295
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1296
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1297
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1298
   *
1299
   * @throws QueryException
1300
   */
1301 1
  public function multi_query(string $sql)
1302
  {
1303 1
    if (!$this->isReady()) {
1304
      return false;
1305
    }
1306
1307 1 View Code Duplication
    if (!$sql || $sql === '') {
1308 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1309
1310 1
      return false;
1311
    }
1312
1313 1
    $query_start_time = microtime(true);
1314 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1315 1
    $query_duration = microtime(true) - $query_start_time;
1316
1317 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1318
1319 1
    $returnTheResult = false;
1320 1
    $result = [];
1321
1322 1
    if ($resultTmp) {
1323
      do {
1324
1325 1
        $resultTmpInner = \mysqli_store_result($this->link);
1326
1327 1
        if ($resultTmpInner instanceof \mysqli_result) {
1328
1329 1
          $returnTheResult = true;
1330 1
          $result[] = new Result($sql, $resultTmpInner);
1331
1332
        } else {
1333
1334
          // is the query successful
1335 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1336 1
            $result[] = true;
1337
          } else {
1338
            $result[] = false;
1339
          }
1340
1341
        }
1342
1343 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1344
1345
    } else {
1346
1347
      // log the error query
1348 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1349
1350 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1351
    }
1352
1353
    // return the result only if there was a "SELECT"-query
1354 1
    if ($returnTheResult === true) {
1355 1
      return $result;
1356
    }
1357
1358
    if (
1359 1
        \count($result) > 0
1360
        &&
1361 1
        \in_array(false, $result, true) === false
1362
    ) {
1363 1
      return true;
1364
    }
1365
1366
    return false;
1367
  }
1368
1369
  /**
1370
   * Pings a server connection, or tries to reconnect
1371
   * if the connection has gone down.
1372
   *
1373
   * @return boolean
1374
   */
1375 3
  public function ping(): bool
1376
  {
1377
    if (
1378 3
        $this->link
1379
        &&
1380 3
        $this->link instanceof \mysqli
1381
    ) {
1382
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1383 2
      return (bool)@\mysqli_ping($this->link);
1384
    }
1385
1386 1
    return false;
1387
  }
1388
1389
  /**
1390
   * Get a new "Prepare"-Object for your sql-query.
1391
   *
1392
   * @param string $query
1393
   *
1394
   * @return Prepare
1395
   */
1396 2
  public function prepare(string $query): Prepare
1397
  {
1398 2
    return new Prepare($this, $query);
1399
  }
1400
1401
  /**
1402
   * Execute a sql-query and return the result-array for select-statements.
1403
   *
1404
   * @param string $query
1405
   *
1406
   * @return mixed
1407
   * @deprecated
1408
   * @throws \Exception
1409
   */
1410 1
  public static function qry(string $query)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1411
  {
1412 1
    $db = self::getInstance();
1413
1414 1
    $args = \func_get_args();
1415
    /** @noinspection SuspiciousAssignmentsInspection */
1416 1
    $query = \array_shift($args);
1417 1
    $query = \str_replace('?', '%s', $query);
1418 1
    $args = \array_map(
1419
        [
1420 1
            $db,
1421 1
            'escape',
1422
        ],
1423 1
        $args
1424
    );
1425 1
    \array_unshift($args, $query);
1426 1
    $query = \sprintf(...$args);
1427 1
    $result = $db->query($query);
1428
1429 1
    if ($result instanceof Result) {
1430 1
      return $result->fetchAllArray();
1431
    }
1432
1433 1
    return $result;
1434
  }
1435
1436
  /**
1437
   * Execute a sql-query.
1438
   *
1439
   * @param string        $sql            <p>The sql query-string.</p>
1440
   *
1441
   * @param array|boolean $params         <p>
1442
   *                                      "array" of sql-query-parameters<br/>
1443
   *                                      "false" if you don't need any parameter (default)<br/>
1444
   *                                      </p>
1445
   *
1446
   * @return bool|int|Result              <p>
1447
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1448
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1449
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1450
   *                                      "true" by e.g. "DROP"-queries<br />
1451
   *                                      "false" on error
1452
   *                                      </p>
1453
   *
1454
   * @throws QueryException
1455
   */
1456 96
  public function query(string $sql = '', $params = false)
1457
  {
1458 96
    if (!$this->isReady()) {
1459
      return false;
1460
    }
1461
1462 96 View Code Duplication
    if (!$sql || $sql === '') {
1463 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1464
1465 4
      return false;
1466
    }
1467
1468
    if (
1469 94
        $params !== false
1470
        &&
1471 94
        \is_array($params)
1472
        &&
1473 94
        \count($params) > 0
1474
    ) {
1475 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1476 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1477 13
      $sql = $parseQueryParamsByName['sql'];
1478
    }
1479
1480
    // DEBUG
1481
    // var_dump($params);
1482
    // echo $sql . "\n";
1483
1484 94
    $query_start_time = microtime(true);
1485 94
    $query_result = \mysqli_real_query($this->link, $sql);
1486 94
    $query_duration = microtime(true) - $query_start_time;
1487
1488 94
    $this->query_count++;
1489
1490 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1491 94
    if ($mysqli_field_count) {
1492 62
      $result = \mysqli_store_result($this->link);
1493
    } else {
1494 64
      $result = $query_result;
1495
    }
1496
1497 94
    if ($result instanceof \mysqli_result) {
1498
1499
      // log the select query
1500 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1501
1502
      // return query result object
1503 61
      return new Result($sql, $result);
1504
    }
1505
1506 66
    if ($query_result === true) {
1507
1508
      // "INSERT" || "REPLACE"
1509 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1510 58
        $insert_id = (int)$this->insert_id();
1511 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1512
1513 58
        return $insert_id;
1514
      }
1515
1516
      // "UPDATE" || "DELETE"
1517 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1518 12
        $affected_rows = $this->affected_rows();
1519 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1520
1521 12
        return $affected_rows;
1522
      }
1523
1524
      // log the ? query
1525 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1526
1527 27
      return true;
1528
    }
1529
1530
    // log the error query
1531 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1532
1533 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1534
  }
1535
1536
  /**
1537
   * Error-handling for the sql-query.
1538
   *
1539
   * @param string     $errorMessage
1540
   * @param int        $errorNumber
1541
   * @param string     $sql
1542
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1543
   * @param bool       $sqlMultiQuery
1544
   *
1545
   * @throws QueryException
1546
   * @throws DBGoneAwayException
1547
   *
1548
   * @return mixed|false
1549
   */
1550 13
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1551
  {
1552
    if (
1553 13
        $errorMessage === 'DB server has gone away'
1554
        ||
1555 12
        $errorMessage === 'MySQL server has gone away'
1556
        ||
1557 13
        $errorNumber === 2006
1558
    ) {
1559 1
      static $RECONNECT_COUNTER;
1560
1561
      // exit if we have more then 3 "DB server has gone away"-errors
1562 1
      if ($RECONNECT_COUNTER > 3) {
1563
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1564
        throw new DBGoneAwayException($errorMessage);
1565
      }
1566
1567 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1568
1569
      // reconnect
1570 1
      $RECONNECT_COUNTER++;
1571 1
      $this->reconnect(true);
1572
1573
      // re-run the current (non multi) query
1574 1
      if ($sqlMultiQuery === false) {
1575 1
        return $this->query($sql, $sqlParams);
1576
      }
1577
1578
      return false;
1579
    }
1580
1581 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1582
1583 12
    $force_exception_after_error = null; // auto
1584 12
    if ($this->_in_transaction === true) {
1585 4
      $force_exception_after_error = false;
1586
    }
1587
    // this query returned an error, we must display it (only for dev) !!!
1588
1589 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1590
1591 12
    return false;
1592
  }
1593
1594
  /**
1595
   * Quote && Escape e.g. a table name string.
1596
   *
1597
   * @param mixed $str
1598
   *
1599
   * @return string
1600
   */
1601 36
  public function quote_string($str): string
1602
  {
1603 36
    $str = \str_replace(
1604 36
        '`',
1605 36
        '``',
1606 36
        \trim(
1607 36
            (string)$this->escape($str, false),
1608 36
            '`'
1609
        )
1610
    );
1611
1612 36
    return '`' . $str . '`';
1613
  }
1614
1615
  /**
1616
   * Reconnect to the MySQL-Server.
1617
   *
1618
   * @param bool $checkViaPing
1619
   *
1620
   * @return bool
1621
   */
1622 3
  public function reconnect(bool $checkViaPing = false): bool
1623
  {
1624 3
    $ping = false;
1625
1626 3
    if ($checkViaPing === true) {
1627 2
      $ping = $this->ping();
1628
    }
1629
1630 3
    if ($ping !== true) {
1631 3
      $this->connected = false;
1632 3
      $this->connect();
1633
    }
1634
1635 3
    return $this->isReady();
1636
  }
1637
1638
  /**
1639
   * Execute a "replace"-query.
1640
   *
1641
   * @param string      $table
1642
   * @param array       $data
1643
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1644
   *
1645
   * @return false|int <p>false on error</p>
1646
   *
1647
   * @throws QueryException
1648
   */
1649 1
  public function replace(string $table, array $data = [], string $databaseName = null)
1650
  {
1651
    // init
1652 1
    $table = \trim($table);
1653
1654 1
    if ($table === '') {
1655 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1656
1657 1
      return false;
1658
    }
1659
1660 1
    if (\count($data) === 0) {
1661 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1662
1663 1
      return false;
1664
    }
1665
1666
    // extracting column names
1667 1
    $columns = \array_keys($data);
1668 1
    foreach ($columns as $k => $_key) {
1669
      /** @noinspection AlterInForeachInspection */
1670 1
      $columns[$k] = $this->quote_string($_key);
1671
    }
1672
1673 1
    $columns = \implode(',', $columns);
1674
1675
    // extracting values
1676 1
    foreach ($data as $k => $_value) {
1677
      /** @noinspection AlterInForeachInspection */
1678 1
      $data[$k] = $this->secure($_value);
1679
    }
1680 1
    $values = \implode(',', $data);
1681
1682 1
    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...
1683
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1684
    }
1685
1686 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
1687
1688 1
    return $this->query($sql);
1689
  }
1690
1691
  /**
1692
   * Rollback in a transaction and end the transaction.
1693
   *
1694
   * @return bool <p>Boolean true on success, false otherwise.</p>
1695
   */
1696 4 View Code Duplication
  public function rollback(): bool
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...
1697
  {
1698 4
    if ($this->_in_transaction === false) {
1699
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1700
1701
      return false;
1702
    }
1703
1704 4
    $return = \mysqli_rollback($this->link);
1705 4
    \mysqli_autocommit($this->link, true);
1706 4
    $this->_in_transaction = false;
1707
1708 4
    return $return;
1709
  }
1710
1711
  /**
1712
   * Try to secure a variable, so can you use it in sql-queries.
1713
   *
1714
   * <p>
1715
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1716
   * 1. parse into "int"
1717
   * </p><br />
1718
   *
1719
   * <p>
1720
   * <strong>float:</strong><br />
1721
   * 1. return "float"
1722
   * </p><br />
1723
   *
1724
   * <p>
1725
   * <strong>string:</strong><br />
1726
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1727
   * 2. \trim whitespace<br />
1728
   * 3. \trim '<br />
1729
   * 4. escape the string (and remove non utf-8 chars)<br />
1730
   * 5. \trim ' again (because we maybe removed some chars)<br />
1731
   * 6. add ' around the new string<br />
1732
   * </p><br />
1733
   *
1734
   * <p>
1735
   * <strong>array:</strong><br />
1736
   * 1. return null
1737
   * </p><br />
1738
   *
1739
   * <p>
1740
   * <strong>object:</strong><br />
1741
   * 1. return false
1742
   * </p><br />
1743
   *
1744
   * <p>
1745
   * <strong>null:</strong><br />
1746
   * 1. return null
1747
   * </p>
1748
   *
1749
   * @param mixed $var
1750
   *
1751
   * @return mixed
1752
   */
1753 44
  public function secure($var)
1754
  {
1755 44
    if ($var === '') {
1756 2
      return "''";
1757
    }
1758
1759 44
    if ($var === "''") {
1760 1
      return "''";
1761
    }
1762
1763 44
    if ($var === null) {
1764 2
      if ($this->_convert_null_to_empty_string === true) {
1765 1
        return "''";
1766
      }
1767
1768 2
      return 'NULL';
1769
    }
1770
1771 43
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1772 1
      return $var;
1773
    }
1774
1775 43
    if (\is_string($var)) {
1776 35
      $var = \trim(\trim($var), "'");
1777
    }
1778
1779 43
    $var = $this->escape($var, false, false, null);
1780
1781 43
    if (\is_string($var)) {
1782 35
      $var = "'" . \trim($var, "'") . "'";
1783
    }
1784
1785 43
    return $var;
1786
  }
1787
1788
  /**
1789
   * Execute a "select"-query.
1790
   *
1791
   * @param string       $table
1792
   * @param string|array $where
1793
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1794
   *
1795
   * @return false|Result <p>false on error</p>
1796
   *
1797
   * @throws QueryException
1798
   */
1799 24 View Code Duplication
  public function select(string $table, $where = '1=1', string $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...
1800
  {
1801
    // init
1802 24
    $table = \trim($table);
1803
1804 24
    if ($table === '') {
1805 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1806
1807 1
      return false;
1808
    }
1809
1810 24
    if (\is_string($where)) {
1811 8
      $WHERE = $this->escape($where, false);
1812 17
    } elseif (\is_array($where)) {
1813 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1814
    } else {
1815 1
      $WHERE = '';
1816
    }
1817
1818 24
    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...
1819
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1820
    }
1821
1822 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
1823
1824 24
    return $this->query($sql);
1825
  }
1826
1827
  /**
1828
   * Selects a different database than the one specified on construction.
1829
   *
1830
   * @param string $database <p>Database name to switch to.</p>
1831
   *
1832
   * @return bool <p>Boolean true on success, false otherwise.</p>
1833
   */
1834
  public function select_db(string $database): bool
1835
  {
1836
    if (!$this->isReady()) {
1837
      return false;
1838
    }
1839
1840
    return mysqli_select_db($this->link, $database);
1841
  }
1842
1843
  /**
1844
   * Set the current charset.
1845
   *
1846
   * @param string $charset
1847
   *
1848
   * @return bool
1849
   */
1850 8
  public function set_charset(string $charset): bool
1851
  {
1852 8
    $charsetLower = strtolower($charset);
1853 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1854 6
      $charset = 'utf8';
1855
    }
1856 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1857 6
      $charset = 'utf8mb4';
1858
    }
1859
1860 8
    $this->charset = $charset;
1861
1862 8
    $return = mysqli_set_charset($this->link, $charset);
1863
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1864 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1865
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1866 8
    @\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...
1867
1868 8
    return $return;
1869
  }
1870
1871
  /**
1872
   * Set the option to convert null to "''" (empty string).
1873
   *
1874
   * Used in secure() => select(), insert(), update(), delete()
1875
   *
1876
   * @deprecated It's not recommended to convert NULL into an empty string!
1877
   *
1878
   * @param bool $bool
1879
   *
1880
   * @return self
1881
   */
1882 1
  public function set_convert_null_to_empty_string(bool $bool): self
1883
  {
1884 1
    $this->_convert_null_to_empty_string = $bool;
1885
1886 1
    return $this;
1887
  }
1888
1889
  /**
1890
   * Enables or disables internal report functions
1891
   *
1892
   * @link http://php.net/manual/en/function.mysqli-report.php
1893
   *
1894
   * @param int $flags <p>
1895
   *                   <table>
1896
   *                   Supported flags
1897
   *                   <tr valign="top">
1898
   *                   <td>Name</td>
1899
   *                   <td>Description</td>
1900
   *                   </tr>
1901
   *                   <tr valign="top">
1902
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1903
   *                   <td>Turns reporting off</td>
1904
   *                   </tr>
1905
   *                   <tr valign="top">
1906
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1907
   *                   <td>Report errors from mysqli function calls</td>
1908
   *                   </tr>
1909
   *                   <tr valign="top">
1910
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1911
   *                   <td>
1912
   *                   Throw <b>mysqli_sql_exception</b> for errors
1913
   *                   instead of warnings
1914
   *                   </td>
1915
   *                   </tr>
1916
   *                   <tr valign="top">
1917
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1918
   *                   <td>Report if no index or bad index was used in a query</td>
1919
   *                   </tr>
1920
   *                   <tr valign="top">
1921
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1922
   *                   <td>Set all options (report all)</td>
1923
   *                   </tr>
1924
   *                   </table>
1925
   *                   </p>
1926
   *
1927
   * @return bool
1928
   */
1929
  public function set_mysqli_report(int $flags): bool
1930
  {
1931
    return \mysqli_report($flags);
1932
  }
1933
1934
  /**
1935
   * Show config errors by throw exceptions.
1936
   *
1937
   * @return bool
1938
   *
1939
   * @throws \InvalidArgumentException
1940
   */
1941 11
  public function showConfigError(): bool
1942
  {
1943
1944
    if (
1945 11
        !$this->hostname
1946
        ||
1947 10
        !$this->username
1948
        ||
1949 11
        !$this->database
1950
    ) {
1951
1952 3
      if (!$this->hostname) {
1953 1
        throw new \InvalidArgumentException('no-sql-hostname');
1954
      }
1955
1956 2
      if (!$this->username) {
1957 1
        throw new \InvalidArgumentException('no-sql-username');
1958
      }
1959
1960 1
      if (!$this->database) {
1961 1
        throw new \InvalidArgumentException('no-sql-database');
1962
      }
1963
1964
      return false;
1965
    }
1966
1967 8
    return true;
1968
  }
1969
1970
  /**
1971
   * alias: "beginTransaction()"
1972
   */
1973 1
  public function startTransaction(): bool
1974
  {
1975 1
    return $this->beginTransaction();
1976
  }
1977
1978
  /**
1979
   * Execute a callback inside a transaction.
1980
   *
1981
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1982
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1983
   *
1984
   * @return bool <p>Boolean true on success, false otherwise.</p>
1985
   */
1986 1
  public function transact($callback): bool
1987
  {
1988
    try {
1989
1990 1
      $beginTransaction = $this->beginTransaction();
1991 1
      if ($beginTransaction === false) {
1992 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1993
1994 1
        return false;
1995
      }
1996
1997 1
      $result = $callback($this);
1998 1
      if ($result === false) {
1999
        /** @noinspection ThrowRawExceptionInspection */
2000 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
2001
      }
2002
2003 1
      return $this->commit();
2004
2005 1
    } catch (\Exception $e) {
2006
2007 1
      $this->rollback();
2008
2009 1
      return false;
2010
    }
2011
  }
2012
2013
  /**
2014
   * Execute a "update"-query.
2015
   *
2016
   * @param string       $table
2017
   * @param array        $data
2018
   * @param array|string $where
2019
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2020
   *
2021
   * @return false|int <p>false on error</p>
2022
   *
2023
   * @throws QueryException
2024
   */
2025 7
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2026
  {
2027
    // init
2028 7
    $table = \trim($table);
2029
2030 7
    if ($table === '') {
2031 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2032
2033 1
      return false;
2034
    }
2035
2036 7
    if (\count($data) === 0) {
2037 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2038
2039 2
      return false;
2040
    }
2041
2042 7
    $SET = $this->_parseArrayPair($data);
2043
2044 7
    if (\is_string($where)) {
2045 2
      $WHERE = $this->escape($where, false);
2046 6
    } elseif (\is_array($where)) {
2047 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2048
    } else {
2049 1
      $WHERE = '';
2050
    }
2051
2052 7
    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...
2053
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2054
    }
2055
2056 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2057
2058 7
    return $this->query($sql);
2059
  }
2060
2061
  /**
2062
   * Determine if database table exists
2063
   *
2064
   * @param string $table
2065
   *
2066
   * @return bool
2067
   */
2068 1
  public function table_exists(string $table): bool
2069
  {
2070 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2071
2072 1
    return $check !== false
2073
           &&
2074 1
           $check instanceof Result
2075
           &&
2076 1
           $check->num_rows > 0;
2077
  }
2078
2079
  /**
2080
   * Count number of rows found matching a specific query.
2081
   *
2082
   * @param string $query
2083
   *
2084
   * @return int
2085
   */
2086 1
  public function num_rows(string $query): int
2087
  {
2088 1
    $check = $this->query($query);
2089
2090
    if (
2091 1
        $check === false
2092
        ||
2093 1
        !$check instanceof Result
2094
    ) {
2095
      return 0;
2096
    }
2097
2098 1
    return $check->num_rows;
2099
  }
2100
}
2101