Completed
Push — master ( 5fbc20...f1a9e0 )
by Lars
17:19 queued 13:44
created

DB::get_charset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace voku\db;
4
5
use voku\cache\Cache;
6
use voku\db\exceptions\DBConnectException;
7
use voku\db\exceptions\DBGoneAwayException;
8
use voku\db\exceptions\QueryException;
9
use voku\helper\UTF8;
10
11
/**
12
 * DB: This class can handle DB queries via MySQLi.
13
 *
14
 * @package voku\db
15
 */
16
final class DB
17
{
18
19
  /**
20
   * @var int
21
   */
22
  public $query_count = 0;
23
24
  /**
25
   * @var \mysqli
26
   */
27
  private $link;
28
29
  /**
30
   * @var bool
31
   */
32
  private $connected = false;
33
34
  /**
35
   * @var array
36
   */
37
  private $mysqlDefaultTimeFunctions;
38
39
  /**
40
   * @var string
41
   */
42
  private $hostname = '';
43
44
  /**
45
   * @var string
46
   */
47
  private $username = '';
48
49
  /**
50
   * @var string
51
   */
52
  private $password = '';
53
54
  /**
55
   * @var string
56
   */
57
  private $database = '';
58
59
  /**
60
   * @var int
61
   */
62
  private $port = 3306;
63
64
  /**
65
   * @var string
66
   */
67
  private $charset = 'utf8';
68
69
  /**
70
   * @var string
71
   */
72
  private $socket = '';
73
74
  /**
75
   * @var bool
76
   */
77
  private $session_to_db = false;
78
79
  /**
80
   * @var bool
81
   */
82
  private $_in_transaction = false;
83
84
  /**
85
   * @var bool
86
   */
87
  private $_convert_null_to_empty_string = false;
88
89
  /**
90
   * @var bool
91
   */
92
  private $_ssl = false;
93
94
  /**
95
   * The path name to the key file
96
   *
97
   * @var string
98
   */
99
  private $_clientkey;
100
101
  /**
102
   * The path name to the certificate file
103
   *
104
   * @var string
105
   */
106
  private $_clientcert;
107
108
  /**
109
   * The path name to the certificate authority file
110
   *
111
   * @var string
112
   */
113
  private $_cacert;
114
115
  /**
116
   * @var Debug
117
   */
118
  private $_debug;
119
120
  /**
121
   * __construct()
122
   *
123
   * @param string      $hostname
124
   * @param string      $username
125
   * @param string      $password
126
   * @param string      $database
127
   * @param int         $port
128
   * @param string      $charset
129
   * @param bool|string $exit_on_error    <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
130
   *                                      Use a empty string "" or false to disable it.</p>
131
   * @param bool|string $echo_on_error    <p>Echo the error if "checkForDev()" returns true.
132
   *                                      Use a empty string "" or false to disable it.</p>
133
   * @param string      $logger_class_name
134
   * @param string      $logger_level     <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
135
   * @param array       $extra_config     <p>
136
   *                                      'session_to_db' => false|true<br>
137
   *                                      'socket' => 'string (path)'<br>
138
   *                                      'ssl' => 'bool'<br>
139
   *                                      'clientkey' => 'string (path)'<br>
140
   *                                      'clientcert' => 'string (path)'<br>
141
   *                                      'cacert' => 'string (path)'<br>
142
   *                                      </p>
143
   */
144 11
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config = array())
145
  {
146 11
    $this->connected = false;
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
        $extra_config
162 11
    );
163
164 8
    $this->connect();
