Completed
Push — master ( 3ed1ba...90dd46 )
by Lars
07:46
created

DB::__invoke()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\cache\Cache;
8
use voku\db\exceptions\DBConnectException;
9
use voku\db\exceptions\DBGoneAwayException;
10
use voku\db\exceptions\QueryException;
11
use voku\helper\UTF8;
12
13
/**
14
 * DB: This class can handle DB queries via MySQLi.
15
 *
16
 * @package voku\db
17
 */
18
final class DB
19
{
20
21
  /**
22
   * @var int
23
   */
24
  public $query_count = 0;
25
26
  /**
27
   * @var \mysqli|null
28
   */
29
  private $link;
30
31
  /**
32
   * @var bool
33
   */
34
  private $connected = false;
35
36
  /**
37
   * @var array
38
   */
39
  private $mysqlDefaultTimeFunctions;
40
41
  /**
42
   * @var string
43
   */
44
  private $hostname = '';
45
46
  /**
47
   * @var string
48
   */
49
  private $username = '';
50
51
  /**
52
   * @var string
53
   */
54
  private $password = '';
55
56
  /**
57
   * @var string
58
   */
59
  private $database = '';
60
61
  /**
62
   * @var int
63
   */
64
  private $port = 3306;
65
66
  /**
67
   * @var string
68
   */
69
  private $charset = 'utf8';
70
71
  /**
72
   * @var string
73
   */
74
  private $socket = '';
75
76
  /**
77
   * @var bool
78
   */
79
  private $session_to_db = false;
80
81
  /**
82
   * @var bool
83
   */
84
  private $_in_transaction = false;
85
86
  /**
87
   * @var bool
88
   */
89
  private $_convert_null_to_empty_string = false;
90
91
  /**
92
   * @var bool
93
   */
94
  private $_ssl = false;
95
96
  /**
97
   * The path name to the key file
98
   *
99
   * @var string
100
   */
101
  private $_clientkey;
102
103
  /**
104
   * The path name to the certificate file
105
   *
106
   * @var string
107
   */
108
  private $_clientcert;
109
110
  /**
111
   * The path name to the certificate authority file
112
   *
113
   * @var string
114
   */
115
  private $_cacert;
116
117
  /**
118
   * @var Debug
119
   */
120
  private $_debug;
121
122
  /**
123
   * __construct()
124
   *
125
   * @param string $hostname
126
   * @param string $username
127
   * @param string $password
128
   * @param string $database
129
   * @param int    $port
130
   * @param string $charset
131
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
132
   *                                      Use false to disable it.</p>
133
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
134
   *                                      Use false to disable it.</p>
135
   * @param string $logger_class_name
136
   * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
137
   * @param array  $extra_config          <p>
138
   *                                      'session_to_db' => bool<br>
139
   *                                      'socket'        => 'string (path)'<br>
140
   *                                      'ssl'           => bool<br>
141
   *                                      'clientkey'     => 'string (path)'<br>
142
   *                                      'clientcert'    => 'string (path)'<br>
143
   *                                      'cacert'        => 'string (path)'<br>
144
   *                                      </p>
145
   */
146 11
  private function __construct(string $hostname, string $username, string $password, string $database, $port, string $charset, bool $exit_on_error, bool $echo_on_error, string $logger_class_name, string $logger_level, array $extra_config = [])
147
  {
148 11
    $this->_debug = new Debug($this);
149
150 11
    $this->_loadConfig(
151 11
        $hostname,
152 11
        $username,
153 11
        $password,
154 11
        $database,
155 11
        $port,
156 11
        $charset,
157 11
        $exit_on_error,
158 11
        $echo_on_error,
159 11
        $logger_class_name,
160 11
        $logger_level,
161 11
        $extra_config
162
    );
163
164 8
    $this->connect();
165
166 5
    $this->mysqlDefaultTimeFunctions = [
167
      // Returns the current date.
168
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176
      'CURTIME()',
177
      // Synonym for NOW()
178
      'LOCALTIME()',
179
      // Synonym for NOW()
180
      'LOCALTIMESTAMP()',
181
      // Returns the current date and time.
182
      'NOW()',
183
      // Returns the time at which the function executes.
184
      'SYSDATE()',
185
      // Returns a UNIX timestamp.
186
      'UNIX_TIMESTAMP()',
187
      // Returns the current UTC date.
188
      'UTC_DATE()',
189
      // Returns the current UTC time.
190
      'UTC_TIME()',
191
      // Returns the current UTC date and time.
192
      'UTC_TIMESTAMP()',
193
    ];
194 5
  }
195
196
  /**
197
   * Prevent the instance from being cloned.
198
   *
199
   * @return void
200
   */
201
  private function __clone()
202
  {
203
  }
204
205
  /**
206
   * __destruct
207
   *
208
   */
209
  public function __destruct()
210
  {
211
    // close the connection only if we don't save PHP-SESSION's in DB
212
    if ($this->session_to_db === false) {
213
      $this->close();
214
    }
215
  }
216
217
  /**
218
   * @param null|string $sql
219
   * @param array       $bindings
220
   *
221
   * @return bool|int|Result|DB           <p>
222
   *                                      "DB" by "$sql" === null<br />
223
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
224
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
225
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
226
   *                                      "true" by e.g. "DROP"-queries<br />
227
   *                                      "false" on error
228
   *                                      </p>
229
   */
230 2
  public function __invoke(string $sql = null, array $bindings = [])
231
  {
232 2
    return null !== $sql ? $this->query($sql, $bindings) : $this;
233
  }
234
235
  /**
236
   * __wakeup
237
   *
238
   * @return void
239
   */
240 2
  public function __wakeup()
241
  {
242 2
    $this->reconnect();
243 2
  }
244
245
  /**
246
   * Load the config from the constructor.
247
   *
248
   * @param string $hostname
249
   * @param string $username
250
   * @param string $password
251
   * @param string $database
252
   * @param int    $port                  <p>default is (int)3306</p>
253
   * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
254
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
255
   *                                      Use false to disable it.</p>
256
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
257
   *                                      Use false to disable it.</p>
258
   * @param string $logger_class_name
259
   * @param string $logger_level
260
   * @param array  $extra_config          <p>
261
   *                                      'session_to_db' => false|true<br>
262
   *                                      'socket' => 'string (path)'<br>
263
   *                                      'ssl' => 'bool'<br>
264
   *                                      'clientkey' => 'string (path)'<br>
265
   *                                      'clientcert' => 'string (path)'<br>
266
   *                                      'cacert' => 'string (path)'<br>
267
   *                                      </p>
268
   *
269
   * @return bool
270
   */
271 11
  private function _loadConfig(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
   * @param array $extra_config           <p>
321
   *                                      'session_to_db' => false|true<br>
322
   *                                      'socket' => 'string (path)'<br>
323
   *                                      'ssl' => 'bool'<br>
324
   *                                      'clientkey' => 'string (path)'<br>
325
   *                                      'clientcert' => 'string (path)'<br>
326
   *                                      'cacert' => 'string (path)'<br>
327
   *                                      </p>
328
   */
329 11
  public function setConfigExtra(array $extra_config)
330
  {
331 11
    if (isset($extra_config['session_to_db'])) {
332
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
333
    }
334
335 11
    if (isset($extra_config['socket'])) {
336
      $this->socket = $extra_config['socket'];
337
    }
338
339 11
    if (isset($extra_config['ssl'])) {
340
      $this->_ssl = $extra_config['ssl'];
341
    }
342
343 11
    if (isset($extra_config['clientkey'])) {
344
      $this->_clientkey = $extra_config['clientkey'];
345
    }
346
347 11
    if (isset($extra_config['clientcert'])) {
348
      $this->_clientcert = $extra_config['clientcert'];
349
    }
350
351 11
    if (isset($extra_config['cacert'])) {
352
      $this->_cacert = $extra_config['cacert'];
353
    }
354 11
  }
355
356
  /**
357
   * Parses arrays with value pairs and generates SQL to use in queries.
358
   *
359
   * @param array  $arrayPair
360
   * @param string $glue <p>This is the separator.</p>
361
   *
362
   * @return string
363
   *
364
   * @internal
365
   */
366 30
  public function _parseArrayPair(array $arrayPair, string $glue = ','): string
367
  {
368
    // init
369 30
    $sql = '';
370
371 30
    if (\count($arrayPair) === 0) {
372
      return '';
373
    }
374
375 30
    $arrayPairCounter = 0;
376 30
    foreach ($arrayPair as $_key => $_value) {
377 30
      $_connector = '=';
378 30
      $_glueHelper = '';
379 30
      $_key_upper = \strtoupper($_key);
380
381 30
      if (\strpos($_key_upper, ' NOT') !== false) {
382 2
        $_connector = 'NOT';
383
      }
384
385 30
      if (\strpos($_key_upper, ' IS') !== false) {
386 1
        $_connector = 'IS';
387
      }
388
389 30
      if (\strpos($_key_upper, ' IS NOT') !== false) {
390 1
        $_connector = 'IS NOT';
391
      }
392
393 30
      if (\strpos($_key_upper, ' IN') !== false) {
394 1
        $_connector = 'IN';
395
      }
396
397 30
      if (\strpos($_key_upper, ' NOT IN') !== false) {
398 1
        $_connector = 'NOT IN';
399
      }
400
401 30
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
402 1
        $_connector = 'BETWEEN';
403
      }
404
405 30
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
406 1
        $_connector = 'NOT BETWEEN';
407
      }
408
409 30
      if (\strpos($_key_upper, ' LIKE') !== false) {
410 2
        $_connector = 'LIKE';
411
      }
412
413 30
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
414 2
        $_connector = 'NOT LIKE';
415
      }
416
417 30 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
418 4
        $_connector = '>';
419
      }
