Completed
Push — master ( 2c53b6...01cac9 )
by Lars
01:43
created

DB::execSQL()   C

Complexity

Conditions 10
Paths 14

Size

Total Lines 49
Code Lines 29

Duplication

Lines 26
Ratio 53.06 %

Code Coverage

Tests 21
CRAP Score 10

Importance

Changes 0
Metric Value
dl 26
loc 49
ccs 21
cts 21
cp 1
rs 5.5471
c 0
b 0
f 0
cc 10
eloc 29
nc 14
nop 4
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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' => false|true<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(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 = []): bool
272
  {
273 11
    $this->hostname = $hostname;
274 11
    $this->username = $username;
275 11
    $this->password = $password;
276 11
    $this->database = $database;
277
278 11
    if ($charset) {
279 5
      $this->charset = $charset;
280
    }
281
282 11
    if ($port) {
283 5
      $this->port = (int)$port;
284
    } else {
285
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
286 7
      $this->port = (int)@ini_get('mysqli.default_port');
287
    }
288
289
    // fallback
290 11
    if (!$this->port) {
291
      $this->port = 3306;
292
    }
293
294 11
    if (!$this->socket) {
295
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
296 11
      $this->socket = @ini_get('mysqli.default_socket');
297
    }
298
299 11
    $this->_debug->setExitOnError($exit_on_error);
300 11
    $this->_debug->setEchoOnError($echo_on_error);
301
302 11
    $this->_debug->setLoggerClassName($logger_class_name);
303 11
    $this->_debug->setLoggerLevel($logger_level);
304
305 11
    if (\is_array($extra_config) === true) {
306
307 11
      $this->setConfigExtra($extra_config);
308
309
    } else {
310
311
      // only for backward compatibility
312
      $this->session_to_db = (boolean)$extra_config;
313
314
    }
315
316 11
    return $this->showConfigError();
317
  }
318
319
320
  /**
321
   * @param array $extra_config           <p>
322
   *                                      'session_to_db' => false|true<br>
323
   *                                      'socket' => 'string (path)'<br>
324
   *                                      'ssl' => 'bool'<br>
325
   *                                      'clientkey' => 'string (path)'<br>
326
   *                                      'clientcert' => 'string (path)'<br>
327
   *                                      'cacert' => 'string (path)'<br>
328
   *                                      </p>
329
   */
330 11
  public function setConfigExtra(array $extra_config)
331
  {
332 11
    if (isset($extra_config['session_to_db'])) {
333
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
334
    }
335
336 11
    if (isset($extra_config['socket'])) {
337
      $this->socket = $extra_config['socket'];
338
    }
339
340 11
    if (isset($extra_config['ssl'])) {
341
      $this->_ssl = $extra_config['ssl'];
342
    }
343
344 11
    if (isset($extra_config['clientkey'])) {
345
      $this->_clientkey = $extra_config['clientkey'];
346
    }
347
348 11
    if (isset($extra_config['clientcert'])) {
349
      $this->_clientcert = $extra_config['clientcert'];
350
    }
351
352 11
    if (isset($extra_config['cacert'])) {
353
      $this->_cacert = $extra_config['cacert'];
354
    }
355 11
  }
356
357
  /**
358
   * Parses arrays with value pairs and generates SQL to use in queries.
359
   *
360
   * @param array  $arrayPair
361
   * @param string $glue <p>This is the separator.</p>
362
   *
363
   * @return string
364
   *
365
   * @internal
366
   */
367 30
  public function _parseArrayPair(array $arrayPair, string $glue = ','): string
368
  {
369
    // init
370 30
    $sql = '';
371
372 30
    if (\count($arrayPair) === 0) {
373
      return '';
374
    }
375
376 30
    $arrayPairCounter = 0;
377 30
    foreach ($arrayPair as $_key => $_value) {
378 30
      $_connector = '=';
379 30
      $_glueHelper = '';
380 30
      $_key_upper = \strtoupper($_key);
381
382 30
      if (\strpos($_key_upper, ' NOT') !== false) {
383 2
        $_connector = 'NOT';
384
      }
385
386 30
      if (\strpos($_key_upper, ' IS') !== false) {
387 1
        $_connector = 'IS';
388
      }
389
390 30
      if (\strpos($_key_upper, ' IS NOT') !== false) {
391 1
        $_connector = 'IS NOT';
392
      }
393
394 30
      if (\strpos($_key_upper, ' IN') !== false) {
395 1
        $_connector = 'IN';
396
      }
397
398 30
      if (\strpos($_key_upper, ' NOT IN') !== false) {
399 1
        $_connector = 'NOT IN';
400
      }
401
402 30
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
403 1
        $_connector = 'BETWEEN';
404
      }
405
406 30
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
407 1
        $_connector = 'NOT BETWEEN';
408
      }
409
410 30
      if (\strpos($_key_upper, ' LIKE') !== false) {
411 2
        $_connector = 'LIKE';
412
      }
413
414 30
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
415 2
        $_connector = 'NOT LIKE';
416
      }
417
418 30 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
419 4
        $_connector = '>';
420
      }
421
422 30 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
423 1
        $_connector = '<';
424
      }
425
426 30
      if (\strpos($_key_upper, ' >=') !== false) {
427 4
        $_connector = '>=';
428
      }
429
430 30
      if (\strpos($_key_upper, ' <=') !== false) {
431 1
        $_connector = '<=';
432
      }
433
434 30
      if (\strpos($_key_upper, ' <>') !== false) {
435 1
        $_connector = '<>';
436
      }
437
438 30
      if (\strpos($_key_upper, ' OR') !== false) {
439 2
        $_glueHelper = 'OR';
440
      }