165
166 5
    $this->mysqlDefaultTimeFunctions = array(
167
      // Returns the current date.
168 5
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170 5
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172 5
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174 5
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176 5
      'CURTIME()',
177
      // Synonym for NOW()
178 5
      'LOCALTIME()',
179
      // Synonym for NOW()
180 5
      'LOCALTIMESTAMP()',
181
      // Returns the current date and time.
182 5
      'NOW()',
183
      // Returns the time at which the function executes.
184 5
      'SYSDATE()',
185
      // Returns a UNIX timestamp.
186 5
      'UNIX_TIMESTAMP()',
187
      // Returns the current UTC date.
188 5
      'UTC_DATE()',
189
      // Returns the current UTC time.
190 5
      'UTC_TIME()',
191
      // Returns the current UTC date and time.
192 5
      '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($sql = null, array $bindings = array())
231
  {
232 2
    return isset($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|string  $port             <p>default is (int)3306</p>
253
   * @param string      $charset          <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
254
   * @param bool|string $exit_on_error    <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
255
   *                                      Use a empty string "" or false to disable it.</p>
256
   * @param bool|string $echo_on_error    <p>Echo the error if "checkForDev()" returns true.
257
   *                                      Use a empty string "" or 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($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
272
  {
273 11
    $this->hostname = (string)$hostname;
274 11
    $this->username = (string)$username;
275 11
    $this->password = (string)$password;
276 11
    $this->database = (string)$database;
277
278 11
    if ($charset) {
279 5
      $this->charset = (string)$charset;
280 5
    }
281
282 11
    if ($port) {
283 5
      $this->port = (int)$port;
284 5
    } else {
285
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
286
      /** @noinspection UsageOfSilenceOperatorInspection */
287 7
      $this->port = (int)@ini_get('mysqli.default_port');
288
    }
289
290
    // fallback
291 11
    if (!$this->port) {
292
      $this->port = 3306;
293
    }
294
295 11
    if (!$this->socket) {
296
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
297 11
      $this->socket = @ini_get('mysqli.default_socket');
298 11
    }
299
300 11
    if ($exit_on_error === true || $exit_on_error === false) {
301 11
      $this->_debug->setExitOnError($exit_on_error);
302 11
    }
303
304 11
    if ($echo_on_error === true || $echo_on_error === false) {
305 11
      $this->_debug->setEchoOnError($echo_on_error);
306 11
    }
307
308 11
    $this->_debug->setLoggerClassName($logger_class_name);
309 11
    $this->_debug->setLoggerLevel($logger_level);
310
311 11
    if (is_array($extra_config) === true) {
312
313 11
      if (isset($extra_config['session_to_db'])) {
314
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
315
      }
316
317 11
      if (isset($extra_config['socket'])) {
318
        $this->socket = $extra_config['socket'];
319
      }
320
321 11
      if (isset($extra_config['ssl'])) {
322
        $this->_ssl = $extra_config['ssl'];
323
      }
324
325 11
      if (isset($extra_config['clientkey'])) {
326
        $this->_clientkey = $extra_config['clientkey'];
327
      }
328
329 11
      if (isset($extra_config['clientcert'])) {
330
        $this->_clientcert = $extra_config['clientcert'];
331
      }
332
333 11
      if (isset($extra_config['cacert'])) {
334
        $this->_cacert = $extra_config['cacert'];
335
      }
336
337 11
    } else {
338
      // only for backward compatibility
339
      $this->session_to_db = (boolean)$extra_config;
340
    }
341
342 11
    return $this->showConfigError();
343
  }
344
345
  /**
346
   * Parses arrays with value pairs and generates SQL to use in queries.
347
   *
348
   * @param array  $arrayPair
349
   * @param string $glue <p>This is the separator.</p>
350
   *
351
   * @return string
352
   *
353
   * @internal
354
   */
355 30
  public function _parseArrayPair(array $arrayPair, $glue = ',')
356
  {
357
    // init
358 30
    $sql = '';
359
360 30
    if (count($arrayPair) === 0) {
361
      return '';
362
    }
363
364 30
    $arrayPairCounter = 0;
365 30
    foreach ($arrayPair as $_key => $_value) {
366 30
      $_connector = '=';
367 30
      $_glueHelper = '';
368 30
      $_key_upper = strtoupper($_key);
369
370 30
      if (strpos($_key_upper, ' NOT') !== false) {
371 2
        $_connector = 'NOT';
372 2
      }
373
374 30
      if (strpos($_key_upper, ' IS') !== false) {
375 1
        $_connector = 'IS';
376 1
      }
377
378 30
      if (strpos($_key_upper, ' IS NOT') !== false) {
379 1
        $_connector = 'IS NOT';
380 1
      }
381
382 30
      if (strpos($_key_upper, ' IN') !== false) {
383 1
        $_connector = 'IN';
384 1
      }
385
386 30
      if (strpos($_key_upper, ' NOT IN') !== false) {
387 1
        $_connector = 'NOT IN';
388 1
      }
389
390 30
      if (strpos($_key_upper, ' BETWEEN') !== false) {
391 1
        $_connector = 'BETWEEN';
392 1
      }
393
394 30
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
395 1
        $_connector = 'NOT BETWEEN';
396 1
      }
397
398 30
      if (strpos($_key_upper, ' LIKE') !== false) {
399 2
        $_connector = 'LIKE';
400 2
      }
401
402 30
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
403 2
        $_connector = 'NOT LIKE';
404 2
      }
405
406 30 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
407 4
        $_connector = '>';
408 4
      }
409
410 30 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
411 1
        $_connector = '<';
412 1
      }
413
414 30
      if (strpos($_key_upper, ' >=') !== false) {
415 4
        $_connector = '>=';
416 4
      }
417
418 30
      if (strpos($_key_upper, ' <=') !== false) {
419 1
        $_connector = '<=';
420 1
      }
421
422 30
      if (strpos($_key_upper, ' <>') !== false) {
423 1
        $_connector = '<>';
424 1
      }
425
426 30
      if (strpos($_key_upper, ' OR') !== false) {
427 2
        $_glueHelper = 'OR';
428 2
      }
429
430 30
      if (strpos($_key_upper, ' AND') !== false) {
431 1
        $_glueHelper = 'AND';
432 1
      }
433
434 30
      if (is_array($_value) === true) {
435 2
        foreach ($_value as $oldKey => $oldValue) {
436 2
          $_value[$oldKey] = $this->secure($oldValue);
437 2
        }
438
439 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
440 1
          $_value = '(' . implode(',', $_value) . ')';
441 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
442 1
          $_value = '(' . implode(' AND ', $_value) . ')';
443 1
        }
444
445 2
      } else {
446 30
        $_value = $this->secure($_value);
447
      }
448
449 30
      $quoteString = $this->quote_string(
450 30
          trim(
451 30
              str_ireplace(
452
                  array(
453 30
                      $_connector,
454 30
                      $_glueHelper,
455 30
                  ),
456 30
                  '',
457
                  $_key
458 30
              )
459 30
          )
460 30
      );
461
462 30
      if (!is_array($_value)) {
463 30
        $_value = array($_value);
464 30
      }
465
466 30
      if (!$_glueHelper) {
467 30
        $_glueHelper = $glue;
468 30
      }
469
470 30
      $tmpCounter = 0;
471 30
      foreach ($_value as $valueInner) {
472
473 30
        $_glueHelperInner = $_glueHelper;
474
475 30
        if ($arrayPairCounter === 0) {
476
477 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
478 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
479 30
          } elseif ($tmpCounter === 0) {
480 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
481 30
          }
482
483 30
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
484 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
485 1
        }
486
487 30
        if (is_string($valueInner) && $valueInner === '') {
488 1
          $valueInner = "''";
489 1
        }
490
491 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
492 30
        $tmpCounter++;
493 30
      }
494
495 30
      if ($_glueHelper === 'OR') {
496 2
        $sql .= ' ) ';
497 2
      }
498
499 30
      $arrayPairCounter++;
500 30
    }
501
502 30
    return $sql;
503
  }
504
505
  /**
506
   * _parseQueryParams
507
   *
508
   * @param string $sql
509
   * @param array  $params
510
   *
511
   * @return array <p>with the keys -> 'sql', 'params'</p>
512
   */
513 13
  private function _parseQueryParams($sql, array $params = array())
514
  {
515
    // is there anything to parse?
516 View Code Duplication
    if (
517 13
        strpos($sql, '?') === false
518 13
        ||
519 3
        count($params) === 0
520 13
    ) {
521 11
      return array('sql' => $sql, 'params' => $params);
522
    }
523
524 3
    $parseKey = md5(uniqid((string)mt_rand(), true));
525 3
    $sql = str_replace('?', $parseKey, $sql);
526
527 3
    $k = 0;
528 3
    while (strpos($sql, $parseKey) !== false) {
529 3
      $sql = UTF8::str_replace_first(
530 3
          $parseKey,
531 3
          isset($params[$k]) ? $this->secure($params[$k]) : '',
532
          $sql
0 ignored issues
show
Bug introduced by
It seems like $sql defined by \voku\helper\UTF8::str_r...params[$k]) : '', $sql) on line 529 can also be of type array<integer,string>; however, voku\helper\UTF8::str_replace_first() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
533 3
      );
534
535 3
      if (isset($params[$k])) {
536 3
        unset($params[$k]);
537 3
      }
538
539 3
      $k++;
540 3
    }
541
542 3
    return array('sql' => $sql, 'params' => $params);
543
  }
544
545
  /**
546
   * Gets the number of affected rows in a previous MySQL operation.
547
   *
548
   * @return int
549
   */
550 12
  public function affected_rows()