420
421 30 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
422 1
        $_connector = '<';
423
      }
424
425 30
      if (\strpos($_key_upper, ' >=') !== false) {
426 4
        $_connector = '>=';
427
      }
428
429 30
      if (\strpos($_key_upper, ' <=') !== false) {
430 1
        $_connector = '<=';
431
      }
432
433 30
      if (\strpos($_key_upper, ' <>') !== false) {
434 1
        $_connector = '<>';
435
      }
436
437 30
      if (\strpos($_key_upper, ' OR') !== false) {
438 2
        $_glueHelper = 'OR';
439
      }
440
441 30
      if (\strpos($_key_upper, ' AND') !== false) {
442 1
        $_glueHelper = 'AND';
443
      }
444
445 30
      if (\is_array($_value) === true) {
446 2
        foreach ($_value as $oldKey => $oldValue) {
447 2
          $_value[$oldKey] = $this->secure($oldValue);
448
        }
449
450 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
451 1
          $_value = '(' . \implode(',', $_value) . ')';
452 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
453 2
          $_value = '(' . \implode(' AND ', $_value) . ')';
454
        }
455
456
      } else {
457 30
        $_value = $this->secure($_value);
458
      }
459
460 30
      $quoteString = $this->quote_string(
461 30
          \trim(
462 30
              \str_ireplace(
463
                  [
464 30
                      $_connector,
465 30
                      $_glueHelper,
466
                  ],
467 30
                  '',
468 30
                  $_key
469
              )
470
          )
471
      );
472
473 30
      $_value = (array)$_value;
474
475 30
      if (!$_glueHelper) {
476 30
        $_glueHelper = $glue;
477
      }
478
479 30
      $tmpCounter = 0;
480 30
      foreach ($_value as $valueInner) {
481
482 30
        $_glueHelperInner = $_glueHelper;
483
484 30
        if ($arrayPairCounter === 0) {
485
486 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
487 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
488 30
          } elseif ($tmpCounter === 0) {
489 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
490
          }
491
492 26
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
493 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
494
        }
495
496 30
        if (\is_string($valueInner) && $valueInner === '') {
497
          $valueInner = "''";
498
        }
499
500 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
501 30
        $tmpCounter++;
502
      }
503
504 30
      if ($_glueHelper === 'OR') {
505 2
        $sql .= ' ) ';
506
      }
507
508 30
      $arrayPairCounter++;
509
    }
510
511 30
    return $sql;
512
  }
513
514
  /**
515
   * _parseQueryParams
516
   *
517
   * @param string $sql
518
   * @param array  $params
519
   *
520
   * @return array <p>with the keys -> 'sql', 'params'</p>
521
   */
522 13
  private function _parseQueryParams(string $sql, array $params = []): array
523
  {
524
    // is there anything to parse?
525 View Code Duplication
    if (
526 13
        \strpos($sql, '?') === false
527
        ||
528 13
        \count($params) === 0
529
    ) {
530 11
      return ['sql' => $sql, 'params' => $params];
531
    }
532
533 3
    static $PARSE_KEY_CACHE = null;
534 3 View Code Duplication
    if ($PARSE_KEY_CACHE === null) {
535 1
      $PARSE_KEY_CACHE = \md5(\uniqid((string)\mt_rand(), true));
536
    }
537
538 3
    $sql = \str_replace('?', $PARSE_KEY_CACHE, $sql);
539
540 3
    $k = 0;
541 3
    while (\strpos($sql, $PARSE_KEY_CACHE) !== false) {
542 3
      $sql = UTF8::str_replace_first(
543 3
          $PARSE_KEY_CACHE,
544 3
          (string)(isset($params[$k]) ? $this->secure($params[$k]) : ''),
545 3
          $sql
546
      );
547
548 3
      if (isset($params[$k])) {
549 3
        unset($params[$k]);
550
      }
551
552 3
      $k++;
553
    }
554
555 3
    return ['sql' => $sql, 'params' => $params];
556
  }