441
442 30
      if (\strpos($_key_upper, ' AND') !== false) {
443 1
        $_glueHelper = 'AND';
444
      }
445
446 30
      if (\is_array($_value) === true) {
447 2
        foreach ($_value as $oldKey => $oldValue) {
448 2
          $_value[$oldKey] = $this->secure($oldValue);
449
        }
450
451 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
452 1
          $_value = '(' . \implode(',', $_value) . ')';
453 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
454 2
          $_value = '(' . \implode(' AND ', $_value) . ')';
455
        }
456
457
      } else {
458 30
        $_value = $this->secure($_value);
459
      }
460
461 30
      $quoteString = $this->quote_string(
462 30
          \trim(
463 30
              \str_ireplace(
464
                  [
465 30
                      $_connector,
466 30
                      $_glueHelper,
467
                  ],
468 30
                  '',
469 30
                  $_key
470
              )
471
          )
472
      );
473
474 30
      if (!\is_array($_value)) {
475 30
        $_value = [$_value];
476
      }
477
478 30
      if (!$_glueHelper) {
479 30
        $_glueHelper = $glue;
480
      }
481
482 30
      $tmpCounter = 0;
483 30
      foreach ($_value as $valueInner) {
484
485 30
        $_glueHelperInner = $_glueHelper;
486
487 30
        if ($arrayPairCounter === 0) {
488
489 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
490 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
491 30
          } elseif ($tmpCounter === 0) {
492 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
493
          }
494
495 26
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
496 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
497
        }
498
499 30
        if (\is_string($valueInner) && $valueInner === '') {
500 1
          $valueInner = "''";
501
        }
502
503 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
504 30
        $tmpCounter++;
505
      }
506
507 30
      if ($_glueHelper === 'OR') {
508 2
        $sql .= ' ) ';
509
      }
510
511 30
      $arrayPairCounter++;
512
    }
513
514 30
    return $sql;
515
  }
516
517
  /**
518
   * _parseQueryParams
519
   *
520
   * @param string $sql
521
   * @param array  $params
522
   *
523
   * @return array <p>with the keys -> 'sql', 'params'</p>
524
   */
525 13
  private function _parseQueryParams(string $sql, array $params = []): array
526
  {
527
    // is there anything to parse?
528 View Code Duplication
    if (
529 13
        \strpos($sql, '?') === false
530
        ||
531 13
        \count($params) === 0
532
    ) {
533 11
      return ['sql' => $sql, 'params' => $params];
534
    }
535
536 3
    $parseKey = \md5(\uniqid((string)\mt_rand(), true));
537 3
    $sql = \str_replace('?', $parseKey, $sql);
538
539 3
    $k = 0;
540 3
    while (\strpos($sql, $parseKey) !== false) {
541 3
      $sql = UTF8::str_replace_first(
542 3
          $parseKey,
543 3
          (string)(isset($params[$k]) ? $this->secure($params[$k]) : ''),
544 3
          $sql
545
      );
546
547 3
      if (isset($params[$k])) {
548 3
        unset($params[$k]);
549
      }
550
551 3
      $k++;
552
    }
553
554 3
    return ['sql' => $sql, 'params' => $params];
555
  }
556
557
  /**
558
   * Gets the number of affected rows in a previous MySQL operation.
559
   *
560
   * @return int
561
   */
562 12
  public function affected_rows(): int
563
  {
564 12
    return \mysqli_affected_rows($this->link);
565
  }
566
567
  /**
568
   * Begins a transaction, by turning off auto commit.
569
   *
570
   * @return bool <p>This will return true or false indicating success of transaction</p>
571
   */
572 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...
573
  {
574 6
    if ($this->_in_transaction === true) {
575 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
576
577 2
      return false;
578
    }
579
580 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
581 6
    $this->_in_transaction = true;
582 6
    $return = \mysqli_autocommit($this->link, false);
583 6
    if ($return === false) {
584
      $this->_in_transaction = false;
585
    }
586
587 6
    return $return;
588
  }
589
590
  /**
591
   * Clear the errors in "_debug->_errors".
592
   *
593
   * @return bool
594
   */
595 6
  public function clearErrors(): bool
596
  {
597 6
    return $this->_debug->clearErrors();
598
  }
599
600
  /**
601
   * Closes a previously opened database connection.
602
   */
603 2
  public function close(): bool
604
  {
605 2
    $this->connected = false;
606
607
    if (
608 2
        $this->link
609
        &&
610 2
        $this->link instanceof \mysqli
611
    ) {
612 2
      $result = \mysqli_close($this->link);
613 2
      $this->link = null;
614
615 2
      return $result;
616
    }
617
618 1
    return false;
619
  }
620
621
  /**
622
   * Commits the current transaction and end the transaction.
623
   *
624
   * @return bool <p>Boolean true on success, false otherwise.</p>
625
   */
626 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...
627
  {
628 2
    if ($this->_in_transaction === false) {
629
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
630
631
      return false;
632
    }
633
634 2
    $return = mysqli_commit($this->link);
635 2
    \mysqli_autocommit($this->link, true);
636 2
    $this->_in_transaction = false;
637
638 2
    return $return;
639
  }
640
641
  /**
642
   * Open a new connection to the MySQL server.
643
   *
644
   * @return bool
645
   *
646
   * @throws DBConnectException
647
   */
648 10
  public function connect(): bool
649
  {
650 10
    if ($this->isReady()) {
651 1
      return true;
652
    }
653
654 10
    $flags = null;
655
656 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
657
    try {
658 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...
659
660 10
      if (Helper::isMysqlndIsUsed() === true) {
661 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
662
      }
663
664 10
      if ($this->_ssl === true) {
665
666
        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...
667
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
668
        }
669
670
        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...
671
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
672
        }
673
674
        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...
675
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
676
        }