551
  {
552 12
    return \mysqli_affected_rows($this->link);
553
  }
554
555
  /**
556
   * Begins a transaction, by turning off auto commit.
557
   *
558
   * @return bool <p>This will return true or false indicating success of transaction</p>
559
   */
560 6 View Code Duplication
  public function beginTransaction()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
561
  {
562 6
    if ($this->_in_transaction === true) {
563 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
564
565 2
      return false;
566
    }
567
568 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
569 6
    $this->_in_transaction = true;
570 6
    $return = \mysqli_autocommit($this->link, false);
571 6
    if ($return === false) {
572
      $this->_in_transaction = false;
573
    }
574
575 6
    return $return;
576
  }
577
578
  /**
579
   * Clear the errors in "_debug->_errors".
580
   *
581
   * @return bool
582
   */
583 6
  public function clearErrors()
584
  {
585 6
    return $this->_debug->clearErrors();
586
  }
587
588
  /**
589
   * Closes a previously opened database connection.
590
   */
591 2
  public function close()
592
  {
593 2
    $this->connected = false;
594
595 2
    if ($this->link) {
596 2
      \mysqli_close($this->link);
597 2
    }
598 2
  }
599
600
  /**
601
   * Commits the current transaction and end the transaction.
602
   *
603
   * @return bool <p>Boolean true on success, false otherwise.</p>
604
   */
605 2 View Code Duplication
  public function commit()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
606
  {
607 2
    if ($this->_in_transaction === false) {
608
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
609
610
      return false;
611
    }
612
613 2
    $return = mysqli_commit($this->link);
614 2
    \mysqli_autocommit($this->link, true);
615 2
    $this->_in_transaction = false;
616
617 2
    return $return;
618
  }
619
620
  /**
621
   * Open a new connection to the MySQL server.
622
   *
623
   * @return bool
624
   *
625
   * @throws DBConnectException
626
   */
627 10
  public function connect()
628
  {
629 10
    if ($this->isReady()) {
630 1
      return true;
631
    }
632
633 10
    $flags = null;
634
635 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
636
    try {
637 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> 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...
638
639 10
      if (Helper::isMysqlndIsUsed() === true) {
640 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
641 10
      }
642
643 10
      if ($this->_ssl === true) {
644
645
        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...
646
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
647
        }
648
649
        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...
650
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
651
        }
652
653
        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...
654
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
655
        }
656
657
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
658
659
        /** @noinspection PhpParamsInspection */
660
        \mysqli_ssl_set(
661
            $this->link,
662
            $this->_clientkey,
663
            $this->_clientcert,
664
            $this->_cacert,
665
            null,
666
            null
667
        );
668
669
        $flags = MYSQLI_CLIENT_SSL;
670
      }
671
672
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
673 10
      $this->connected = @\mysqli_real_connect(
674 10
          $this->link,
675 10
          $this->hostname,
676 10
          $this->username,
677 10
          $this->password,
678 10
          $this->database,
679 10
          $this->port,
680 10
          $this->socket,
681
          $flags
682 10
      );
683
684 10
    } catch (\Exception $e) {
685 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
686 3
      $this->_debug->displayError($error, true);
687
      throw new DBConnectException($error, 100, $e);
688
    }
689 7
    \mysqli_report(MYSQLI_REPORT_OFF);
690
691 7
    $errno = mysqli_connect_errno();
692 7
    if (!$this->connected || $errno) {
693
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
694
      $this->_debug->displayError($error, true);
695
      throw new DBConnectException($error, 101);
696
    }
697
698 7
    $this->set_charset($this->charset);
699
700 7
    return $this->isReady();
701
  }
702
703
  /**
704
   * Execute a "delete"-query.
705
   *
706
   * @param string       $table
707
   * @param string|array $where
708
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
709
   *
710
   * @return false|int <p>false on error</p>
711
   *
712
   *    * @throws QueryException
713
   */
714 2 View Code Duplication
  public function delete($table, $where, $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
715
  {
716
    // init
717 2
    $table = trim($table);
718
719 2
    if ($table === '') {
720 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
721
722 1
      return false;
723
    }
724
725 2
    if (is_string($where)) {
726 1
      $WHERE = $this->escape($where, false);
727 2
    } elseif (is_array($where)) {
728 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
729 2
    } else {
730 1
      $WHERE = '';
731
    }
732
733 2
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
734
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
735
    }
736
737 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
738
739 2
    return $this->query($sql);
740
  }
741
742
  /**
743
   * Ends a transaction and commits if no errors, then ends autocommit.
744
   *
745
   * @return bool <p>This will return true or false indicating success of transactions.</p>
746
   */
747 4 View Code Duplication
  public function endTransaction()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
748
  {
749 4
    if ($this->_in_transaction === false) {
750
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
751
752
      return false;
753
    }
754
755 4
    if (!$this->errors()) {
756 1
      $return = \mysqli_commit($this->link);
757 1
    } else {
758 3
      $this->rollback();
759 3
      $return = false;
760
    }
761
762 4
    \mysqli_autocommit($this->link, true);
763 4
    $this->_in_transaction = false;
764
765 4
    return $return;
766
  }
767
768
  /**
769
   * Get all errors from "$this->_errors".
770
   *
771
   * @return array|false <p>false === on errors</p>
772
   */
773 4
  public function errors()
774
  {
775 4
    $errors = $this->_debug->getErrors();
776
777 4
    return count($errors) > 0 ? $errors : false;
778
  }
779
780
  /**
781
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
782
   *
783
   * @param mixed $sql    <p>The SQL string.</p>
784
   * @param array $params <p>An array of key-value bindings.</p>
785
   *
786
   * @return array <p>with the keys -> 'sql', 'params'</p>
787
   */
788 14
  public function _parseQueryParamsByName($sql, array $params = array())
789
  {
790
    // is there anything to parse?
791 View Code Duplication
    if (
792 14
        strpos($sql, ':') === false
793 14
        ||
794 12
        count($params) === 0
795 14
    ) {
796 3
      return array('sql' => $sql, 'params' => $params);
797
    }
798
799 12
    $parseKey = md5(uniqid((string)mt_rand(), true));
800
801 12
    foreach ($params as $name => $value) {
802 12
      $nameTmp = $name;
803 12
      if (strpos($name, ':') === 0) {
804 10
        $nameTmp = substr($name, 1);
805 10
      }
806
807 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
808 12
      $sql = str_replace(':' . $nameTmp, $parseKeyInner, $sql);
809 12
    }
810
811 12
    foreach ($params as $name => $value) {
812 12
      $nameTmp = $name;
813 12
      if (strpos($name, ':') === 0) {
814 10
        $nameTmp = substr($name, 1);
815 10
      }
816
817 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
818 12
      $sqlBefore = $sql;
819 12
      $secureParamValue = $this->secure($params[$name]);
820
821 12
      while (strpos($sql, $parseKeyInner) !== false) {
822 12
        $sql = UTF8::str_replace_first(
823 12
            $parseKeyInner,
824 12
            $secureParamValue,
825
            $sql
826 12
        );
827 12
      }
828
829 12
      if ($sqlBefore !== $sql) {
830 12
        unset($params[$name]);
831 12
      }
832 12
    }
833
834 12
    return array('sql' => $sql, 'params' => $params);
835
  }