557
558
  /**
559
   * Gets the number of affected rows in a previous MySQL operation.
560
   *
561
   * @return int
562
   */
563 12
  public function affected_rows(): int
564
  {
565 12
    return \mysqli_affected_rows($this->link);
566
  }
567
568
  /**
569
   * Begins a transaction, by turning off auto commit.
570
   *
571
   * @return bool <p>This will return true or false indicating success of transaction</p>
572
   */
573 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...
574
  {
575 6
    if ($this->_in_transaction === true) {
576 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
577
578 2
      return false;
579
    }
580
581 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
582 6
    $this->_in_transaction = true;
583 6
    $return = \mysqli_autocommit($this->link, false);
584 6
    if ($return === false) {
585
      $this->_in_transaction = false;
586
    }
587
588 6
    return $return;
589
  }
590
591
  /**
592
   * Clear the errors in "_debug->_errors".
593
   *
594
   * @return bool
595
   */
596 6
  public function clearErrors(): bool
597
  {
598 6
    return $this->_debug->clearErrors();
599
  }
600
601
  /**
602
   * Closes a previously opened database connection.
603
   */
604 2
  public function close(): bool
605
  {
606 2
    $this->connected = false;
607
608
    if (
609 2
        $this->link
610
        &&
611 2
        $this->link instanceof \mysqli
612
    ) {
613 2
      $result = \mysqli_close($this->link);
614 2
      $this->link = null;
615
616 2
      return $result;
617
    }
618
619 1
    return false;
620
  }
621
622
  /**
623
   * Commits the current transaction and end the transaction.
624
   *
625
   * @return bool <p>Boolean true on success, false otherwise.</p>
626
   */
627 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...
628
  {
629 2
    if ($this->_in_transaction === false) {
630
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
631
632
      return false;
633
    }
634
635 2
    $return = \mysqli_commit($this->link);
636 2
    \mysqli_autocommit($this->link, true);
637 2
    $this->_in_transaction = false;
638
639 2
    return $return;
640
  }
641
642
  /**
643
   * Open a new connection to the MySQL server.
644
   *
645
   * @return bool
646
   *
647
   * @throws DBConnectException
648
   */
649 10
  public function connect(): bool
650
  {
651 10
    if ($this->isReady()) {
652 1
      return true;
653
    }
654
655 10
    $flags = null;
656
657 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
658
    try {
659 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...
660
661 10
      if (Helper::isMysqlndIsUsed() === true) {
662 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
663
      }
664
665 10
      if ($this->_ssl === true) {
666
667
        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...
668
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
669
        }
670
671
        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...
672
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
673
        }
674
675
        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...
676
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
677
        }
678
679
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
680
681
        /** @noinspection PhpParamsInspection */
682
        \mysqli_ssl_set(
683
            $this->link,
684
            $this->_clientkey,
685
            $this->_clientcert,
686
            $this->_cacert,
687
            null,
688
            null
689
        );
690
691
        $flags = MYSQLI_CLIENT_SSL;
692
      }
693
694
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
695 10
      $this->connected = @\mysqli_real_connect(
696 10
          $this->link,
697 10
          $this->hostname,
698 10
          $this->username,
699 10
          $this->password,
700 10
          $this->database,
701 10
          $this->port,
702 10
          $this->socket,
703 10
          (int)$flags
704
      );
705
706 3
    } catch (\Exception $e) {
707 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
708 3
      $this->_debug->displayError($error, true);
709
      throw new DBConnectException($error, 100, $e);
710
    }
711 7
    \mysqli_report(MYSQLI_REPORT_OFF);
712
713 7
    $errno = \mysqli_connect_errno();
714 7
    if (!$this->connected || $errno) {
715
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
716
      $this->_debug->displayError($error, true);
717
      throw new DBConnectException($error, 101);
718
    }
719
720 7
    $this->set_charset($this->charset);
721
722 7
    return $this->isReady();
723
  }
724
725
  /**
726
   * Execute a "delete"-query.
727
   *
728
   * @param string       $table
729
   * @param string|array $where
730
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
731
   *
732
   * @return false|int <p>false on error</p>
733
   *
734
   *    * @throws QueryException
735
   */
736 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...
737
  {
738
    // init
739 2
    $table = \trim($table);
740
741 2
    if ($table === '') {
742 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
743
744 1
      return false;
745
    }
746
747 2
    if (\is_string($where)) {
748 1
      $WHERE = $this->escape($where, false);
749 2
    } elseif (\is_array($where)) {
750 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
751
    } else {
752 1
      $WHERE = '';
753
    }
754
755 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...
756
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
757
    }
758
759 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
760
761 2
    return $this->query($sql);
762
  }
763
764
  /**
765
   * Ends a transaction and commits if no errors, then ends autocommit.
766
   *
767
   * @return bool <p>This will return true or false indicating success of transactions.</p>
768
   */
769 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...
770
  {
771 4
    if ($this->_in_transaction === false) {
772
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
773
774
      return false;
775
    }
776
777 4
    if (!$this->errors()) {
778 1
      $return = \mysqli_commit($this->link);
779
    } else {
780 3
      $this->rollback();
781 3
      $return = false;
782
    }
783
784 4
    \mysqli_autocommit($this->link, true);
785 4
    $this->_in_transaction = false;
786
787 4
    return $return;
788
  }
789
790
  /**
791
   * Get all errors from "$this->_errors".
792
   *
793
   * @return array|false <p>false === on errors</p>
794
   */
795 4
  public function errors()
796
  {
797 4
    $errors = $this->_debug->getErrors();
798
799 4
    return \count($errors) > 0 ? $errors : false;
800
  }
801
802
  /**
803
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
804
   *
805
   * @param mixed $sql    <p>The SQL string.</p>
806
   * @param array $params <p>An array of key-value bindings.</p>
807
   *
808
   * @return array <p>with the keys -> 'sql', 'params'</p>
809
   */
810 14
  public function _parseQueryParamsByName(string $sql, array $params = []): array