677
678
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
679
680
        /** @noinspection PhpParamsInspection */
681
        \mysqli_ssl_set(
682
            $this->link,
683
            $this->_clientkey,
684
            $this->_clientcert,
685
            $this->_cacert,
686
            null,
687
            null
688
        );
689
690
        $flags = MYSQLI_CLIENT_SSL;
691
      }
692
693
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
694 10
      $this->connected = @\mysqli_real_connect(
695 10
          $this->link,
696 10
          $this->hostname,
697 10
          $this->username,
698 10
          $this->password,
699 10
          $this->database,
700 10
          $this->port,
701 10
          $this->socket,
702 10
          (int)$flags
703
      );
704
705 3
    } catch (\Exception $e) {
706 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
707 3
      $this->_debug->displayError($error, true);
708
      throw new DBConnectException($error, 100, $e);
709
    }
710 7
    \mysqli_report(MYSQLI_REPORT_OFF);
711
712 7
    $errno = mysqli_connect_errno();
713 7
    if (!$this->connected || $errno) {
714
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
715
      $this->_debug->displayError($error, true);
716
      throw new DBConnectException($error, 101);
717
    }
718
719 7
    $this->set_charset($this->charset);
720
721 7
    return $this->isReady();
722
  }
723
724
  /**
725
   * Execute a "delete"-query.
726
   *
727
   * @param string       $table
728
   * @param string|array $where
729
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
730
   *
731
   * @return false|int <p>false on error</p>
732
   *
733
   *    * @throws QueryException
734
   */
735 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...
736
  {
737
    // init
738 2
    $table = \trim($table);
739
740 2
    if ($table === '') {
741 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
742
743 1
      return false;
744
    }
745
746 2
    if (\is_string($where)) {
747 1
      $WHERE = $this->escape($where, false);
748 2
    } elseif (\is_array($where)) {
749 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
750
    } else {
751 1
      $WHERE = '';
752
    }
753
754 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...
755
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
756
    }
757
758 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
759
760 2
    return $this->query($sql);
761
  }
762
763
  /**
764
   * Ends a transaction and commits if no errors, then ends autocommit.
765
   *
766
   * @return bool <p>This will return true or false indicating success of transactions.</p>
767
   */
768 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...
769
  {
770 4
    if ($this->_in_transaction === false) {
771
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
772
773
      return false;
774
    }
775
776 4
    if (!$this->errors()) {
777 1
      $return = \mysqli_commit($this->link);
778
    } else {
779 3
      $this->rollback();
780 3
      $return = false;
781
    }
782
783 4
    \mysqli_autocommit($this->link, true);
784 4
    $this->_in_transaction = false;
785
786 4
    return $return;
787
  }
788
789
  /**
790
   * Get all errors from "$this->_errors".
791
   *
792
   * @return array|false <p>false === on errors</p>
793
   */
794 4
  public function errors()
795
  {
796 4
    $errors = $this->_debug->getErrors();
797
798 4
    return \count($errors) > 0 ? $errors : false;
799
  }
800
801
  /**
802
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
803
   *
804
   * @param mixed $sql    <p>The SQL string.</p>
805
   * @param array $params <p>An array of key-value bindings.</p>
806
   *
807
   * @return array <p>with the keys -> 'sql', 'params'</p>
808
   */
809 14
  public function _parseQueryParamsByName(string $sql, array $params = []): array