836
837
  /**
838
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
839
   *
840
   * @param mixed     $var           boolean: convert into "integer"<br />
841
   *                                 int: int (don't change it)<br />
842
   *                                 float: float (don't change it)<br />
843
   *                                 null: null (don't change it)<br />
844
   *                                 array: run escape() for every key => value<br />
845
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
846
   * @param bool      $stripe_non_utf8
847
   * @param bool      $html_entity_decode
848
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
849
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
850
   *                                 <strong>null</strong> => Convert the array into null, every time.
851
   *
852
   * @return mixed
853
   */
854 55
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
855
  {
856 55
    if ($var === '') {
857 2
      return '';
858
    }
859
860 55
    if ($var === "''") {
861
      return "''";
862
    }
863
864 55
    if ($var === null) {
865
      if (
866 2
          $this->_convert_null_to_empty_string === true
867 2
      ) {
868
        return "''";
869
      }
870
871 2
      return 'NULL';
872
    }
873
874
    // save the current value as int (for later usage)
875 55
    if (!is_object($var)) {
876 55
      $varInt = (int)$var;
877 55
    }
878
879
    /** @noinspection TypeUnsafeComparisonInspection */
880
    if (
881 55
        is_int($var)
882
        ||
883 52
        is_bool($var)
884 52
        ||
885
        (
886 52
            isset($varInt, $var[0])
887 52
            &&
888 52
            $var[0] != '0'
889 52
            &&
890
            "$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...
891 52
        )
892 55
    ) {
893
894
      // "int" || int || bool
895
896 32
      return (int)$var;
897
    }
898
899 52
    if (is_float($var)) {
900
901
      // float
902
903 5
      return $var;
904
    }
905
906 52
    if (is_array($var)) {
907
908
      // array
909
910 4
      if ($convert_array === null) {
911 3
        return null;
912
      }
913
914 2
      $varCleaned = array();
915 2
      foreach ((array)$var as $key => $value) {
916
917 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
918 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
919
920
        /** @noinspection OffsetOperationsInspection */
921 2
        $varCleaned[$key] = $value;
922 2
      }
923
924 2
      if ($convert_array === true) {
925 1
        $varCleaned = implode(',', $varCleaned);
926
927 1
        return $varCleaned;
928
      }
929
930 2
      return (array)$varCleaned;
931
    }
932
933
    if (
934 52
        is_string($var)
935
        ||
936
        (
937 3
            is_object($var)
938 3
            &&
939 3
            method_exists($var, '__toString')
940 3
        )
941 52
    ) {
942
943
      // "string"
944
945 52
      $var = (string)$var;
946
947 52
      if ($stripe_non_utf8 === true) {
948 11
        $var = UTF8::cleanup($var);
949 11
      }
950
951 52
      if ($html_entity_decode === true) {
952
        // use no-html-entity for db
953 1
        $var = UTF8::html_entity_decode($var);
954 1
      }
955
956 52
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
957
958 52
      $var = \mysqli_real_escape_string($this->getLink(), $var);
959
960 52
      return (string)$var;
961
962
    }
963
964 3
    if ($var instanceof \DateTime) {
965
966
      // "DateTime"-object
967
968
      try {
969 3
        return $this->escape($var->format('Y-m-d H:i:s'), false);
970
      } catch (\Exception $e) {
971
        return null;
972
      }
973
974
    } else {
975 2
      return false;
976
    }
977
  }
978
979
  /**
980
   * Execute select/insert/update/delete sql-queries.
981
   *
982
   * @param string $query    <p>sql-query</p>
983
   * @param bool   $useCache <p>use cache?</p>
984
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
985
   * @param DB     $db       optional <p>the database connection</p>
986
   *
987
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
988
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
989
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
990
   *               "true" by e.g. "DROP"-queries<br />
991
   *               "false" on error
992
   *
993
   * @throws QueryException
994
   */
995 3
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
996
  {
997
    // init
998 3
    $cacheKey = null;
999 3
    if (!$db) {
1000 3
      $db = self::getInstance();
1001 3
    }
1002
1003 3 View Code Duplication
    if ($useCache === true) {
1004 1
      $cache = new Cache(null, null, false, $useCache);
1005 1
      $cacheKey = 'sql-' . md5($query);
1006
1007
      if (
1008 1
          $cache->getCacheIsReady() === true
1009 1
          &&
1010 1
          $cache->existsItem($cacheKey)
1011 1
      ) {
1012 1
        return $cache->getItem($cacheKey);
1013
      }
1014
1015 1
    } else {
1016 3
      $cache = false;
1017
    }
1018
1019 3
    $result = $db->query($query);
1020
1021 3
    if ($result instanceof Result) {
1022
1023 1
      $return = $result->fetchAllArray();
1024
1025
      // save into the cache
1026 View Code Duplication
      if (
1027
          $cacheKey !== null
1028 1
          &&
1029
          $useCache === true
1030 1
          &&
1031
          $cache instanceof Cache
1032 1
          &&
1033 1
          $cache->getCacheIsReady() === true
1034 1
      ) {
1035 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1036 1
      }
1037
1038 1
    } else {
1039 2
      $return = $result;
1040
    }
1041
1042 3
    return $return;
1043
  }
1044
1045
  /**
1046
   * Get all table-names via "SHOW TABLES".
1047
   *
1048
   * @return array
1049
   */
1050 1
  public function getAllTables()
1051
  {
1052 1
    $query = 'SHOW TABLES';
1053 1
    $result = $this->query($query);
1054
1055 1
    return $result->fetchAllArray();
1056
  }
1057
1058
  /**
1059
   * @return Debug
1060
   */
1061 9
  public function getDebugger()
1062
  {
1063 9
    return $this->_debug;
1064
  }
1065
1066
  /**
1067
   * Get errors from "$this->_errors".
1068
   *
1069
   * @return array
1070
   */
1071 1
  public function getErrors()
1072
  {
1073 1
    return $this->_debug->getErrors();
1074
  }