811
  {
812
    // is there anything to parse?
813 View Code Duplication
    if (
814 14
        \strpos($sql, ':') === false
815
        ||
816 14
        \count($params) === 0
817
    ) {
818 3
      return ['sql' => $sql, 'params' => $params];
819
    }
820
821 12
    static $PARSE_KEY_CACHE = null;
822 12 View Code Duplication
    if ($PARSE_KEY_CACHE === null) {
823 1
      $PARSE_KEY_CACHE = \md5(\uniqid((string)\mt_rand(), true));
824
    }
825
826 12
    foreach ($params as $name => $value) {
827 12
      $nameTmp = $name;
828 12
      if (\strpos($name, ':') === 0) {
829 10
        $nameTmp = \substr($name, 1);
830
      }
831
832 12
      $parseKeyInner = $nameTmp . '-' . $PARSE_KEY_CACHE;
833 12
      $sql = \str_replace(':' . $nameTmp, $parseKeyInner, $sql);
834
    }
835
836 12
    foreach ($params as $name => $value) {
837 12
      $nameTmp = $name;
838 12
      if (\strpos($name, ':') === 0) {
839 10
        $nameTmp = \substr($name, 1);
840
      }
841
842 12
      $parseKeyInner = $nameTmp . '-' . $PARSE_KEY_CACHE;
843 12
      $sqlBefore = $sql;
844 12
      $secureParamValue = $this->secure($params[$name]);
845
846 12
      while (\strpos($sql, $parseKeyInner) !== false) {
847 12
        $sql = UTF8::str_replace_first(
848 12
            $parseKeyInner,
849 12
            (string)$secureParamValue,
850 12
            $sql
851
        );
852
      }
853
854 12
      if ($sqlBefore !== $sql) {
855 12
        unset($params[$name]);
856
      }
857
    }
858
859 12
    return ['sql' => $sql, 'params' => $params];
860
  }
861
862
  /**
863
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
864
   *
865
   * @param mixed     $var           boolean: convert into "integer"<br />
866
   *                                 int: int (don't change it)<br />
867
   *                                 float: float (don't change it)<br />
868
   *                                 null: null (don't change it)<br />
869
   *                                 array: run escape() for every key => value<br />
870
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
871
   * @param bool      $stripe_non_utf8
872
   * @param bool      $html_entity_decode
873
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
874
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
875
   *                                 <strong>null</strong> => Convert the array into null, every time.
876
   *
877
   * @return mixed
878
   */
879 55
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
880
  {
881
    // [empty]
882 55
    if ($var === '') {
883 2
      return '';
884
    }
885
886
    // ''
887 55
    if ($var === "''") {
888
      return "''";
889
    }
890
891
    // NULL
892 55
    if ($var === null) {
893
      if (
894 2
          $this->_convert_null_to_empty_string === true
895
      ) {
896
        return "''";
897
      }
898
899 2
      return 'NULL';
900
    }
901
902
    // save the current value as int (for later usage)
903 55
    if (!\is_object($var)) {
904 55
      $varInt = (int)$var;
905
    }
906
907
    // "int" || int || bool
908
    if (
909 55
        \is_int($var)
910
        ||
911 52
        \is_bool($var)
912
        ||
913
        (
914 52
            isset($varInt, $var[0])
915
            &&
916 52
            $var[0] != '0'
917
            &&
918 55
            (string)$varInt == $var
0 ignored issues
show
Bug introduced by
The variable $varInt does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
919
        )
920
    ) {
921 32
      return (int)$var;
922
    }
923
924
    // float
925 52
    if (\is_float($var)) {
926 5
      return $var;
927
    }
928
929
    // array
930 52
    if (\is_array($var)) {
931
932 4
      if ($convert_array === null) {
933 3
        return null;
934
      }
935
936 2
      $varCleaned = [];
937 2
      foreach ((array)$var as $key => $value) {
938
939 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
940 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
941
942
        /** @noinspection OffsetOperationsInspection */
943 2
        $varCleaned[$key] = $value;
944
      }
945
946 2
      if ($convert_array === true) {
947 1
        $varCleaned = \implode(',', $varCleaned);
948
949 1
        return $varCleaned;
950
      }
951
952 2
      return $varCleaned;
953
    }
954
955
    // "string"
956
    if (
957 52
        \is_string($var)
958
        ||
959
        (
960 3
            \is_object($var)
961
            &&
962 52
            \method_exists($var, '__toString')
963
        )
964
    ) {
965 52
      $var = (string)$var;
966
967 52
      if ($stripe_non_utf8 === true) {
968 11
        $var = UTF8::cleanup($var);
969
      }
970
971 52
      if ($html_entity_decode === true) {
972
        // use no-html-entity for db
973 1
        $var = UTF8::html_entity_decode($var);
974
      }
975
976 52
      $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
977
978 52
      $var = \mysqli_real_escape_string($this->getLink(), $var);
979
980 52
      return (string)$var;
981
982
    }
983
984
    // "DateTime"-object
985 3
    if ($var instanceof \DateTime) {
986 3
      return $this->escape($var->format('Y-m-d H:i:s'), false);
987
    }
988
989 2
    return false;
990
  }
991
992
  /**
993
   * Execute select/insert/update/delete sql-queries.
994
   *
995
   * @param string    $query    <p>sql-query</p>
996
   * @param bool      $useCache <p>use cache?</p>
997
   * @param int       $cacheTTL <p>cache-ttl in seconds</p>
998
   * @param self|null $db       optional <p>the database connection</p>
999
   *
1000
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
1001
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
1002
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1003
   *               "true" by e.g. "DROP"-queries<br />
1004
   *               "false" on error
1005
   *
1006
   * @throws QueryException
1007
   */
1008 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1009
  {
1010
    // init
1011 3
    $cacheKey = null;
1012 3
    if (!$db) {
1013 3
      $db = self::getInstance();
1014
    }
1015
1016 3 View Code Duplication
    if ($useCache === true) {
1017 1
      $cache = new Cache(null, null, false, $useCache);
1018 1
      $cacheKey = 'sql-' . \md5($query);
1019
1020
      if (
1021 1
          $cache->getCacheIsReady() === true
1022
          &&
1023 1
          $cache->existsItem($cacheKey)
1024
      ) {
1025 1
        return $cache->getItem($cacheKey);
1026
      }
1027
1028
    } else {
1029 3
      $cache = false;
1030
    }
1031
1032 3
    $result = $db->query($query);
1033
1034 3
    if ($result instanceof Result) {
1035
1036 1
      $return = $result->fetchAllArray();
1037
1038
      // save into the cache
1039 View Code Duplication
      if (
1040 1
          $cacheKey !== null
1041
          &&
1042 1
          $useCache === true
1043
          &&
1044 1
          $cache instanceof Cache
1045
          &&
1046 1
          $cache->getCacheIsReady() === true
1047
      ) {
1048 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1049
      }
1050
1051
    } else {
1052 2
      $return = $result;
1053
    }
1054
1055 3
    return $return;
1056
  }