810
  {
811
    // is there anything to parse?
812 View Code Duplication
    if (
813 14
        \strpos($sql, ':') === false
814
        ||
815 14
        \count($params) === 0
816
    ) {
817 3
      return ['sql' => $sql, 'params' => $params];
818
    }
819
820 12
    $parseKey = \md5(\uniqid((string)\mt_rand(), true));
821
822 12
    foreach ($params as $name => $value) {
823 12
      $nameTmp = $name;
824 12
      if (\strpos($name, ':') === 0) {
825 10
        $nameTmp = \substr($name, 1);
826
      }
827
828 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
829 12
      $sql = \str_replace(':' . $nameTmp, $parseKeyInner, $sql);
830
    }
831
832 12
    foreach ($params as $name => $value) {
833 12
      $nameTmp = $name;
834 12
      if (\strpos($name, ':') === 0) {
835 10
        $nameTmp = \substr($name, 1);
836
      }
837
838 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
839 12
      $sqlBefore = $sql;
840 12
      $secureParamValue = $this->secure($params[$name]);
841
842 12
      while (\strpos($sql, $parseKeyInner) !== false) {
843 12
        $sql = UTF8::str_replace_first(
844 12
            $parseKeyInner,
845 12
            (string)$secureParamValue,
846 12
            $sql
847
        );
848
      }
849
850 12
      if ($sqlBefore !== $sql) {
851 12
        unset($params[$name]);
852
      }
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 52
        \is_bool($var)
908
        ||
909
        (
910 52
            isset($varInt, $var[0])
911
            &&
912 52
            $var[0] != '0'
913
            &&
914 55
            "$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 52
    if (\is_float($var)) {
922 5
      return $var;
923
    }
924
925
    // array
926 52
    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 52
        \is_string($var)
954
        ||
955
        (
956 3
            \is_object($var)
957
            &&
958 52
            method_exists($var, '__toString')
959
        )
960
    ) {
961 52
      $var = (string)$var;
962
963 52
      if ($stripe_non_utf8 === true) {
964 11
        $var = UTF8::cleanup($var);
965
      }
966
967 52
      if ($html_entity_decode === true) {
968
        // use no-html-entity for db
969 1
        $var = UTF8::html_entity_decode($var);
970
      }
971
972 52
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
973
974 52
      $var = \mysqli_real_escape_string($this->getLink(), $var);
975
976 52
      return (string)$var;
977
978
    }
979
980
    // "DateTime"-object
981 3
    if ($var instanceof \DateTime) {
982 3
      return $this->escape($var->format('Y-m-d H:i:s'), false);
983
    }
984
985 2
    return false;
986
  }
987
988
  /**
989
   * Execute select/insert/update/delete sql-queries.
990
   *
991
   * @param string $query    <p>sql-query</p>
992
   * @param bool   $useCache <p>use cache?</p>
993
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
994
   * @param DB     $db       optional <p>the database connection</p>
995
   *
996
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
997
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
998
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
999
   *               "true" by e.g. "DROP"-queries<br />
1000
   *               "false" on error
1001
   *
1002
   * @throws QueryException
1003
   */
1004 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1005
  {
1006
    // init
1007 3
    $cacheKey = null;
1008 3
    if (!$db) {
1009 3
      $db = self::getInstance();
1010
    }
1011
1012 3 View Code Duplication
    if ($useCache === true) {
1013 1
      $cache = new Cache(null, null, false, $useCache);
1014 1
      $cacheKey = 'sql-' . \md5($query);
1015
1016
      if (
1017 1
          $cache->getCacheIsReady() === true
1018
          &&
1019 1
          $cache->existsItem($cacheKey)
1020
      ) {
1021 1
        return $cache->getItem($cacheKey);
1022
      }
1023
1024
    } else {
1025 3
      $cache = false;
1026
    }
1027
1028 3
    $result = $db->query($query);
1029
1030 3
    if ($result instanceof Result) {
1031
1032 1
      $return = $result->fetchAllArray();
1033
1034
      // save into the cache
1035 View Code Duplication
      if (
1036 1
          $cacheKey !== null
1037
          &&
1038 1
          $useCache === true
1039
          &&
1040 1
          $cache instanceof Cache
1041
          &&
1042 1
          $cache->getCacheIsReady() === true
1043
      ) {
1044 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1045
      }
1046
1047
    } else {
1048 2
      $return = $result;
1049
    }
1050
1051 3
    return $return;
1052
  }
1053
1054
  /**
1055
   * Get all table-names via "SHOW TABLES".
1056
   *
1057
   * @return array
1058
   */
1059 1
  public function getAllTables(): array
1060
  {
1061 1
    $query = 'SHOW TABLES';
1062 1
    $result = $this->query($query);
1063
1064 1
    return $result->fetchAllArray();
1065
  }
1066
1067
  /**
1068
   * @return Debug
1069
   */
1070 9
  public function getDebugger(): Debug
1071
  {
1072 9
    return $this->_debug;
1073
  }
1074
1075
  /**
1076
   * Get errors from "$this->_errors".
1077
   *
1078
   * @return array
1079
   */
1080 1
  public function getErrors(): array
1081
  {
1082 1
    return $this->_debug->getErrors();
1083
  }
1084
1085
  /**
1086
   * getInstance()
1087
   *
1088
   * @param string $hostname
1089
   * @param string $username
1090
   * @param string $password
1091
   * @param string $database
1092
   * @param int    $port                 <p>default is (int)3306</p>
1093
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1094
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1095
   *                                     Use false to disable it.</p>
1096
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1097
   *                                     Use false to disable it.</p>
1098
   * @param string $logger_class_name
1099
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1100
   * @param array  $extra_config         <p>
1101
   *                                     'session_to_db' => false|true<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 \voku\db\DB
1110
   */
1111 97
  public static function getInstance(string $hostname = '', string $username = '', string $password = '', string $database = '', $port = 3306, string $charset = 'utf8', bool $exit_on_error = true, bool $echo_on_error = true, string $logger_class_name = '', string $logger_level = '', array $extra_config = []): self
1112
  {
1113
    /**
1114
     * @var $instance DB[]
1115
     */
1116 97
    static $instance = [];
1117
1118
    /**
1119
     * @var $firstInstance DB
1120
     */
1121 97
    static $firstInstance = null;
1122
1123
    if (
1124 97
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1125
        &&
1126 97
        null !== $firstInstance
1127
    ) {
1128 14
      return $firstInstance;
1129
    }
1130
1131 97
    $extra_config_string = '';
1132 97
    if (\is_array($extra_config) === true) {
1133 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1134 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1135
      }
1136
    } else {
1137
      // only for backward compatibility
1138
      $extra_config_string = (int)$extra_config;
1139
    }
1140
1141 97
    $connection = \md5(
1142 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1143
    );
1144
1145 97
    if (!isset($instance[$connection])) {
1146 11
      $instance[$connection] = new self(
1147 11
          $hostname,
1148 11
          $username,
1149 11
          $password,
1150 11
          $database,
1151 11
          $port,
1152 11
          $charset,
1153 11
          $exit_on_error,
1154 11
          $echo_on_error,
1155 11
          $logger_class_name,
1156 11
          $logger_level,
1157 11
          $extra_config
1158
      );
1159
1160 5
      if (null === $firstInstance) {
1161 1
        $firstInstance = $instance[$connection];
1162
      }
1163
    }
1164
1165 97
    return $instance[$connection];
1166
  }
1167
1168
  /**
1169
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1170
   *
1171
   * @return \mysqli
1172
   */
1173 56
  public function getLink(): \mysqli
1174
  {
1175 56
    return $this->link;
1176
  }
1177
1178
  /**
1179
   * Get the current charset.
1180
   *
1181
   * @return string
1182
   */
1183 1
  public function get_charset(): string
1184
  {
1185 1
    return $this->charset;
1186
  }
1187
1188
  /**
1189
   * Check if we are in a transaction.
1190
   *
1191
   * @return bool
1192
   */