1075
1076
  /**
1077
   * getInstance()
1078
   *
1079
   * @param string      $hostname
1080
   * @param string      $username
1081
   * @param string      $password
1082
   * @param string      $database
1083
   * @param int|string  $port            <p>default is (int)3306</p>
1084
   * @param string      $charset         <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1085
   * @param bool|string $exit_on_error   <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1086
   *                                     Use a empty string "" or false to disable it.</p>
1087
   * @param bool|string $echo_on_error   <p>Echo the error if "checkForDev()" returns true.
1088
   *                                     Use a empty string "" or false to disable it.</p>
1089
   * @param string      $logger_class_name
1090
   * @param string      $logger_level    <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1091
   * @param array       $extra_config    <p>
1092
   *                                     'session_to_db' => false|true<br>
1093
   *                                     'socket' => 'string (path)'<br>
1094
   *                                     'ssl' => 'bool'<br>
1095
   *                                     'clientkey' => 'string (path)'<br>
1096
   *                                     'clientcert' => 'string (path)'<br>
1097
   *                                     'cacert' => 'string (path)'<br>
1098
   *                                     </p>
1099
   *
1100
   * @return \voku\db\DB
1101
   */
1102 97
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = true, $echo_on_error = true, $logger_class_name = '', $logger_level = '', $extra_config = array())
1103
  {
1104
    /**
1105
     * @var $instance DB[]
1106
     */
1107 97
    static $instance = array();
1108
1109
    /**
1110
     * @var $firstInstance DB
1111
     */
1112 97
    static $firstInstance = null;
1113
1114
    if (
1115 97
        $hostname . $username . $password . $database . $port . $charset == ''
1116 97
        &&
1117 14
        null !== $firstInstance
1118 97
    ) {
1119 14
      return $firstInstance;
1120
    }
1121
1122 97
    $extra_config_string = '';
1123 97
    if (is_array($extra_config) === true) {
1124 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1125
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1126 97
      }
1127 97
    } else {
1128
      // only for backward compatibility
1129
      $extra_config_string = (int)$extra_config;
1130
    }
1131
1132 97
    $connection = md5(
1133 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1134 97
    );
1135
1136 97
    if (!isset($instance[$connection])) {
1137 11
      $instance[$connection] = new self(
1138 11
          $hostname,
1139 11
          $username,
1140 11
          $password,
1141 11
          $database,
1142 11
          $port,
1143 11
          $charset,
1144 11
          $exit_on_error,
1145 11
          $echo_on_error,
1146 11
          $logger_class_name,
1147 11
          $logger_level,
1148
          $extra_config
1149 11
      );
1150
1151 5
      if (null === $firstInstance) {
1152 1
        $firstInstance = $instance[$connection];
1153 1
      }
1154 5
    }
1155
1156 97
    return $instance[$connection];
1157
  }
1158
1159
  /**
1160
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1161
   *
1162
   * @return \mysqli
1163
   */
1164 56
  public function getLink()
1165
  {
1166 56
    return $this->link;
1167
  }
1168
1169
  /**
1170
   * Get the current charset.
1171
   *
1172
   * @return string
1173
   */
1174 1
  public function get_charset()
1175
  {
1176 1
    return $this->charset;
1177
  }
1178
1179
  /**
1180
   * Check if we are in a transaction.
1181
   *
1182
   * @return bool
1183
   */
1184
  public function inTransaction()
1185
  {
1186
    return $this->_in_transaction;
1187
  }
1188
1189
  /**
1190
   * Execute a "insert"-query.
1191
   *
1192
   * @param string      $table
1193
   * @param array       $data
1194
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1195
   *
1196
   * @return false|int <p>false on error</p>
1197
   *
1198
   * @throws QueryException
1199
   */
1200 28
  public function insert($table, array $data = array(), $databaseName = null)
1201
  {
1202
    // init
1203 28
    $table = trim($table);
1204
1205 28
    if ($table === '') {
1206 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1207
1208 2
      return false;
1209
    }
1210
1211 27
    if (count($data) === 0) {
1212 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1213
1214 3
      return false;
1215
    }
1216
1217 25
    $SET = $this->_parseArrayPair($data);
1218
1219 25
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1220
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1221
    }
1222
1223 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1224
1225 25
    return $this->query($sql);
1226
  }
1227
1228
  /**
1229
   * Returns the auto generated id used in the last query.
1230
   *
1231
   * @return int|string
1232
   */
1233 58
  public function insert_id()
1234
  {
1235 58
    return \mysqli_insert_id($this->link);
1236
  }
1237
1238
  /**
1239
   * Check if db-connection is ready.
1240
   *
1241
   * @return boolean
1242
   */
1243 106
  public function isReady()
1244
  {
1245 106
    return $this->connected ? true : false;
1246
  }
1247
1248
  /**
1249
   * Get the last sql-error.
1250
   *
1251
   * @return string|false <p>false === there was no error</p>
1252
   */
1253 1
  public function lastError()
1254
  {
1255 1
    $errors = $this->_debug->getErrors();
1256
1257 1
    return count($errors) > 0 ? end($errors) : false;
1258
  }
1259
1260
  /**
1261
   * Execute a sql-multi-query.
1262
   *
1263
   * @param string $sql
1264
   *
1265
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1266
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1267
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1268
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1269
   *
1270
   * @throws QueryException
1271
   */
1272 1
  public function multi_query($sql)
1273
  {
1274 1
    if (!$this->isReady()) {
1275
      return false;
1276
    }
1277
1278 1 View Code Duplication
    if (!$sql || $sql === '') {
1279 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1280
1281 1
      return false;
1282
    }
1283
1284 1
    $query_start_time = microtime(true);
1285 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1286 1
    $query_duration = microtime(true) - $query_start_time;
1287
1288 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1289
1290 1
    $returnTheResult = false;
1291 1
    $result = array();
1292
1293 1
    if ($resultTmp) {
1294
      do {
1295
1296 1
        $resultTmpInner = \mysqli_store_result($this->link);
1297
1298 1
        if ($resultTmpInner instanceof \mysqli_result) {
1299
1300 1
          $returnTheResult = true;
1301 1
          $result[] = new Result($sql, $resultTmpInner);
1302
1303 1
        } else {
1304
1305
          // is the query successful
1306 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1307 1
            $result[] = true;
1308 1
          } else {
1309
            $result[] = false;
1310
          }
1311
1312
        }
1313
1314 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1315
1316 1
    } else {
1317
1318
      // log the error query
1319 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1320
1321 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1322
    }
1323
1324
    // return the result only if there was a "SELECT"-query
1325 1
    if ($returnTheResult === true) {
1326 1
      return $result;
1327
    }
1328
1329
    if (
1330 1
        count($result) > 0
1331 1
        &&
1332 1
        in_array(false, $result, true) === false
1333 1
    ) {
1334 1
      return true;
1335
    }
1336
1337
    return false;
1338
  }