1057
1058
  /**
1059
   * Get all table-names via "SHOW TABLES".
1060
   *
1061
   * @return array
1062
   */
1063 1
  public function getAllTables(): array
1064
  {
1065 1
    $query = 'SHOW TABLES';
1066 1
    $result = $this->query($query);
1067
1068 1
    return $result->fetchAllArray();
1069
  }
1070
1071
  /**
1072
   * @return Debug
1073
   */
1074 9
  public function getDebugger(): Debug
1075
  {
1076 9
    return $this->_debug;
1077
  }
1078
1079
  /**
1080
   * Get errors from "$this->_errors".
1081
   *
1082
   * @return array
1083
   */
1084 1
  public function getErrors(): array
1085
  {
1086 1
    return $this->_debug->getErrors();
1087
  }
1088
1089
  /**
1090
   * getInstance()
1091
   *
1092
   * @param string $hostname
1093
   * @param string $username
1094
   * @param string $password
1095
   * @param string $database
1096
   * @param int    $port                 <p>default is (int)3306</p>
1097
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1098
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1099
   *                                     Use false to disable it.</p>
1100
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1101
   *                                     Use false to disable it.</p>
1102
   * @param string $logger_class_name
1103
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1104
   * @param array  $extra_config         <p>
1105
   *                                     're_connect'    => bool<br>
1106
   *                                     'session_to_db' => bool<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 self
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 self[]
1120
     */
1121 97
    static $instance = [];
1122
1123
    /**
1124
     * @var $firstInstance self
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
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1134
        $firstInstance->reconnect(true);
1135
      }
1136
1137 14
      return $firstInstance;
1138
    }
1139
1140 97
    $extra_config_string = '';
1141 97
    if (\is_array($extra_config) === true) {
1142 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1143 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1144
      }
1145
    } else {
1146
      // only for backward compatibility
1147
      $extra_config_string = (int)$extra_config;
1148
    }
1149
1150 97
    $connection = \md5(
1151 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1152
    );
1153
1154 97
    if (!isset($instance[$connection])) {
1155 11
      $instance[$connection] = new self(
1156 11
          $hostname,
1157 11
          $username,
1158 11
          $password,
1159 11
          $database,
1160 11
          $port,
1161 11
          $charset,
1162 11
          $exit_on_error,
1163 11
          $echo_on_error,
1164 11
          $logger_class_name,
1165 11
          $logger_level,
1166 11
          $extra_config
1167
      );
1168
1169 5
      if (null === $firstInstance) {
1170 1
        $firstInstance = $instance[$connection];
1171
      }
1172
    }
1173
1174 97
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1175
      $instance[$connection]->reconnect(true);
1176
    }
1177
1178 97
    return $instance[$connection];
1179
  }
1180
1181
  /**
1182
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1183
   *
1184
   * @return \mysqli
1185
   */
1186 56
  public function getLink(): \mysqli
1187
  {
1188 56
    return $this->link;
1189
  }
1190
1191
  /**
1192
   * Get the current charset.
1193
   *
1194
   * @return string
1195
   */
1196 1
  public function get_charset(): string
1197
  {
1198 1
    return $this->charset;
1199
  }
1200
1201
  /**
1202
   * Check if we are in a transaction.
1203
   *
1204
   * @return bool
1205
   */
1206
  public function inTransaction(): bool
1207
  {
1208
    return $this->_in_transaction;
1209
  }
1210
1211
  /**
1212
   * Execute a "insert"-query.
1213
   *
1214
   * @param string      $table
1215
   * @param array       $data
1216
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1217
   *
1218
   * @return false|int <p>false on error</p>
1219
   *
1220
   * @throws QueryException
1221
   */
1222 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1223
  {
1224
    // init
1225 28
    $table = \trim($table);
1226
1227 28
    if ($table === '') {
1228 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1229
1230 2
      return false;
1231
    }
1232
1233 27
    if (\count($data) === 0) {
1234 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1235
1236 3
      return false;
1237
    }
1238
1239 25
    $SET = $this->_parseArrayPair($data);
1240
1241 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...
1242
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1243
    }
1244
1245 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET";
1246
1247 25
    return $this->query($sql);
1248
  }
1249
1250
  /**
1251
   * Returns the auto generated id used in the last query.
1252
   *
1253
   * @return int|string
1254
   */
1255 58
  public function insert_id()
1256
  {
1257 58
    return \mysqli_insert_id($this->link);
1258
  }
1259
1260
  /**
1261
   * Check if db-connection is ready.
1262
   *
1263
   * @return boolean
1264
   */
1265 106
  public function isReady(): bool
1266
  {
1267 106
    return $this->connected ? true : false;
1268
  }
1269
1270
  /**
1271
   * Get the last sql-error.
1272
   *
1273
   * @return string|false <p>false === there was no error</p>
1274
   */
1275 1
  public function lastError()
1276
  {
1277 1
    $errors = $this->_debug->getErrors();
1278
1279 1
    return \count($errors) > 0 ? end($errors) : false;
1280
  }
1281
1282
  /**
1283
   * Execute a sql-multi-query.
1284
   *
1285
   * @param string $sql
1286
   *
1287
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1288
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1289
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1290
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1291
   *
1292
   * @throws QueryException
1293
   */
1294 1
  public function multi_query(string $sql)
1295
  {
1296 1
    if (!$this->isReady()) {
1297
      return false;
1298
    }
1299
1300 1 View Code Duplication
    if (!$sql || $sql === '') {
1301 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1302
1303 1
      return false;
1304
    }
1305
1306 1
    $query_start_time = microtime(true);
1307 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1308 1
    $query_duration = microtime(true) - $query_start_time;
1309
1310 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1311
1312 1
    $returnTheResult = false;
1313 1
    $result = [];
1314
1315 1
    if ($resultTmp) {
1316
      do {
1317
1318 1
        $resultTmpInner = \mysqli_store_result($this->link);
1319
1320 1
        if ($resultTmpInner instanceof \mysqli_result) {
1321
1322 1
          $returnTheResult = true;
1323 1
          $result[] = new Result($sql, $resultTmpInner);
1324
1325
        } else {
1326
1327
          // is the query successful
1328 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1329 1
            $result[] = true;
1330
          } else {
1331
            $result[] = false;
1332
          }
1333
1334
        }
1335
1336 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1337
1338
    } else {
1339
1340
      // log the error query
1341 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1342
1343 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1344
    }
1345
1346
    // return the result only if there was a "SELECT"-query
1347 1
    if ($returnTheResult === true) {
1348 1
      return $result;
1349
    }
1350
1351
    if (
1352 1
        \count($result) > 0
1353
        &&
1354 1
        \in_array(false, $result, true) === false
1355
    ) {
1356 1
      return true;
1357
    }
1358
1359
    return false;
1360
  }