1193
  public function inTransaction(): bool
1194
  {
1195
    return $this->_in_transaction;
1196
  }
1197
1198
  /**
1199
   * Execute a "insert"-query.
1200
   *
1201
   * @param string      $table
1202
   * @param array       $data
1203
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1204
   *
1205
   * @return false|int <p>false on error</p>
1206
   *
1207
   * @throws QueryException
1208
   */
1209 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1210
  {
1211
    // init
1212 28
    $table = \trim($table);
1213
1214 28
    if ($table === '') {
1215 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1216
1217 2
      return false;
1218
    }
1219
1220 27
    if (\count($data) === 0) {
1221 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1222
1223 3
      return false;
1224
    }
1225
1226 25
    $SET = $this->_parseArrayPair($data);
1227
1228 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...
1229
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1230
    }
1231
1232 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1233
1234 25
    return $this->query($sql);
1235
  }
1236
1237
  /**
1238
   * Returns the auto generated id used in the last query.
1239
   *
1240
   * @return int|string
1241
   */
1242 58
  public function insert_id()
1243
  {
1244 58
    return \mysqli_insert_id($this->link);
1245
  }
1246
1247
  /**
1248
   * Check if db-connection is ready.
1249
   *
1250
   * @return boolean
1251
   */
1252 106
  public function isReady(): bool
1253
  {
1254 106
    return $this->connected ? true : false;
1255
  }
1256
1257
  /**
1258
   * Get the last sql-error.
1259
   *
1260
   * @return string|false <p>false === there was no error</p>
1261
   */
1262 1
  public function lastError()
1263
  {
1264 1
    $errors = $this->_debug->getErrors();
1265
1266 1
    return \count($errors) > 0 ? end($errors) : false;
1267
  }
1268
1269
  /**
1270
   * Execute a sql-multi-query.
1271
   *
1272
   * @param string $sql
1273
   *
1274
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1275
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1276
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1277
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1278
   *
1279
   * @throws QueryException
1280
   */
1281 1
  public function multi_query(string $sql)
1282
  {
1283 1
    if (!$this->isReady()) {
1284
      return false;
1285
    }
1286
1287 1 View Code Duplication
    if (!$sql || $sql === '') {
1288 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1289
1290 1
      return false;
1291
    }
1292
1293 1
    $query_start_time = microtime(true);
1294 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1295 1
    $query_duration = microtime(true) - $query_start_time;
1296
1297 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1298
1299 1
    $returnTheResult = false;
1300 1
    $result = [];
1301
1302 1
    if ($resultTmp) {
1303
      do {
1304
1305 1
        $resultTmpInner = \mysqli_store_result($this->link);
1306
1307 1
        if ($resultTmpInner instanceof \mysqli_result) {
1308
1309 1
          $returnTheResult = true;
1310 1
          $result[] = new Result($sql, $resultTmpInner);
1311
1312
        } else {
1313
1314
          // is the query successful
1315 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1316 1
            $result[] = true;
1317
          } else {
1318
            $result[] = false;
1319
          }
1320
1321
        }
1322
1323 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1324
1325
    } else {
1326
1327
      // log the error query
1328 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1329
1330 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1331
    }
1332
1333
    // return the result only if there was a "SELECT"-query
1334 1
    if ($returnTheResult === true) {
1335 1
      return $result;
1336
    }
1337
1338
    if (
1339 1
        \count($result) > 0
1340
        &&
1341 1
        \in_array(false, $result, true) === false
1342
    ) {
1343 1
      return true;
1344
    }
1345
1346
    return false;
1347
  }
1348
1349
  /**
1350
   * Pings a server connection, or tries to reconnect
1351
   * if the connection has gone down.
1352
   *
1353
   * @return boolean
1354
   */
1355 3
  public function ping(): bool
1356
  {
1357
    if (
1358 3
        $this->link
1359
        &&
1360 3
        $this->link instanceof \mysqli
1361
    ) {
1362
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1363 2
      return (bool)@\mysqli_ping($this->link);
1364
    }
1365
1366 1
    return false;
1367
  }
1368
1369
  /**
1370
   * Get a new "Prepare"-Object for your sql-query.
1371
   *
1372
   * @param string $query
1373
   *
1374
   * @return Prepare
1375
   */
1376 2
  public function prepare(string $query): Prepare
1377
  {
1378 2
    return new Prepare($this, $query);
1379
  }
1380
1381
  /**
1382
   * Execute a sql-query and return the result-array for select-statements.
1383
   *
1384
   * @param string $query
1385
   *
1386
   * @return mixed
1387
   * @deprecated
1388
   * @throws \Exception
1389
   */
1390 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...
1391
  {
1392 1
    $db = self::getInstance();
1393
1394 1
    $args = \func_get_args();
1395
    /** @noinspection SuspiciousAssignmentsInspection */
1396 1
    $query = \array_shift($args);
1397 1
    $query = \str_replace('?', '%s', $query);
1398 1
    $args = \array_map(
1399
        [
1400 1
            $db,
1401 1
            'escape',
1402
        ],
1403 1
        $args
1404
    );
1405 1
    \array_unshift($args, $query);
1406 1
    $query = \sprintf(...$args);
1407 1
    $result = $db->query($query);
1408
1409 1
    if ($result instanceof Result) {
1410 1
      return $result->fetchAllArray();
1411
    }
1412
1413 1
    return $result;
1414
  }
1415
1416
  /**
1417
   * Execute a sql-query.
1418
   *
1419
   * @param string        $sql            <p>The sql query-string.</p>
1420
   *
1421
   * @param array|boolean $params         <p>
1422
   *                                      "array" of sql-query-parameters<br/>
1423
   *                                      "false" if you don't need any parameter (default)<br/>
1424
   *                                      </p>
1425
   *
1426
   * @return bool|int|Result              <p>
1427
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1428
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1429
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1430
   *                                      "true" by e.g. "DROP"-queries<br />
1431
   *                                      "false" on error
1432
   *                                      </p>
1433
   *
1434
   * @throws QueryException
1435
   */