1339
1340
  /**
1341
   * Pings a server connection, or tries to reconnect
1342
   * if the connection has gone down.
1343
   *
1344
   * @return boolean
1345
   */
1346 3
  public function ping()
1347
  {
1348
    if (
1349 3
        $this->link
1350 3
        &&
1351 3
        $this->link instanceof \mysqli
1352 3
    ) {
1353
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1354
      /** @noinspection UsageOfSilenceOperatorInspection */
1355 3
      return (bool)@\mysqli_ping($this->link);
1356
    }
1357
1358
    return false;
1359
  }
1360
1361
  /**
1362
   * Get a new "Prepare"-Object for your sql-query.
1363
   *
1364
   * @param string $query
1365
   *
1366
   * @return Prepare
1367
   */
1368 2
  public function prepare($query)
1369
  {
1370 2
    return new Prepare($this, $query);
1371
  }
1372
1373
  /**
1374
   * Execute a sql-query and return the result-array for select-statements.
1375
   *
1376
   * @param string $query
1377
   *
1378
   * @return mixed
1379
   * @deprecated
1380
   * @throws \Exception
1381
   */
1382
  public static function qry($query)
1383
  {
1384
    $db = self::getInstance();
1385
1386
    $args = func_get_args();
1387
    /** @noinspection SuspiciousAssignmentsInspection */
1388
    $query = array_shift($args);
1389
    $query = str_replace('?', '%s', $query);
1390
    $args = array_map(
1391
        array(
1392
            $db,
1393
            'escape',
1394
        ),
1395
        $args
1396
    );
1397
    array_unshift($args, $query);
1398
    $query = call_user_func_array('sprintf', $args);
1399
    $result = $db->query($query);
1400
1401
    if ($result instanceof Result) {
1402
      return $result->fetchAllArray();
1403
    }
1404
1405
    return $result;
1406
  }
1407
1408
  /**
1409
   * Execute a sql-query.
1410
   *
1411
   * @param string        $sql            <p>The sql query-string.</p>
1412
   *
1413
   * @param array|boolean $params         <p>
1414
   *                                      "array" of sql-query-parameters<br/>
1415
   *                                      "false" if you don't need any parameter (default)<br/>
1416
   *                                      </p>
1417
   *
1418
   * @return bool|int|Result              <p>
1419
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1420
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1421
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1422
   *                                      "true" by e.g. "DROP"-queries<br />
1423
   *                                      "false" on error
1424
   *                                      </p>
1425
   *
1426
   * @throws QueryException
1427
   */
1428 96
  public function query($sql = '', $params = false)
1429
  {
1430 96
    if (!$this->isReady()) {
1431
      return false;
1432
    }
1433
1434 96 View Code Duplication
    if (!$sql || $sql === '') {
1435 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1436
1437 4
      return false;
1438
    }
1439
1440
    if (
1441
        $params !== false
1442 94
        &&
1443 29
        is_array($params)
1444 94
        &&
1445 29
        count($params) > 0
1446 94
    ) {
1447 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1448 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1449 13
      $sql = $parseQueryParamsByName['sql'];
1450 13
    }
1451
1452
    // DEBUG
1453
    // var_dump($params);
1454
    // echo $sql . "\n";
1455
1456 94
    $query_start_time = microtime(true);
1457 94
    $query_result = \mysqli_real_query($this->link, $sql);
1458 94
    $query_duration = microtime(true) - $query_start_time;
1459
1460 94
    $this->query_count++;
1461
1462 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1463 94
    if ($mysqli_field_count) {
1464 62
      $result = \mysqli_store_result($this->link);
1465 62
    } else {
1466 64
      $result = $query_result;
1467
    }
1468
1469 94
    if ($result instanceof \mysqli_result) {
1470
1471
      // log the select query
1472 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1473
1474
      // return query result object
1475 61
      return new Result($sql, $result);
1476
    }
1477
1478 66
    if ($query_result === true) {
1479
1480
      // "INSERT" || "REPLACE"
1481 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1482 58
        $insert_id = (int)$this->insert_id();
1483 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1484
1485 58
        return $insert_id;
1486
      }
1487
1488
      // "UPDATE" || "DELETE"
1489 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1490 12
        $affected_rows = (int)$this->affected_rows();
1491 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1492
1493 12
        return $affected_rows;
1494
      }
1495
1496
      // log the ? query
1497 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1498
1499 27
      return true;
1500
    }
1501
1502
    // log the error query
1503 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1504
1505 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1506
  }
1507
1508
  /**
1509
   * Error-handling for the sql-query.
1510
   *
1511
   * @param string     $errorMessage
1512
   * @param int        $errorNumber
1513
   * @param string     $sql
1514
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1515
   * @param bool       $sqlMultiQuery
1516
   *
1517
   * @throws QueryException
1518
   * @throws DBGoneAwayException
1519
   *
1520
   * @return bool
1521
   */
1522 13
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1523
  {
1524 13
    $errorNumber = (int)$errorNumber;
1525
1526
    if (
1527
        $errorMessage === 'DB server has gone away'
1528 13
        ||
1529
        $errorMessage === 'MySQL server has gone away'
1530 12
        ||
1531
        $errorNumber === 2006
1532 13
    ) {
1533 1
      static $RECONNECT_COUNTER;
1534
1535
      // exit if we have more then 3 "DB server has gone away"-errors
1536 1
      if ($RECONNECT_COUNTER > 3) {
1537
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1538
        throw new DBGoneAwayException($errorMessage);
1539
      }
1540
1541 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1542
1543
      // reconnect
1544 1
      $RECONNECT_COUNTER++;
1545 1
      $this->reconnect(true);
1546
1547
      // re-run the current (non multi) query
1548 1
      if ($sqlMultiQuery === false) {
1549 1
        return $this->query($sql, $sqlParams);
1550
      }
1551
1552
      return false;
1553
    }
1554
1555 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1556
1557 12
    $force_exception_after_error = null; // auto
1558 12
    if ($this->_in_transaction === true) {
1559 4
      $force_exception_after_error = false;
1560 4
    }
1561
    // this query returned an error, we must display it (only for dev) !!!
1562
1563 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1564
1565 12
    return false;
1566
  }
1567
1568
  /**
1569
   * Quote && Escape e.g. a table name string.
1570
   *
1571
   * @param string $str
1572
   *
1573
   * @return string
1574
   */