1361
1362
  /**
1363
   * Pings a server connection, or tries to reconnect
1364
   * if the connection has gone down.
1365
   *
1366
   * @return boolean
1367
   */
1368 3
  public function ping(): bool
1369
  {
1370
    if (
1371 3
        $this->link
1372
        &&
1373 3
        $this->link instanceof \mysqli
1374
    ) {
1375
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1376 2
      return (bool)@\mysqli_ping($this->link);
1377
    }
1378
1379 1
    return false;
1380
  }
1381
1382
  /**
1383
   * Get a new "Prepare"-Object for your sql-query.
1384
   *
1385
   * @param string $query
1386
   *
1387
   * @return Prepare
1388
   */
1389 2
  public function prepare(string $query): Prepare
1390
  {
1391 2
    return new Prepare($this, $query);
1392
  }
1393
1394
  /**
1395
   * Execute a sql-query and return the result-array for select-statements.
1396
   *
1397
   * @param string $query
1398
   *
1399
   * @return mixed
1400
   * @deprecated
1401
   * @throws \Exception
1402
   */
1403 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...
1404
  {
1405 1
    $db = self::getInstance();
1406
1407 1
    $args = \func_get_args();
1408
    /** @noinspection SuspiciousAssignmentsInspection */
1409 1
    $query = \array_shift($args);
1410 1
    $query = \str_replace('?', '%s', $query);
1411 1
    $args = \array_map(
1412
        [
1413 1
            $db,
1414 1
            'escape',
1415
        ],
1416 1
        $args
1417
    );
1418 1
    \array_unshift($args, $query);
1419 1
    $query = \sprintf(...$args);
1420 1
    $result = $db->query($query);
1421
1422 1
    if ($result instanceof Result) {
1423 1
      return $result->fetchAllArray();
1424
    }
1425
1426 1
    return $result;
1427
  }
1428
1429
  /**
1430
   * Execute a sql-query.
1431
   *
1432
   * @param string        $sql            <p>The sql query-string.</p>
1433
   *
1434
   * @param array|boolean $params         <p>
1435
   *                                      "array" of sql-query-parameters<br/>
1436
   *                                      "false" if you don't need any parameter (default)<br/>
1437
   *                                      </p>
1438
   *
1439
   * @return bool|int|Result              <p>
1440
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1441
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1442
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1443
   *                                      "true" by e.g. "DROP"-queries<br />
1444
   *                                      "false" on error
1445
   *                                      </p>
1446
   *
1447
   * @throws QueryException
1448
   */
1449 96
  public function query(string $sql = '', $params = false)
1450
  {
1451 96
    if (!$this->isReady()) {
1452
      return false;
1453
    }
1454
1455 96 View Code Duplication
    if (!$sql || $sql === '') {
1456 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1457
1458 4
      return false;
1459
    }
1460
1461
    if (
1462 94
        $params !== false
1463
        &&
1464 94
        \is_array($params)
1465
        &&
1466 94
        \count($params) > 0
1467
    ) {
1468 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1469 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1470 13
      $sql = $parseQueryParamsByName['sql'];
1471
    }
1472
1473
    // DEBUG
1474
    // var_dump($params);
1475
    // echo $sql . "\n";
1476
1477 94
    $query_start_time = microtime(true);
1478 94
    $query_result = \mysqli_real_query($this->link, $sql);
1479 94
    $query_duration = microtime(true) - $query_start_time;
1480
1481 94
    $this->query_count++;
1482
1483 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1484 94
    if ($mysqli_field_count) {
1485 62
      $result = \mysqli_store_result($this->link);
1486
    } else {
1487 64
      $result = $query_result;
1488
    }
1489
1490 94
    if ($result instanceof \mysqli_result) {
1491
1492
      // log the select query
1493 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1494
1495
      // return query result object
1496 61
      return new Result($sql, $result);
1497
    }
1498
1499 66
    if ($query_result === true) {
1500
1501
      // "INSERT" || "REPLACE"
1502 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1503 58
        $insert_id = (int)$this->insert_id();
1504 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1505
1506 58
        return $insert_id;
1507
      }
1508
1509
      // "UPDATE" || "DELETE"
1510 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1511 12
        $affected_rows = $this->affected_rows();
1512 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1513
1514 12
        return $affected_rows;
1515
      }
1516
1517
      // log the ? query
1518 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1519
1520 27
      return true;
1521
    }
1522
1523
    // log the error query
1524 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1525
1526 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1527
  }
1528
1529
  /**
1530
   * Error-handling for the sql-query.
1531
   *
1532
   * @param string     $errorMessage
1533
   * @param int        $errorNumber
1534
   * @param string     $sql
1535
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1536
   * @param bool       $sqlMultiQuery
1537
   *
1538
   * @throws QueryException
1539
   * @throws DBGoneAwayException
1540
   *
1541
   * @return mixed|false
1542
   */
1543 13
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1544
  {
1545
    if (
1546 13
        $errorMessage === 'DB server has gone away'
1547
        ||
1548 12
        $errorMessage === 'MySQL server has gone away'
1549
        ||
1550 13
        $errorNumber === 2006
1551
    ) {
1552 1
      static $RECONNECT_COUNTER;
1553
1554
      // exit if we have more then 3 "DB server has gone away"-errors
1555 1
      if ($RECONNECT_COUNTER > 3) {
1556
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1557
        throw new DBGoneAwayException($errorMessage);
1558
      }
1559
1560 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1561
1562
      // reconnect
1563 1
      $RECONNECT_COUNTER++;
1564 1
      $this->reconnect(true);
1565
1566
      // re-run the current (non multi) query
1567 1
      if ($sqlMultiQuery === false) {
1568 1
        return $this->query($sql, $sqlParams);
1569
      }
1570
1571
      return false;
1572
    }
1573
1574 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1575
1576 12
    $force_exception_after_error = null; // auto
1577 12
    if ($this->_in_transaction === true) {
1578 4
      $force_exception_after_error = false;
1579
    }
1580
    // this query returned an error, we must display it (only for dev) !!!
1581
1582 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1583
1584 12
    return false;
1585
  }