1436 96
  public function query(string $sql = '', $params = false)
1437
  {
1438 96
    if (!$this->isReady()) {
1439
      return false;
1440
    }
1441
1442 96 View Code Duplication
    if (!$sql || $sql === '') {
1443 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1444
1445 4
      return false;
1446
    }
1447
1448
    if (
1449 94
        $params !== false
1450
        &&
1451 94
        \is_array($params)
1452
        &&
1453 94
        \count($params) > 0
1454
    ) {
1455 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1456 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1457 13
      $sql = $parseQueryParamsByName['sql'];
1458
    }
1459
1460
    // DEBUG
1461
    // var_dump($params);
1462
    // echo $sql . "\n";
1463
1464 94
    $query_start_time = microtime(true);
1465 94
    $query_result = \mysqli_real_query($this->link, $sql);
1466 94
    $query_duration = microtime(true) - $query_start_time;
1467
1468 94
    $this->query_count++;
1469
1470 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1471 94
    if ($mysqli_field_count) {
1472 62
      $result = \mysqli_store_result($this->link);
1473
    } else {
1474 64
      $result = $query_result;
1475
    }
1476
1477 94
    if ($result instanceof \mysqli_result) {
1478
1479
      // log the select query
1480 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1481
1482
      // return query result object
1483 61
      return new Result($sql, $result);
1484
    }
1485
1486 66
    if ($query_result === true) {
1487
1488
      // "INSERT" || "REPLACE"
1489 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1490 58
        $insert_id = (int)$this->insert_id();
1491 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1492
1493 58
        return $insert_id;
1494
      }
1495
1496
      // "UPDATE" || "DELETE"
1497 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1498 12
        $affected_rows = $this->affected_rows();
1499 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1500
1501 12
        return $affected_rows;
1502
      }
1503
1504
      // log the ? query
1505 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1506
1507 27
      return true;
1508
    }
1509
1510
    // log the error query
1511 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1512
1513 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1514
  }
1515
1516
  /**
1517
   * Error-handling for the sql-query.
1518
   *
1519
   * @param string     $errorMessage
1520
   * @param int        $errorNumber
1521
   * @param string     $sql
1522
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1523
   * @param bool       $sqlMultiQuery
1524
   *
1525
   * @throws QueryException
1526
   * @throws DBGoneAwayException
1527
   *
1528
   * @return mixed|false
1529
   */
1530 13
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1531
  {
1532
    if (
1533 13
        $errorMessage === 'DB server has gone away'
1534
        ||
1535 12
        $errorMessage === 'MySQL server has gone away'
1536
        ||
1537 13
        $errorNumber === 2006
1538
    ) {
1539 1
      static $RECONNECT_COUNTER;
1540
1541
      // exit if we have more then 3 "DB server has gone away"-errors
1542 1
      if ($RECONNECT_COUNTER > 3) {
1543
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1544
        throw new DBGoneAwayException($errorMessage);
1545
      }
1546
1547 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1548
1549
      // reconnect
1550 1
      $RECONNECT_COUNTER++;
1551 1
      $this->reconnect(true);
1552
1553
      // re-run the current (non multi) query
1554 1
      if ($sqlMultiQuery === false) {
1555 1
        return $this->query($sql, $sqlParams);
1556
      }
1557
1558
      return false;
1559
    }
1560
1561 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1562
1563 12
    $force_exception_after_error = null; // auto
1564 12
    if ($this->_in_transaction === true) {
1565 4
      $force_exception_after_error = false;
1566
    }
1567
    // this query returned an error, we must display it (only for dev) !!!
1568
1569 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1570
1571 12
    return false;
1572
  }
1573
1574
  /**
1575
   * Quote && Escape e.g. a table name string.
1576
   *
1577
   * @param mixed $str
1578
   *
1579
   * @return string
1580
   */
1581 36
  public function quote_string($str): string
1582
  {
1583 36
    $str = \str_replace(
1584 36
        '`',
1585 36
        '``',
1586 36
        \trim(
1587 36
            (string)$this->escape($str, false),
1588 36
            '`'
1589
        )
1590
    );
1591
1592 36
    return '`' . $str . '`';
1593
  }
1594
1595
  /**
1596
   * Reconnect to the MySQL-Server.
1597
   *
1598
   * @param bool $checkViaPing
1599
   *
1600
   * @return bool
1601
   */
1602 3
  public function reconnect(bool $checkViaPing = false): bool
1603
  {
1604 3
    $ping = false;
1605
1606 3
    if ($checkViaPing === true) {
1607 2
      $ping = $this->ping();
1608
    }
1609
1610 3
    if ($ping !== true) {
1611 3
      $this->connected = false;
1612 3
      $this->connect();
1613
    }
1614
1615 3
    return $this->isReady();
1616
  }
1617
1618
  /**
1619
   * Execute a "replace"-query.
1620
   *
1621
   * @param string      $table
1622
   * @param array       $data
1623
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1624
   *
1625
   * @return false|int <p>false on error</p>
1626
   *
1627
   * @throws QueryException
1628
   */
1629 1
  public function replace(string $table, array $data = [], string $databaseName = null)
