Completed
Push — master ( 0795a7...da9721 )
by Lars
01:58
created

DB::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 49
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 17
cts 17
cp 1
rs 9.2258
c 0
b 0
f 0
cc 1
eloc 29
nc 1
nop 11
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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