1586
1587
  /**
1588
   * Quote && Escape e.g. a table name string.
1589
   *
1590
   * @param mixed $str
1591
   *
1592
   * @return string
1593
   */
1594 36
  public function quote_string($str): string
1595
  {
1596 36
    $str = \str_replace(
1597 36
        '`',
1598 36
        '``',
1599 36
        \trim(
1600 36
            (string)$this->escape($str, false),
1601 36
            '`'
1602
        )
1603
    );
1604
1605 36
    return '`' . $str . '`';
1606
  }
1607
1608
  /**
1609
   * Reconnect to the MySQL-Server.
1610
   *
1611
   * @param bool $checkViaPing
1612
   *
1613
   * @return bool
1614
   */
1615 3
  public function reconnect(bool $checkViaPing = false): bool
1616
  {
1617 3
    $ping = false;
1618
1619 3
    if ($checkViaPing === true) {
1620 2
      $ping = $this->ping();
1621
    }
1622
1623 3
    if ($ping !== true) {
1624 3
      $this->connected = false;
1625 3
      $this->connect();
1626
    }
1627
1628 3
    return $this->isReady();
1629
  }
1630
1631
  /**
1632
   * Execute a "replace"-query.
1633
   *
1634
   * @param string      $table
1635
   * @param array       $data
1636
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1637
   *
1638
   * @return false|int <p>false on error</p>
1639
   *
1640
   * @throws QueryException
1641
   */
1642 1
  public function replace(string $table, array $data = [], string $databaseName = null)
1643
  {
1644
    // init
1645 1
    $table = \trim($table);
1646
1647 1
    if ($table === '') {
1648 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1649
1650 1
      return false;
1651
    }
1652
1653 1
    if (\count($data) === 0) {
1654 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1655
1656 1
      return false;
1657
    }
1658
1659
    // extracting column names
1660 1
    $columns = \array_keys($data);
1661 1
    foreach ($columns as $k => $_key) {
1662
      /** @noinspection AlterInForeachInspection */
1663 1
      $columns[$k] = $this->quote_string($_key);
1664
    }
1665
1666 1
    $columns = \implode(',', $columns);
1667
1668
    // extracting values
1669 1
    foreach ($data as $k => $_value) {
1670
      /** @noinspection AlterInForeachInspection */
1671 1
      $data[$k] = $this->secure($_value);
1672
    }
1673 1
    $values = \implode(',', $data);
1674
1675 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...
1676
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1677
    }
1678
1679 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
1680
1681 1
    return $this->query($sql);
1682
  }
1683
1684
  /**
1685
   * Rollback in a transaction and end the transaction.
1686
   *
1687
   * @return bool <p>Boolean true on success, false otherwise.</p>
1688
   */
1689 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...
1690
  {
1691 4
    if ($this->_in_transaction === false) {
1692
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1693
1694
      return false;
1695
    }
1696
1697 4
    $return = \mysqli_rollback($this->link);
1698 4
    \mysqli_autocommit($this->link, true);
1699 4
    $this->_in_transaction = false;
1700
1701 4
    return $return;
1702
  }
1703
1704
  /**
1705
   * Try to secure a variable, so can you use it in sql-queries.
1706
   *
1707
   * <p>
1708
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1709
   * 1. parse into "int"
1710
   * </p><br />
1711
   *
1712
   * <p>
1713
   * <strong>float:</strong><br />
1714
   * 1. return "float"
1715
   * </p><br />
1716
   *
1717
   * <p>
1718
   * <strong>string:</strong><br />
1719
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1720
   * 2. \trim whitespace<br />
1721
   * 3. \trim '<br />
1722
   * 4. escape the string (and remove non utf-8 chars)<br />
1723
   * 5. \trim ' again (because we maybe removed some chars)<br />
1724
   * 6. add ' around the new string<br />
1725
   * </p><br />
1726
   *
1727
   * <p>
1728
   * <strong>array:</strong><br />
1729
   * 1. return null
1730
   * </p><br />
1731
   *
1732
   * <p>
1733
   * <strong>object:</strong><br />
1734
   * 1. return false
1735
   * </p><br />
1736
   *
1737
   * <p>
1738
   * <strong>null:</strong><br />
1739
   * 1. return null
1740
   * </p>
1741
   *
1742
   * @param mixed $var
1743
   *
1744
   * @return mixed
1745
   */
1746 44
  public function secure($var)
1747
  {
1748 44
    if ($var === '') {
1749 2
      return "''";
1750
    }
1751
1752 44
    if ($var === "''") {
1753 1
      return "''";
1754
    }
1755
1756 44
    if ($var === null) {
1757 2
      if ($this->_convert_null_to_empty_string === true) {
1758 1
        return "''";
1759
      }
1760
1761 2
      return 'NULL';
1762
    }
1763
1764 43
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1765 1
      return $var;
1766
    }
1767
1768 43
    if (\is_string($var)) {
1769 36
      $var = \trim(\trim($var), "'");
1770
    }
1771
1772 43
    $var = $this->escape($var, false, false, null);
1773
1774 43
    if (\is_string($var)) {
1775 36
      $var = "'" . \trim($var, "'") . "'";
1776
    }
1777
1778 43
    return $var;
1779
  }
1780
1781
  /**
1782
   * Execute a "select"-query.
1783
   *
1784
   * @param string       $table
1785
   * @param string|array $where
1786
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1787
   *
1788
   * @return false|Result <p>false on error</p>
1789
   *
1790
   * @throws QueryException
1791
   */
1792 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...
1793
  {
1794
    // init
1795 24
    $table = \trim($table);
1796
1797 24
    if ($table === '') {
1798 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1799
1800 1
      return false;
1801
    }
1802
1803 24
    if (\is_string($where)) {
1804 8
      $WHERE = $this->escape($where, false);
1805 17
    } elseif (\is_array($where)) {
1806 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1807
    } else {
1808 1
      $WHERE = '';
1809
    }
1810
1811 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...
1812
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1813
    }
1814
1815 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
1816
1817 24
    return $this->query($sql);
1818
  }
1819
1820
  /**
1821
   * Selects a different database than the one specified on construction.
1822
   *
1823
   * @param string $database <p>Database name to switch to.</p>
1824
   *
1825
   * @return bool <p>Boolean true on success, false otherwise.</p>
1826
   */
1827
  public function select_db(string $database): bool
1828
  {
1829
    if (!$this->isReady()) {
1830
      return false;
1831
    }
1832
1833
    return mysqli_select_db($this->link, $database);
1834
  }