1630
  {
1631
    // init
1632 1
    $table = \trim($table);
1633
1634 1
    if ($table === '') {
1635 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1636
1637 1
      return false;
1638
    }
1639
1640 1
    if (\count($data) === 0) {
1641 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1642
1643 1
      return false;
1644
    }
1645
1646
    // extracting column names
1647 1
    $columns = \array_keys($data);
1648 1
    foreach ($columns as $k => $_key) {
1649
      /** @noinspection AlterInForeachInspection */
1650 1
      $columns[$k] = $this->quote_string($_key);
1651
    }
1652
1653 1
    $columns = \implode(',', $columns);
1654
1655
    // extracting values
1656 1
    foreach ($data as $k => $_value) {
1657
      /** @noinspection AlterInForeachInspection */
1658 1
      $data[$k] = $this->secure($_value);
1659
    }
1660 1
    $values = \implode(',', $data);
1661
1662 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...
1663
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1664
    }
1665
1666 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1667
1668 1
    return $this->query($sql);
1669
  }
1670
1671
  /**
1672
   * Rollback in a transaction and end the transaction.
1673
   *
1674
   * @return bool <p>Boolean true on success, false otherwise.</p>
1675
   */
1676 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...
1677
  {
1678 4
    if ($this->_in_transaction === false) {
1679
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1680
1681
      return false;
1682
    }
1683
1684 4
    $return = \mysqli_rollback($this->link);
1685 4
    \mysqli_autocommit($this->link, true);
1686 4
    $this->_in_transaction = false;
1687
1688 4
    return $return;
1689
  }
1690
1691
  /**
1692
   * Try to secure a variable, so can you use it in sql-queries.
1693
   *
1694
   * <p>
1695
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1696
   * 1. parse into "int"
1697
   * </p><br />
1698
   *
1699
   * <p>
1700
   * <strong>float:</strong><br />
1701
   * 1. return "float"
1702
   * </p><br />
1703
   *
1704
   * <p>
1705
   * <strong>string:</strong><br />
1706
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1707
   * 2. \trim whitespace<br />
1708
   * 3. \trim '<br />
1709
   * 4. escape the string (and remove non utf-8 chars)<br />
1710
   * 5. \trim ' again (because we maybe removed some chars)<br />
1711
   * 6. add ' around the new string<br />
1712
   * </p><br />
1713
   *
1714
   * <p>
1715
   * <strong>array:</strong><br />
1716
   * 1. return null
1717
   * </p><br />
1718
   *
1719
   * <p>
1720
   * <strong>object:</strong><br />
1721
   * 1. return false
1722
   * </p><br />
1723
   *
1724
   * <p>
1725
   * <strong>null:</strong><br />
1726
   * 1. return null
1727
   * </p>
1728
   *
1729
   * @param mixed $var
1730
   *
1731
   * @return mixed
1732
   */
1733 44
  public function secure($var)
1734
  {
1735 44
    if ($var === '') {
1736 2
      return '';
1737
    }
1738
1739 44
    if ($var === "''") {
1740 1
      return "''";
1741
    }
1742
1743 44
    if ($var === null) {
1744
      if (
1745 2
          $this->_convert_null_to_empty_string === true
1746
      ) {
1747 1
        return "''";
1748
      }
1749
1750 2
      return 'NULL';
1751
    }
1752
1753 43
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1754 1
      return $var;
1755
    }
1756
1757 43
    if (\is_string($var)) {
1758 36
      $var = \trim(\trim($var), "'");
1759
    }
1760
1761 43
    $var = $this->escape($var, false, false, null);
1762
1763 43
    if (\is_string($var)) {
1764 36
      $var = "'" . \trim($var, "'") . "'";
1765
    }
1766
1767 43
    return $var;
1768
  }
1769
1770
  /**
1771
   * Execute a "select"-query.
1772
   *
1773
   * @param string       $table
1774
   * @param string|array $where
1775
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1776
   *
1777
   * @return false|Result <p>false on error</p>
1778
   *
1779
   * @throws QueryException
1780
   */
1781 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...
1782
  {
1783
    // init
1784 24
    $table = \trim($table);
1785
1786 24
    if ($table === '') {
1787 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1788
1789 1
      return false;
1790
    }
1791
1792 24
    if (\is_string($where)) {
1793 8
      $WHERE = $this->escape($where, false);
1794 17
    } elseif (\is_array($where)) {
1795 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1796
    } else {
1797 1
      $WHERE = '';
1798
    }
1799
1800 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...
1801
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1802
    }
1803
1804 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1805
1806 24
    return $this->query($sql);
1807
  }
1808
1809
  /**
1810
   * Selects a different database than the one specified on construction.
1811
   *
1812
   * @param string $database <p>Database name to switch to.</p>
1813
   *
1814
   * @return bool <p>Boolean true on success, false otherwise.</p>
1815
   */
1816
  public function select_db(string $database): bool
1817
  {
1818
    if (!$this->isReady()) {
1819
      return false;
1820
    }
1821
1822
    return mysqli_select_db($this->link, $database);
1823
  }
1824
1825
  /**
1826
   * Set the current charset.
1827
   *
1828
   * @param string $charset
1829
   *
1830
   * @return bool
1831
   */
1832 8
  public function set_charset(string $charset): bool
1833
  {
1834 8
    $charsetLower = strtolower($charset);
1835 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1836 6
      $charset = 'utf8';
1837
    }
1838 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1839 6
      $charset = 'utf8mb4';
1840
    }
1841
1842 8
    $this->charset = $charset;
1843
1844 8
    $return = mysqli_set_charset($this->link, $charset);
1845
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1846 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1847
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1848 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...
1849
1850 8
    return $return;
1851
  }
1852
1853
  /**
1854
   * Set the option to convert null to "''" (empty string).
1855
   *
1856
   * Used in secure() => select(), insert(), update(), delete()
1857
   *
1858
   * @deprecated It's not recommended to convert NULL into an empty string!
1859
   *
1860
   * @param $bool
1861
   *
1862
   * @return $this
1863
   */