1575 36
  public function quote_string($str)
1576
  {
1577 36
    $str = str_replace(
1578 36
        '`',
1579 36
        '``',
1580 36
        trim(
1581 36
            $this->escape($str, false),
1582
            '`'
1583 36
        )
1584 36
    );
1585
1586 36
    return '`' . $str . '`';
1587
  }
1588
1589
  /**
1590
   * Reconnect to the MySQL-Server.
1591
   *
1592
   * @param bool $checkViaPing
1593
   *
1594
   * @return bool
1595
   */
1596 3
  public function reconnect($checkViaPing = false)
1597
  {
1598 3
    $ping = false;
1599
1600 3
    if ($checkViaPing === true) {
1601 2
      $ping = $this->ping();
1602 2
    }
1603
1604 3
    if ($ping !== true) {
1605 3
      $this->connected = false;
1606 3
      $this->connect();
1607 3
    }
1608
1609 3
    return $this->isReady();
1610
  }
1611
1612
  /**
1613
   * Execute a "replace"-query.
1614
   *
1615
   * @param string      $table
1616
   * @param array       $data
1617
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1618
   *
1619
   * @return false|int <p>false on error</p>
1620
   *
1621
   * @throws QueryException
1622
   */
1623 1
  public function replace($table, array $data = array(), $databaseName = null)
1624
  {
1625
    // init
1626 1
    $table = trim($table);
1627
1628 1
    if ($table === '') {
1629 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1630
1631 1
      return false;
1632
    }
1633
1634 1
    if (count($data) === 0) {
1635 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1636
1637 1
      return false;
1638
    }
1639
1640
    // extracting column names
1641 1
    $columns = array_keys($data);
1642 1
    foreach ($columns as $k => $_key) {
1643
      /** @noinspection AlterInForeachInspection */
1644 1
      $columns[$k] = $this->quote_string($_key);
1645 1
    }
1646
1647 1
    $columns = implode(',', $columns);
1648
1649
    // extracting values
1650 1
    foreach ($data as $k => $_value) {
1651
      /** @noinspection AlterInForeachInspection */
1652 1
      $data[$k] = $this->secure($_value);
1653 1
    }
1654 1
    $values = implode(',', $data);
1655
1656 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...
1657
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1658
    }
1659
1660 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1661
1662 1
    return $this->query($sql);
1663
  }
1664
1665
  /**
1666
   * Rollback in a transaction and end the transaction.
1667
   *
1668
   * @return bool <p>Boolean true on success, false otherwise.</p>
1669
   */
1670 4 View Code Duplication
  public function rollback()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1671
  {
1672 4
    if ($this->_in_transaction === false) {
1673
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1674
1675
      return false;
1676
    }
1677
1678 4
    $return = \mysqli_rollback($this->link);
1679 4
    \mysqli_autocommit($this->link, true);
1680 4
    $this->_in_transaction = false;
1681
1682 4
    return $return;
1683
  }
1684
1685
  /**
1686
   * Try to secure a variable, so can you use it in sql-queries.
1687
   *
1688
   * <p>
1689
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1690
   * 1. parse into "int"
1691
   * </p><br />
1692
   *
1693
   * <p>
1694
   * <strong>float:</strong><br />
1695
   * 1. return "float"
1696
   * </p><br />
1697
   *
1698
   * <p>
1699
   * <strong>string:</strong><br />
1700
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1701
   * 2. trim whitespace<br />
1702
   * 3. trim '<br />
1703
   * 4. escape the string (and remove non utf-8 chars)<br />
1704
   * 5. trim ' again (because we maybe removed some chars)<br />
1705
   * 6. add ' around the new string<br />
1706
   * </p><br />
1707
   *
1708
   * <p>
1709
   * <strong>array:</strong><br />
1710
   * 1. return null
1711
   * </p><br />
1712
   *
1713
   * <p>
1714
   * <strong>object:</strong><br />
1715
   * 1. return false
1716
   * </p><br />
1717
   *
1718
   * <p>
1719
   * <strong>null:</strong><br />
1720
   * 1. return null
1721
   * </p>
1722
   *
1723
   * @param mixed $var
1724
   *
1725
   * @return mixed
1726
   */
1727 44
  public function secure($var)
1728
  {
1729 44
    if ($var === '') {
1730 2
      return '';
1731
    }
1732
1733 44
    if ($var === "''") {
1734 1
      return "''";
1735
    }
1736
1737 44
    if ($var === null) {
1738
      if (
1739 2
          $this->_convert_null_to_empty_string === true
1740 2
      ) {
1741 1
        return "''";
1742
      }
1743
1744 2
      return 'NULL';
1745
    }
1746
1747 43
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1748 1
      return $var;
1749
    }
1750
1751 43
    if (is_string($var)) {
1752 36
      $var = trim(trim($var), "'");
1753 36
    }
1754
1755 43
    $var = $this->escape($var, false, false, null);
1756
1757 43
    if (is_string($var)) {
1758 36
      $var = "'" . trim($var, "'") . "'";
1759 36
    }
1760
1761 43
    return $var;
1762
  }
1763
1764
  /**
1765
   * Execute a "select"-query.
1766
   *
1767
   * @param string       $table
1768
   * @param string|array $where
1769
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1770
   *
1771
   * @return false|Result <p>false on error</p>
1772
   *
1773
   * @throws QueryException
1774
   */
1775 24 View Code Duplication
  public function select($table, $where = '1=1', $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1776
  {
1777
    // init
1778 24
    $table = trim($table);
1779
1780 24
    if ($table === '') {
1781 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1782
1783 1
      return false;
1784
    }
1785
1786 24
    if (is_string($where)) {
1787 8
      $WHERE = $this->escape($where, false);
1788 24
    } elseif (is_array($where)) {
1789 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1790 17
    } else {
1791 1
      $WHERE = '';
1792
    }
1793
1794 24
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1795
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1796
    }
1797
1798 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1799
1800 24
    return $this->query($sql);
1801
  }
1802
1803
  /**
1804
   * Selects a different database than the one specified on construction.
1805
   *
1806
   * @param string $database <p>Database name to switch to.</p>
1807
   *
1808
   * @return bool <p>Boolean true on success, false otherwise.</p>
1809
   */
1810
  public function select_db($database)
1811
  {
1812
    if (!$this->isReady()) {
1813
      return false;
1814
    }
1815
1816
    return mysqli_select_db($this->link, $database);
1817
  }
1818
1819
  /**
1820
   * Set the current charset.
1821
   *
1822
   * @param string $charset
1823
   *
1824
   * @return bool
1825
   */
1826 8
  public function set_charset($charset)