1835
1836
  /**
1837
   * Set the current charset.
1838
   *
1839
   * @param string $charset
1840
   *
1841
   * @return bool
1842
   */
1843 8
  public function set_charset(string $charset): bool
1844
  {
1845 8
    $charsetLower = strtolower($charset);
1846 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1847 6
      $charset = 'utf8';
1848
    }
1849 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1850 6
      $charset = 'utf8mb4';
1851
    }
1852
1853 8
    $this->charset = $charset;
1854
1855 8
    $return = mysqli_set_charset($this->link, $charset);
1856
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1857 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1858
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1859 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...
1860
1861 8
    return $return;
1862
  }
1863
1864
  /**
1865
   * Set the option to convert null to "''" (empty string).
1866
   *
1867
   * Used in secure() => select(), insert(), update(), delete()
1868
   *
1869
   * @deprecated It's not recommended to convert NULL into an empty string!
1870
   *
1871
   * @param $bool
1872
   *
1873
   * @return $this
1874
   */
1875 1
  public function set_convert_null_to_empty_string(bool $bool)
1876
  {
1877 1
    $this->_convert_null_to_empty_string = $bool;
1878
1879 1
    return $this;
1880
  }
1881
1882
  /**
1883
   * Enables or disables internal report functions
1884
   *
1885
   * @link http://php.net/manual/en/function.mysqli-report.php
1886
   *
1887
   * @param int $flags <p>
1888
   *                   <table>
1889
   *                   Supported flags
1890
   *                   <tr valign="top">
1891
   *                   <td>Name</td>
1892
   *                   <td>Description</td>
1893
   *                   </tr>
1894
   *                   <tr valign="top">
1895
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1896
   *                   <td>Turns reporting off</td>
1897
   *                   </tr>
1898
   *                   <tr valign="top">
1899
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1900
   *                   <td>Report errors from mysqli function calls</td>
1901
   *                   </tr>
1902
   *                   <tr valign="top">
1903
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1904
   *                   <td>
1905
   *                   Throw <b>mysqli_sql_exception</b> for errors
1906
   *                   instead of warnings
1907
   *                   </td>
1908
   *                   </tr>
1909
   *                   <tr valign="top">
1910
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1911
   *                   <td>Report if no index or bad index was used in a query</td>
1912
   *                   </tr>
1913
   *                   <tr valign="top">
1914
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1915
   *                   <td>Set all options (report all)</td>
1916
   *                   </tr>
1917
   *                   </table>
1918
   *                   </p>
1919
   *
1920
   * @return bool
1921
   */
1922
  public function set_mysqli_report(int $flags): bool
1923
  {
1924
    return \mysqli_report($flags);
1925
  }
1926
1927
  /**
1928
   * Show config errors by throw exceptions.
1929
   *
1930
   * @return bool
1931
   *
1932
   * @throws \InvalidArgumentException
1933
   */
1934 11
  public function showConfigError(): bool
1935
  {
1936
1937
    if (
1938 11
        !$this->hostname
1939
        ||
1940 10
        !$this->username
1941
        ||
1942 11
        !$this->database
1943
    ) {
1944
1945 3
      if (!$this->hostname) {
1946 1
        throw new \InvalidArgumentException('no-sql-hostname');
1947
      }
1948
1949 2
      if (!$this->username) {
1950 1
        throw new \InvalidArgumentException('no-sql-username');
1951
      }
1952
1953 1
      if (!$this->database) {
1954 1
        throw new \InvalidArgumentException('no-sql-database');
1955
      }
1956
1957
      return false;
1958
    }
1959
1960 8
    return true;
1961
  }
1962
1963
  /**
1964
   * alias: "beginTransaction()"
1965
   */
1966 1
  public function startTransaction(): bool
1967
  {
1968 1
    return $this->beginTransaction();
1969
  }
1970
1971
  /**
1972
   * Execute a callback inside a transaction.
1973
   *
1974
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1975
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1976
   *
1977
   * @return bool <p>Boolean true on success, false otherwise.</p>
1978
   */
1979 1
  public function transact($callback): bool
1980
  {
1981
    try {
1982
1983 1
      $beginTransaction = $this->beginTransaction();
1984 1
      if ($beginTransaction === false) {
1985 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1986
1987 1
        return false;
1988
      }
1989
1990 1
      $result = $callback($this);
1991 1
      if ($result === false) {
1992
        /** @noinspection ThrowRawExceptionInspection */
1993 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1994
      }
1995
1996 1
      return $this->commit();
1997
1998 1
    } catch (\Exception $e) {
1999
2000 1
      $this->rollback();
2001
2002 1
      return false;
2003
    }
2004
  }
2005
2006
  /**
2007
   * Execute a "update"-query.
2008
   *
2009
   * @param string       $table
2010
   * @param array        $data
2011
   * @param array|string $where
2012
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2013
   *
2014
   * @return false|int <p>false on error</p>
2015
   *
2016
   * @throws QueryException
2017
   */
2018 7
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2019
  {
2020
    // init
2021 7
    $table = \trim($table);
2022
2023 7
    if ($table === '') {
2024 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2025
2026 1
      return false;
2027
    }
2028
2029 7
    if (\count($data) === 0) {
2030 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2031
2032 2
      return false;
2033
    }
2034
2035 7
    $SET = $this->_parseArrayPair($data);
2036
2037 7
    if (\is_string($where)) {
2038 2
      $WHERE = $this->escape($where, false);
2039 6
    } elseif (\is_array($where)) {
2040 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2041
    } else {
2042 1
      $WHERE = '';
2043
    }
2044
2045 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...
2046
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2047
    }
2048
2049 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2050
2051 7
    return $this->query($sql);
2052
  }
2053
2054
  /**
2055
   * Determine if database table exists
2056
   *
2057
   * @param string $table
2058
   *
2059
   * @return bool
2060
   */
2061 1
  public function table_exists(string $table): bool
2062
  {
2063 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2064
2065 1
    return $check !== false
2066
           &&
2067 1
           $check instanceof Result
2068
           &&
2069 1
           $check->num_rows > 0;
2070
  }
2071
2072
  /**
2073
   * Count number of rows found matching a specific query.
2074
   *
2075
   * @param string $query
2076
   *
2077
   * @return int
2078
   */
2079 1
  public function num_rows(string $query): int
2080
  {
2081 1
    $check = $this->query($query);
2082
2083
    if (
2084 1
        $check === false
2085
        ||
2086 1
        !$check instanceof Result
2087
    ) {
2088
      return 0;
2089
    }
2090
2091 1
    return $check->num_rows;
2092
  }
2093
}
2094