1864 1
  public function set_convert_null_to_empty_string(bool $bool)
1865
  {
1866 1
    $this->_convert_null_to_empty_string = $bool;
1867
1868 1
    return $this;
1869
  }
1870
1871
  /**
1872
   * Enables or disables internal report functions
1873
   *
1874
   * @link http://php.net/manual/en/function.mysqli-report.php
1875
   *
1876
   * @param int $flags <p>
1877
   *                   <table>
1878
   *                   Supported flags
1879
   *                   <tr valign="top">
1880
   *                   <td>Name</td>
1881
   *                   <td>Description</td>
1882
   *                   </tr>
1883
   *                   <tr valign="top">
1884
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1885
   *                   <td>Turns reporting off</td>
1886
   *                   </tr>
1887
   *                   <tr valign="top">
1888
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1889
   *                   <td>Report errors from mysqli function calls</td>
1890
   *                   </tr>
1891
   *                   <tr valign="top">
1892
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1893
   *                   <td>
1894
   *                   Throw <b>mysqli_sql_exception</b> for errors
1895
   *                   instead of warnings
1896
   *                   </td>
1897
   *                   </tr>
1898
   *                   <tr valign="top">
1899
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1900
   *                   <td>Report if no index or bad index was used in a query</td>
1901
   *                   </tr>
1902
   *                   <tr valign="top">
1903
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1904
   *                   <td>Set all options (report all)</td>
1905
   *                   </tr>
1906
   *                   </table>
1907
   *                   </p>
1908
   *
1909
   * @return bool
1910
   */
1911
  public function set_mysqli_report(int $flags): bool
1912
  {
1913
    return \mysqli_report($flags);
1914
  }
1915
1916
  /**
1917
   * Show config errors by throw exceptions.
1918
   *
1919
   * @return bool
1920
   *
1921
   * @throws \InvalidArgumentException
1922
   */
1923 11
  public function showConfigError(): bool
1924
  {
1925
1926
    if (
1927 11
        !$this->hostname
1928
        ||
1929 10
        !$this->username
1930
        ||
1931 11
        !$this->database
1932
    ) {
1933
1934 3
      if (!$this->hostname) {
1935 1
        throw new \InvalidArgumentException('no-sql-hostname');
1936
      }
1937
1938 2
      if (!$this->username) {
1939 1
        throw new \InvalidArgumentException('no-sql-username');
1940
      }
1941
1942 1
      if (!$this->database) {
1943 1
        throw new \InvalidArgumentException('no-sql-database');
1944
      }
1945
1946
      return false;
1947
    }
1948
1949 8
    return true;
1950
  }
1951
1952
  /**
1953
   * alias: "beginTransaction()"
1954
   */
1955 1
  public function startTransaction(): bool
1956
  {
1957 1
    return $this->beginTransaction();
1958
  }
1959
1960
  /**
1961
   * Execute a callback inside a transaction.
1962
   *
1963
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1964
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1965
   *
1966
   * @return bool <p>Boolean true on success, false otherwise.</p>
1967
   */
1968 1
  public function transact($callback): bool
1969
  {
1970
    try {
1971
1972 1
      $beginTransaction = $this->beginTransaction();
1973 1
      if ($beginTransaction === false) {
1974 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1975
1976 1
        return false;
1977
      }
1978
1979 1
      $result = $callback($this);
1980 1
      if ($result === false) {
1981
        /** @noinspection ThrowRawExceptionInspection */
1982 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1983
      }
1984
1985 1
      return $this->commit();
1986
1987 1
    } catch (\Exception $e) {
1988
1989 1
      $this->rollback();
1990
1991 1
      return false;
1992
    }
1993
  }
1994
1995
  /**
1996
   * Execute a "update"-query.
1997
   *
1998
   * @param string       $table
1999
   * @param array        $data
2000
   * @param array|string $where
2001
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2002
   *
2003
   * @return false|int <p>false on error</p>
2004
   *
2005
   * @throws QueryException
2006
   */
2007 7
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2008
  {
2009
    // init
2010 7
    $table = \trim($table);
2011
2012 7
    if ($table === '') {
2013 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2014
2015 1
      return false;
2016
    }
2017
2018 7
    if (\count($data) === 0) {
2019 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2020
2021 2
      return false;
2022
    }
2023
2024 7
    $SET = $this->_parseArrayPair($data);
2025
2026 7
    if (\is_string($where)) {
2027 2
      $WHERE = $this->escape($where, false);
2028 6
    } elseif (\is_array($where)) {
2029 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2030
    } else {
2031 1
      $WHERE = '';
2032
    }
2033
2034 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...
2035
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2036
    }
2037
2038 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
2039
2040 7
    return $this->query($sql);
2041
  }
2042
2043
  /**
2044
   * Determine if database table exists
2045
   *
2046
   * @param string $table
2047
   *
2048
   * @return bool
2049
   */
2050 1
  public function table_exists(string $table): bool
2051
  {
2052 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2053
2054 1
    return $check !== false
2055
           &&
2056 1
           $check instanceof Result
2057
           &&
2058 1
           $check->num_rows > 0;
2059
  }
2060
2061
  /**
2062
   * Count number of rows found matching a specific query.
2063
   *
2064
   * @param string $query
2065
   *
2066
   * @return int
2067
   */
2068 1
  public function num_rows(string $query): int
2069
  {
2070 1
    $check = $this->query($query);
2071
2072
    if (
2073 1
        $check === false
2074
        ||
2075 1
        !$check instanceof Result
2076
    ) {
2077
      return 0;
2078
    }
2079
2080 1
    return $check->num_rows;
2081
  }
2082
}
2083