Completed
Push — master ( e40050...d6bda9 )
by Lars
13:59 queued 01:47
created

DB::transact()   B

Complexity

Conditions 4
Paths 7

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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