1827
  {
1828 8
    $charsetLower = strtolower($charset);
1829 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1830 6
      $charset = 'utf8';
1831 6
    }
1832 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1833 6
      $charset = 'utf8mb4';
1834 6
    }
1835
1836 8
    $this->charset = (string)$charset;
1837
1838 8
    $return = mysqli_set_charset($this->link, $charset);
1839
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1840
    /** @noinspection UsageOfSilenceOperatorInspection */
1841 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1842
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1843
    /** @noinspection UsageOfSilenceOperatorInspection */
1844 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...
1845
1846 8
    return $return;
1847
  }
1848
1849
  /**
1850
   * Set the option to convert null to "''" (empty string).
1851
   *
1852
   * Used in secure() => select(), insert(), update(), delete()
1853
   *
1854
   * @deprecated It's not recommended to convert NULL into an empty string!
1855
   *
1856
   * @param $bool
1857
   *
1858
   * @return $this
1859
   */
1860
  public function set_convert_null_to_empty_string($bool)
1861
  {
1862
    $this->_convert_null_to_empty_string = (bool)$bool;
1863
1864
    return $this;
1865
  }
1866
1867
  /**
1868
   * Enables or disables internal report functions
1869
   *
1870
   * @link http://php.net/manual/en/function.mysqli-report.php
1871
   *
1872
   * @param int $flags <p>
1873
   *                   <table>
1874
   *                   Supported flags
1875
   *                   <tr valign="top">
1876
   *                   <td>Name</td>
1877
   *                   <td>Description</td>
1878
   *                   </tr>
1879
   *                   <tr valign="top">
1880
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1881
   *                   <td>Turns reporting off</td>
1882
   *                   </tr>
1883
   *                   <tr valign="top">
1884
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1885
   *                   <td>Report errors from mysqli function calls</td>
1886
   *                   </tr>
1887
   *                   <tr valign="top">
1888
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1889
   *                   <td>
1890
   *                   Throw <b>mysqli_sql_exception</b> for errors
1891
   *                   instead of warnings
1892
   *                   </td>
1893
   *                   </tr>
1894
   *                   <tr valign="top">
1895
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1896
   *                   <td>Report if no index or bad index was used in a query</td>
1897
   *                   </tr>
1898
   *                   <tr valign="top">
1899
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1900
   *                   <td>Set all options (report all)</td>
1901
   *                   </tr>
1902
   *                   </table>
1903
   *                   </p>
1904
   *
1905
   * @return bool
1906
   */
1907
  public function set_mysqli_report($flags)
1908
  {
1909
    return \mysqli_report($flags);
1910
  }
1911
1912
  /**
1913
   * Show config errors by throw exceptions.
1914
   *
1915
   * @return bool
1916
   *
1917
   * @throws \InvalidArgumentException
1918
   */
1919 11
  public function showConfigError()
1920
  {
1921
1922
    if (
1923 11
        !$this->hostname
1924 11
        ||
1925 10
        !$this->username
1926 10
        ||
1927 9
        !$this->database
1928 11
    ) {
1929
1930 3
      if (!$this->hostname) {
1931 1
        throw new \InvalidArgumentException('no-sql-hostname');
1932
      }
1933
1934 2
      if (!$this->username) {
1935 1
        throw new \InvalidArgumentException('no-sql-username');
1936
      }
1937
1938 1
      if (!$this->database) {
1939 1
        throw new \InvalidArgumentException('no-sql-database');
1940
      }
1941
1942
      return false;
1943
    }
1944
1945 8
    return true;
1946
  }
1947
1948
  /**
1949
   * alias: "beginTransaction()"
1950
   */
1951 1
  public function startTransaction()
1952
  {
1953 1
    $this->beginTransaction();
1954 1
  }
1955
1956
  /**
1957
   * Execute a callback inside a transaction.
1958
   *
1959
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1960
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1961
   *
1962
   * @return bool <p>Boolean true on success, false otherwise.</p>
1963
   */
1964 1
  public function transact($callback)
1965
  {
1966
    try {
1967
1968 1
      $beginTransaction = $this->beginTransaction();
1969 1
      if ($beginTransaction === false) {
1970 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1971
1972 1
        return false;
1973
      }
1974
1975 1
      $result = call_user_func($callback, $this);
1976 1
      if ($result === false) {
1977
        /** @noinspection ThrowRawExceptionInspection */
1978 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1979
      }
1980
1981 1
      return $this->commit();
1982
1983 1
    } catch (\Exception $e) {
1984
1985 1
      $this->rollback();
1986
1987 1
      return false;
1988
    }
1989
  }
1990
1991
  /**
1992
   * Execute a "update"-query.
1993
   *
1994
   * @param string       $table
1995
   * @param array        $data
1996
   * @param array|string $where
1997
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1998
   *
1999
   * @return false|int <p>false on error</p>
2000
   *
2001
   * @throws QueryException
2002
   */
2003 7
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
2004
  {
2005
    // init
2006 7
    $table = trim($table);
2007
2008 7
    if ($table === '') {
2009 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2010
2011 1
      return false;
2012
    }
2013
2014 7
    if (count($data) === 0) {
2015 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2016
2017 2
      return false;
2018
    }
2019
2020 7
    $SET = $this->_parseArrayPair($data);
2021
2022 7
    if (is_string($where)) {
2023 2
      $WHERE = $this->escape($where, false);
2024 7
    } elseif (is_array($where)) {
2025 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2026 5
    } else {
2027 1
      $WHERE = '';
2028
    }
2029
2030 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...
2031
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
2032
    }
2033
2034 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
2035
2036 7
    return $this->query($sql);
2037
  }
2038
2039
  /**
2040
   * Determine if database table exists
2041
   *
2042
   * @param string $table
2043
   *
2044
   * @return bool
2045
   */
2046 1
  public function table_exists($table)
2047
  {
2048 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2049
    if (
2050
        $check !== false
2051 1
        &&
2052
        $check instanceof Result
2053 1
        &&
2054 1
        $check->num_rows > 0
2055 1
    ) {
2056 1
      return true;
2057
    }
2058
2059 1
    return false;
2060
  }
2061
2062
  /**
2063
   * Count number of rows found matching a specific query.
2064
   *
2065
   * @param string
2066
   *
2067
   * @return int
2068
   */
2069 1
  public function num_rows($query)
2070
  {
2071 1
    $check = $this->query($query);
2072
2073
    if (
2074
        $check === false
2075 1
        ||
2076
        !$check instanceof Result
2077 1
    ) {
2078
      return 0;
2079
    }
2080
2081 1
    return $check->num_rows;
2082
  }
2083
}
2084