Completed
Push — master ( 73663d...a6f79b )
by Lars
01:49
created

DB::delete()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 27
Code Lines 15

Duplication

Lines 27
Ratio 100 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 0
Metric Value
dl 27
loc 27
ccs 13
cts 14
cp 0.9286
rs 8.439
c 0
b 0
f 0
cc 5
eloc 15
nc 7
nop 3
crap 5.009
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use voku\cache\Cache;
8
use voku\db\exceptions\DBConnectException;
9
use voku\db\exceptions\DBGoneAwayException;
10
use voku\db\exceptions\QueryException;
11
use voku\helper\UTF8;
12
13
/**
14
 * DB: This class can handle DB queries via MySQLi.
15
 *
16
 * @package voku\db
17
 */
18
final class DB
19
{
20
21
  /**
22
   * @var int
23
   */
24
  public $query_count = 0;
25
26
  /**
27
   * @var \mysqli|null
28
   */
29
  private $link;
30
31
  /**
32
   * @var bool
33
   */
34
  private $connected = false;
35
36
  /**
37
   * @var array
38
   */
39
  private $mysqlDefaultTimeFunctions;
40
41
  /**
42
   * @var string
43
   */
44
  private $hostname = '';
45
46
  /**
47
   * @var string
48
   */
49
  private $username = '';
50
51
  /**
52
   * @var string
53
   */
54
  private $password = '';
55
56
  /**
57
   * @var string
58
   */
59
  private $database = '';
60
61
  /**
62
   * @var int
63
   */
64
  private $port = 3306;
65
66
  /**
67
   * @var string
68
   */
69
  private $charset = 'utf8';
70
71
  /**
72
   * @var string
73
   */
74
  private $socket = '';
75
76
  /**
77
   * @var bool
78
   */
79
  private $session_to_db = false;
80
81
  /**
82
   * @var bool
83
   */
84
  private $_in_transaction = false;
85
86
  /**
87
   * @var bool
88
   */
89
  private $_convert_null_to_empty_string = false;
90
91
  /**
92
   * @var bool
93
   */
94
  private $_ssl = false;
95
96
  /**
97
   * The path name to the key file
98
   *
99
   * @var string
100
   */
101
  private $_clientkey;
102
103
  /**
104
   * The path name to the certificate file
105
   *
106
   * @var string
107
   */
108
  private $_clientcert;
109
110
  /**
111
   * The path name to the certificate authority file
112
   *
113
   * @var string
114
   */
115
  private $_cacert;
116
117
  /**
118
   * @var Debug
119
   */
120
  private $_debug;
121
122
  /**
123
   * __construct()
124
   *
125
   * @param string $hostname
126
   * @param string $username
127
   * @param string $password
128
   * @param string $database
129
   * @param int    $port
130
   * @param string $charset
131
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
132
   *                                      Use false to disable it.</p>
133
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
134
   *                                      Use false to disable it.</p>
135
   * @param string $logger_class_name
136
   * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
137
   * @param array  $extra_config          <p>
138
   *                                      'session_to_db' => false|true<br>
139
   *                                      'socket' => 'string (path)'<br>
140
   *                                      'ssl' => 'bool'<br>
141
   *                                      'clientkey' => 'string (path)'<br>
142
   *                                      'clientcert' => 'string (path)'<br>
143
   *                                      'cacert' => 'string (path)'<br>
144
   *                                      </p>
145
   */
146 11
  private function __construct(string $hostname, string $username, string $password, string $database, $port, string $charset, bool $exit_on_error, bool $echo_on_error, string $logger_class_name, string $logger_level, array $extra_config = [])
147
  {
148 11
    $this->_debug = new Debug($this);
149
150 11
    $this->_loadConfig(
151 11
        $hostname,
152 11
        $username,
153 11
        $password,
154 11
        $database,
155 11
        $port,
156 11
        $charset,
157 11
        $exit_on_error,
158 11
        $echo_on_error,
159 11
        $logger_class_name,
160 11
        $logger_level,
161 11
        $extra_config
162
    );
163
164 8
    $this->connect();
165
166 5
    $this->mysqlDefaultTimeFunctions = [
167
      // Returns the current date.
168
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176
      'CURTIME()',
177
      // Synonym for NOW()
178
      'LOCALTIME()',
179
      // Synonym for NOW()
180
      'LOCALTIMESTAMP()',
181
      // Returns the current date and time.
182
      'NOW()',
183
      // Returns the time at which the function executes.
184
      'SYSDATE()',
185
      // Returns a UNIX timestamp.
186
      'UNIX_TIMESTAMP()',
187
      // Returns the current UTC date.
188
      'UTC_DATE()',
189
      // Returns the current UTC time.
190
      'UTC_TIME()',
191
      // Returns the current UTC date and time.
192
      'UTC_TIMESTAMP()',
193
    ];
194 5
  }
195
196
  /**
197
   * Prevent the instance from being cloned.
198
   *
199
   * @return void
200
   */
201
  private function __clone()
202
  {
203
  }
204
205
  /**
206
   * __destruct
207
   *
208
   */
209
  public function __destruct()
210
  {
211
    // close the connection only if we don't save PHP-SESSION's in DB
212
    if ($this->session_to_db === false) {
213
      $this->close();
214
    }
215
  }
216
217
  /**
218
   * @param null|string $sql
219
   * @param array       $bindings
220
   *
221
   * @return bool|int|Result|DB           <p>
222
   *                                      "DB" by "$sql" === null<br />
223
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
224
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
225
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
226
   *                                      "true" by e.g. "DROP"-queries<br />
227
   *                                      "false" on error
228
   *                                      </p>
229
   */
230 2
  public function __invoke(string $sql = null, array $bindings = [])
231
  {
232 2
    return null !== $sql ? $this->query($sql, $bindings) : $this;
233
  }
234
235
  /**
236
   * __wakeup
237
   *
238
   * @return void
239
   */
240 2
  public function __wakeup()
241
  {
242 2
    $this->reconnect();
243 2
  }
244
245
  /**
246
   * Load the config from the constructor.
247
   *
248
   * @param string $hostname
249
   * @param string $username
250
   * @param string $password
251
   * @param string $database
252
   * @param int    $port                  <p>default is (int)3306</p>
253
   * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
254
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
255
   *                                      Use false to disable it.</p>
256
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
257
   *                                      Use false to disable it.</p>
258
   * @param string $logger_class_name
259
   * @param string $logger_level
260
   * @param array  $extra_config          <p>
261
   *                                      'session_to_db' => false|true<br>
262
   *                                      'socket' => 'string (path)'<br>
263
   *                                      'ssl' => 'bool'<br>
264
   *                                      'clientkey' => 'string (path)'<br>
265
   *                                      'clientcert' => 'string (path)'<br>
266
   *                                      'cacert' => 'string (path)'<br>
267
   *                                      </p>
268
   *
269
   * @return bool
270
   */
271 11
  private function _loadConfig(string $hostname, string $username, string $password, string $database, $port, string $charset, bool $exit_on_error, bool $echo_on_error, string $logger_class_name, string $logger_level, array $extra_config = []): bool
272
  {
273 11
    $this->hostname = $hostname;
274 11
    $this->username = $username;
275 11
    $this->password = $password;
276 11
    $this->database = $database;
277
278 11
    if ($charset) {
279 5
      $this->charset = $charset;
280
    }
281
282 11
    if ($port) {
283 5
      $this->port = (int)$port;
284
    } else {
285
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
286 7
      $this->port = (int)@ini_get('mysqli.default_port');
287
    }
288
289
    // fallback
290 11
    if (!$this->port) {
291
      $this->port = 3306;
292
    }
293
294 11
    if (!$this->socket) {
295
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
296 11
      $this->socket = @ini_get('mysqli.default_socket');
297
    }
298
299 11
    $this->_debug->setExitOnError($exit_on_error);
300 11
    $this->_debug->setEchoOnError($echo_on_error);
301
302 11
    $this->_debug->setLoggerClassName($logger_class_name);
303 11
    $this->_debug->setLoggerLevel($logger_level);
304
305 11
    if (\is_array($extra_config) === true) {
306
307 11
      $this->setConfigExtra($extra_config);
308
309
    } else {
310
311
      // only for backward compatibility
312
      $this->session_to_db = (boolean)$extra_config;
313
314
    }
315
316 11
    return $this->showConfigError();
317
  }
318
319
320
  /**
321
   * @param array $extra_config           <p>
322
   *                                      'session_to_db' => false|true<br>
323
   *                                      'socket' => 'string (path)'<br>
324
   *                                      'ssl' => 'bool'<br>
325
   *                                      'clientkey' => 'string (path)'<br>
326
   *                                      'clientcert' => 'string (path)'<br>
327
   *                                      'cacert' => 'string (path)'<br>
328
   *                                      </p>
329
   */
330 11
  public function setConfigExtra(array $extra_config)
331
  {
332 11
    if (isset($extra_config['session_to_db'])) {
333
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
334
    }
335
336 11
    if (isset($extra_config['socket'])) {
337
      $this->socket = $extra_config['socket'];
338
    }
339
340 11
    if (isset($extra_config['ssl'])) {
341
      $this->_ssl = $extra_config['ssl'];
342
    }
343
344 11
    if (isset($extra_config['clientkey'])) {
345
      $this->_clientkey = $extra_config['clientkey'];
346
    }
347
348 11
    if (isset($extra_config['clientcert'])) {
349
      $this->_clientcert = $extra_config['clientcert'];
350
    }
351
352 11
    if (isset($extra_config['cacert'])) {
353
      $this->_cacert = $extra_config['cacert'];
354
    }
355 11
  }
356
357
  /**
358
   * Parses arrays with value pairs and generates SQL to use in queries.
359
   *
360
   * @param array  $arrayPair
361
   * @param string $glue <p>This is the separator.</p>
362
   *
363
   * @return string
364
   *
365
   * @internal
366
   */
367 30
  public function _parseArrayPair(array $arrayPair, string $glue = ','): string
368
  {
369
    // init
370 30
    $sql = '';
371
372 30
    if (\count($arrayPair) === 0) {
373
      return '';
374
    }
375
376 30
    $arrayPairCounter = 0;
377 30
    foreach ($arrayPair as $_key => $_value) {
378 30
      $_connector = '=';
379 30
      $_glueHelper = '';
380 30
      $_key_upper = \strtoupper($_key);
381
382 30
      if (\strpos($_key_upper, ' NOT') !== false) {
383 2
        $_connector = 'NOT';
384
      }
385
386 30
      if (\strpos($_key_upper, ' IS') !== false) {
387 1
        $_connector = 'IS';
388
      }
389
390 30
      if (\strpos($_key_upper, ' IS NOT') !== false) {
391 1
        $_connector = 'IS NOT';
392
      }
393
394 30
      if (\strpos($_key_upper, ' IN') !== false) {
395 1
        $_connector = 'IN';
396
      }
397
398 30
      if (\strpos($_key_upper, ' NOT IN') !== false) {
399 1
        $_connector = 'NOT IN';
400
      }
401
402 30
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
403 1
        $_connector = 'BETWEEN';
404
      }
405
406 30
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
407 1
        $_connector = 'NOT BETWEEN';
408
      }
409
410 30
      if (\strpos($_key_upper, ' LIKE') !== false) {
411 2
        $_connector = 'LIKE';
412
      }
413
414 30
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
415 2
        $_connector = 'NOT LIKE';
416
      }
417
418 30 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
419 4
        $_connector = '>';
420
      }
421
422 30 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
423 1
        $_connector = '<';
424
      }
425
426 30
      if (\strpos($_key_upper, ' >=') !== false) {
427 4
        $_connector = '>=';
428
      }
429
430 30
      if (\strpos($_key_upper, ' <=') !== false) {
431 1
        $_connector = '<=';
432
      }
433
434 30
      if (\strpos($_key_upper, ' <>') !== false) {
435 1
        $_connector = '<>';
436
      }
437
438 30
      if (\strpos($_key_upper, ' OR') !== false) {
439 2
        $_glueHelper = 'OR';
440
      }
441
442 30
      if (\strpos($_key_upper, ' AND') !== false) {
443 1
        $_glueHelper = 'AND';
444
      }
445
446 30
      if (\is_array($_value) === true) {
447 2
        foreach ($_value as $oldKey => $oldValue) {
448 2
          $_value[$oldKey] = $this->secure($oldValue);
449
        }
450
451 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
452 1
          $_value = '(' . \implode(',', $_value) . ')';
453 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
454 2
          $_value = '(' . \implode(' AND ', $_value) . ')';
455
        }
456
457
      } else {
458 30
        $_value = $this->secure($_value);
459
      }
460
461 30
      $quoteString = $this->quote_string(
462 30
          \trim(
463 30
              \str_ireplace(
464
                  [
465 30
                      $_connector,
466 30
                      $_glueHelper,
467
                  ],
468 30
                  '',
469 30
                  $_key
470
              )
471
          )
472
      );
473
474 30
      $_value = (array)$_value;
475
476 30
      if (!$_glueHelper) {
477 30
        $_glueHelper = $glue;
478
      }
479
480 30
      $tmpCounter = 0;
481 30
      foreach ($_value as $valueInner) {
482
483 30
        $_glueHelperInner = $_glueHelper;
484
485 30
        if ($arrayPairCounter === 0) {
486
487 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
488 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
489 30
          } elseif ($tmpCounter === 0) {
490 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
491
          }
492
493 26
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
494 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
495
        }
496
497 30
        if (\is_string($valueInner) && $valueInner === '') {
498 1
          $valueInner = "''";
499
        }
500
501 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
502 30
        $tmpCounter++;
503
      }
504
505 30
      if ($_glueHelper === 'OR') {
506 2
        $sql .= ' ) ';
507
      }
508
509 30
      $arrayPairCounter++;
510
    }
511
512 30
    return $sql;
513
  }
514
515
  /**
516
   * _parseQueryParams
517
   *
518
   * @param string $sql
519
   * @param array  $params
520
   *
521
   * @return array <p>with the keys -> 'sql', 'params'</p>
522
   */
523 13
  private function _parseQueryParams(string $sql, array $params = []): array
524
  {
525
    // is there anything to parse?
526 View Code Duplication
    if (
527 13
        \strpos($sql, '?') === false
528
        ||
529 13
        \count($params) === 0
530
    ) {
531 11
      return ['sql' => $sql, 'params' => $params];
532
    }
533
534 3
    $parseKey = \md5(\uniqid((string)\mt_rand(), true));
535 3
    $sql = \str_replace('?', $parseKey, $sql);
536
537 3
    $k = 0;
538 3
    while (\strpos($sql, $parseKey) !== false) {
539 3
      $sql = UTF8::str_replace_first(
540 3
          $parseKey,
541 3
          (string)(isset($params[$k]) ? $this->secure($params[$k]) : ''),
542 3
          $sql
543
      );
544
545 3
      if (isset($params[$k])) {
546 3
        unset($params[$k]);
547
      }
548
549 3
      $k++;
550
    }
551
552 3
    return ['sql' => $sql, 'params' => $params];
553
  }
554
555
  /**
556
   * Gets the number of affected rows in a previous MySQL operation.
557
   *
558
   * @return int
559
   */
560 12
  public function affected_rows(): int
561
  {
562 12
    return \mysqli_affected_rows($this->link);
563
  }
564
565
  /**
566
   * Begins a transaction, by turning off auto commit.
567
   *
568
   * @return bool <p>This will return true or false indicating success of transaction</p>
569
   */
570 6 View Code Duplication
  public function beginTransaction(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
571
  {
572 6
    if ($this->_in_transaction === true) {
573 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
574
575 2
      return false;
576
    }
577
578 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
579 6
    $this->_in_transaction = true;
580 6
    $return = \mysqli_autocommit($this->link, false);
581 6
    if ($return === false) {
582
      $this->_in_transaction = false;
583
    }
584
585 6
    return $return;
586
  }
587
588
  /**
589
   * Clear the errors in "_debug->_errors".
590
   *
591
   * @return bool
592
   */
593 6
  public function clearErrors(): bool
594
  {
595 6
    return $this->_debug->clearErrors();
596
  }
597
598
  /**
599
   * Closes a previously opened database connection.
600
   */
601 2
  public function close(): bool
602
  {
603 2
    $this->connected = false;
604
605
    if (
606 2
        $this->link
607
        &&
608 2
        $this->link instanceof \mysqli
609
    ) {
610 2
      $result = \mysqli_close($this->link);
611 2
      $this->link = null;
612
613 2
      return $result;
614
    }
615
616 1
    return false;
617
  }
618
619
  /**
620
   * Commits the current transaction and end the transaction.
621
   *
622
   * @return bool <p>Boolean true on success, false otherwise.</p>
623
   */
624 2 View Code Duplication
  public function commit(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
734
  {
735
    // init
736 2
    $table = \trim($table);
737
738 2
    if ($table === '') {
739 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
740
741 1
      return false;
742
    }
743
744 2
    if (\is_string($where)) {
745 1
      $WHERE = $this->escape($where, false);
746 2
    } elseif (\is_array($where)) {
747 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
748
    } else {
749 1
      $WHERE = '';
750
    }
751
752 2
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
753
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
754
    }
755
756 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
757
758 2
    return $this->query($sql);
759
  }
760
761
  /**
762
   * Ends a transaction and commits if no errors, then ends autocommit.
763
   *
764
   * @return bool <p>This will return true or false indicating success of transactions.</p>
765
   */
766 4 View Code Duplication
  public function endTransaction(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
767
  {
768 4
    if ($this->_in_transaction === false) {
769
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
770
771
      return false;
772
    }
773
774 4
    if (!$this->errors()) {
775 1
      $return = \mysqli_commit($this->link);
776
    } else {
777 3
      $this->rollback();
778 3
      $return = false;
779
    }
780
781 4
    \mysqli_autocommit($this->link, true);
782 4
    $this->_in_transaction = false;
783
784 4
    return $return;
785
  }
786
787
  /**
788
   * Get all errors from "$this->_errors".
789
   *
790
   * @return array|false <p>false === on errors</p>
791
   */
792 4
  public function errors()
793
  {
794 4
    $errors = $this->_debug->getErrors();
795
796 4
    return \count($errors) > 0 ? $errors : false;
797
  }
798
799
  /**
800
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
801
   *
802
   * @param mixed $sql    <p>The SQL string.</p>
803
   * @param array $params <p>An array of key-value bindings.</p>
804
   *
805
   * @return array <p>with the keys -> 'sql', 'params'</p>
806
   */
807 14
  public function _parseQueryParamsByName(string $sql, array $params = []): array
808
  {
809
    // is there anything to parse?
810 View Code Duplication
    if (
811 14
        \strpos($sql, ':') === false
812
        ||
813 14
        \count($params) === 0
814
    ) {
815 3
      return ['sql' => $sql, 'params' => $params];
816
    }
817
818 12
    $parseKey = \md5(\uniqid((string)\mt_rand(), true));
819
820 12
    foreach ($params as $name => $value) {
821 12
      $nameTmp = $name;
822 12
      if (\strpos($name, ':') === 0) {
823 10
        $nameTmp = \substr($name, 1);
824
      }
825
826 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
827 12
      $sql = \str_replace(':' . $nameTmp, $parseKeyInner, $sql);
828
    }
829
830 12
    foreach ($params as $name => $value) {
831 12
      $nameTmp = $name;
832 12
      if (\strpos($name, ':') === 0) {
833 10
        $nameTmp = \substr($name, 1);
834
      }
835
836 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
837 12
      $sqlBefore = $sql;
838 12
      $secureParamValue = $this->secure($params[$name]);
839
840 12
      while (\strpos($sql, $parseKeyInner) !== false) {
841 12
        $sql = UTF8::str_replace_first(
842 12
            $parseKeyInner,
843 12
            (string)$secureParamValue,
844 12
            $sql
845
        );
846
      }
847
848 12
      if ($sqlBefore !== $sql) {
849 12
        unset($params[$name]);
850
      }
851
    }
852
853 12
    return ['sql' => $sql, 'params' => $params];
854
  }
855
856
  /**
857
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
858
   *
859
   * @param mixed     $var           boolean: convert into "integer"<br />
860
   *                                 int: int (don't change it)<br />
861
   *                                 float: float (don't change it)<br />
862
   *                                 null: null (don't change it)<br />
863
   *                                 array: run escape() for every key => value<br />
864
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
865
   * @param bool      $stripe_non_utf8
866
   * @param bool      $html_entity_decode
867
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
868
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
869
   *                                 <strong>null</strong> => Convert the array into null, every time.
870
   *
871
   * @return mixed
872
   */
873 55
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
874
  {
875
    // [empty]
876 55
    if ($var === '') {
877 2
      return '';
878
    }
879
880
    // ''
881 55
    if ($var === "''") {
882
      return "''";
883
    }
884
885
    // NULL
886 55
    if ($var === null) {
887
      if (
888 2
          $this->_convert_null_to_empty_string === true
889
      ) {
890
        return "''";
891
      }
892
893 2
      return 'NULL';
894
    }
895
896
    // save the current value as int (for later usage)
897 55
    if (!\is_object($var)) {
898 55
      $varInt = (int)$var;
899
    }
900
901
    // "int" || int || bool
902
    if (
903 55
        \is_int($var)
904
        ||
905 52
        \is_bool($var)
906
        ||
907
        (
908 52
            isset($varInt, $var[0])
909
            &&
910 52
            $var[0] != '0'
911
            &&
912 55
            "$varInt" == $var
0 ignored issues
show
Bug introduced by
The variable $varInt does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
913
        )
914
    ) {
915 32
      return (int)$var;
916
    }
917
918
    // float
919 52
    if (\is_float($var)) {
920 5
      return $var;
921
    }
922
923
    // array
924 52
    if (\is_array($var)) {
925
926 4
      if ($convert_array === null) {
927 3
        return null;
928
      }
929
930 2
      $varCleaned = [];
931 2
      foreach ((array)$var as $key => $value) {
932
933 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
934 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
935
936
        /** @noinspection OffsetOperationsInspection */
937 2
        $varCleaned[$key] = $value;
938
      }
939
940 2
      if ($convert_array === true) {
941 1
        $varCleaned = \implode(',', $varCleaned);
942
943 1
        return $varCleaned;
944
      }
945
946 2
      return $varCleaned;
947
    }
948
949
    // "string"
950
    if (
951 52
        \is_string($var)
952
        ||
953
        (
954 3
            \is_object($var)
955
            &&
956 52
            method_exists($var, '__toString')
957
        )
958
    ) {
959 52
      $var = (string)$var;
960
961 52
      if ($stripe_non_utf8 === true) {
962 11
        $var = UTF8::cleanup($var);
963
      }
964
965 52
      if ($html_entity_decode === true) {
966
        // use no-html-entity for db
967 1
        $var = UTF8::html_entity_decode($var);
968
      }
969
970 52
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
971
972 52
      $var = \mysqli_real_escape_string($this->getLink(), $var);
973
974 52
      return (string)$var;
975
976
    }
977
978
    // "DateTime"-object
979 3
    if ($var instanceof \DateTime) {
980 3
      return $this->escape($var->format('Y-m-d H:i:s'), false);
981
    }
982
983 2
    return false;
984
  }
985
986
  /**
987
   * Execute select/insert/update/delete sql-queries.
988
   *
989
   * @param string $query    <p>sql-query</p>
990
   * @param bool   $useCache <p>use cache?</p>
991
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
992
   * @param DB     $db       optional <p>the database connection</p>
993
   *
994
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
995
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
996
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
997
   *               "true" by e.g. "DROP"-queries<br />
998
   *               "false" on error
999
   *
1000
   * @throws QueryException
1001
   */
1002 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1003
  {
1004
    // init
1005 3
    $cacheKey = null;
1006 3
    if (!$db) {
1007 3
      $db = self::getInstance();
1008
    }
1009
1010 3 View Code Duplication
    if ($useCache === true) {
1011 1
      $cache = new Cache(null, null, false, $useCache);
1012 1
      $cacheKey = 'sql-' . \md5($query);
1013
1014
      if (
1015 1
          $cache->getCacheIsReady() === true
1016
          &&
1017 1
          $cache->existsItem($cacheKey)
1018
      ) {
1019 1
        return $cache->getItem($cacheKey);
1020
      }
1021
1022
    } else {
1023 3
      $cache = false;
1024
    }
1025
1026 3
    $result = $db->query($query);
1027
1028 3
    if ($result instanceof Result) {
1029
1030 1
      $return = $result->fetchAllArray();
1031
1032
      // save into the cache
1033 View Code Duplication
      if (
1034 1
          $cacheKey !== null
1035
          &&
1036 1
          $useCache === true
1037
          &&
1038 1
          $cache instanceof Cache
1039
          &&
1040 1
          $cache->getCacheIsReady() === true
1041
      ) {
1042 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1043
      }
1044
1045
    } else {
1046 2
      $return = $result;
1047
    }
1048
1049 3
    return $return;
1050
  }
1051
1052
  /**
1053
   * Get all table-names via "SHOW TABLES".
1054
   *
1055
   * @return array
1056
   */
1057 1
  public function getAllTables(): array
1058
  {
1059 1
    $query = 'SHOW TABLES';
1060 1
    $result = $this->query($query);
1061
1062 1
    return $result->fetchAllArray();
1063
  }
1064
1065
  /**
1066
   * @return Debug
1067
   */
1068 9
  public function getDebugger(): Debug
1069
  {
1070 9
    return $this->_debug;
1071
  }
1072
1073
  /**
1074
   * Get errors from "$this->_errors".
1075
   *
1076
   * @return array
1077
   */
1078 1
  public function getErrors(): array
1079
  {
1080 1
    return $this->_debug->getErrors();
1081
  }
1082
1083
  /**
1084
   * getInstance()
1085
   *
1086
   * @param string $hostname
1087
   * @param string $username
1088
   * @param string $password
1089
   * @param string $database
1090
   * @param int    $port                 <p>default is (int)3306</p>
1091
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1092
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1093
   *                                     Use false to disable it.</p>
1094
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1095
   *                                     Use false to disable it.</p>
1096
   * @param string $logger_class_name
1097
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1098
   * @param array  $extra_config         <p>
1099
   *                                     'session_to_db' => false|true<br>
1100
   *                                     'socket' => 'string (path)'<br>
1101
   *                                     'ssl' => 'bool'<br>
1102
   *                                     'clientkey' => 'string (path)'<br>
1103
   *                                     'clientcert' => 'string (path)'<br>
1104
   *                                     'cacert' => 'string (path)'<br>
1105
   *                                     </p>
1106
   *
1107
   * @return \voku\db\DB
1108
   */
1109 97
  public static function getInstance(string $hostname = '', string $username = '', string $password = '', string $database = '', $port = 3306, string $charset = 'utf8', bool $exit_on_error = true, bool $echo_on_error = true, string $logger_class_name = '', string $logger_level = '', array $extra_config = []): self
1110
  {
1111
    /**
1112
     * @var $instance DB[]
1113
     */
1114 97
    static $instance = [];
1115
1116
    /**
1117
     * @var $firstInstance DB
1118
     */
1119 97
    static $firstInstance = null;
1120
1121
    if (
1122 97
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1123
        &&
1124 97
        null !== $firstInstance
1125
    ) {
1126 14
      return $firstInstance;
1127
    }
1128
1129 97
    $extra_config_string = '';
1130 97
    if (\is_array($extra_config) === true) {
1131 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1132 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1133
      }
1134
    } else {
1135
      // only for backward compatibility
1136
      $extra_config_string = (int)$extra_config;
1137
    }
1138
1139 97
    $connection = \md5(
1140 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1141
    );
1142
1143 97
    if (!isset($instance[$connection])) {
1144 11
      $instance[$connection] = new self(
1145 11
          $hostname,
1146 11
          $username,
1147 11
          $password,
1148 11
          $database,
1149 11
          $port,
1150 11
          $charset,
1151 11
          $exit_on_error,
1152 11
          $echo_on_error,
1153 11
          $logger_class_name,
1154 11
          $logger_level,
1155 11
          $extra_config
1156
      );
1157
1158 5
      if (null === $firstInstance) {
1159 1
        $firstInstance = $instance[$connection];
1160
      }
1161
    }
1162
1163 97
    return $instance[$connection];
1164
  }
1165
1166
  /**
1167
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1168
   *
1169
   * @return \mysqli
1170
   */
1171 56
  public function getLink(): \mysqli
1172
  {
1173 56
    return $this->link;
1174
  }
1175
1176
  /**
1177
   * Get the current charset.
1178
   *
1179
   * @return string
1180
   */
1181 1
  public function get_charset(): string
1182
  {
1183 1
    return $this->charset;
1184
  }
1185
1186
  /**
1187
   * Check if we are in a transaction.
1188
   *
1189
   * @return bool
1190
   */
1191
  public function inTransaction(): bool
1192
  {
1193
    return $this->_in_transaction;
1194
  }
1195
1196
  /**
1197
   * Execute a "insert"-query.
1198
   *
1199
   * @param string      $table
1200
   * @param array       $data
1201
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1202
   *
1203
   * @return false|int <p>false on error</p>
1204
   *
1205
   * @throws QueryException
1206
   */
1207 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1208
  {
1209
    // init
1210 28
    $table = \trim($table);
1211
1212 28
    if ($table === '') {
1213 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1214
1215 2
      return false;
1216
    }
1217
1218 27
    if (\count($data) === 0) {
1219 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1220
1221 3
      return false;
1222
    }
1223
1224 25
    $SET = $this->_parseArrayPair($data);
1225
1226 25
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1227
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1228
    }
1229
1230 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1231
1232 25
    return $this->query($sql);
1233
  }
1234
1235
  /**
1236
   * Returns the auto generated id used in the last query.
1237
   *
1238
   * @return int|string
1239
   */
1240 58
  public function insert_id()
1241
  {
1242 58
    return \mysqli_insert_id($this->link);
1243
  }
1244
1245
  /**
1246
   * Check if db-connection is ready.
1247
   *
1248
   * @return boolean
1249
   */
1250 106
  public function isReady(): bool
1251
  {
1252 106
    return $this->connected ? true : false;
1253
  }
1254
1255
  /**
1256
   * Get the last sql-error.
1257
   *
1258
   * @return string|false <p>false === there was no error</p>
1259
   */
1260 1
  public function lastError()
1261
  {
1262 1
    $errors = $this->_debug->getErrors();
1263
1264 1
    return \count($errors) > 0 ? end($errors) : false;
1265
  }
1266
1267
  /**
1268
   * Execute a sql-multi-query.
1269
   *
1270
   * @param string $sql
1271
   *
1272
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1273
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1274
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1275
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1276
   *
1277
   * @throws QueryException
1278
   */
1279 1
  public function multi_query(string $sql)
1280
  {
1281 1
    if (!$this->isReady()) {
1282
      return false;
1283
    }
1284
1285 1 View Code Duplication
    if (!$sql || $sql === '') {
1286 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1287
1288 1
      return false;
1289
    }
1290
1291 1
    $query_start_time = microtime(true);
1292 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1293 1
    $query_duration = microtime(true) - $query_start_time;
1294
1295 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1296
1297 1
    $returnTheResult = false;
1298 1
    $result = [];
1299
1300 1
    if ($resultTmp) {
1301
      do {
1302
1303 1
        $resultTmpInner = \mysqli_store_result($this->link);
1304
1305 1
        if ($resultTmpInner instanceof \mysqli_result) {
1306
1307 1
          $returnTheResult = true;
1308 1
          $result[] = new Result($sql, $resultTmpInner);
1309
1310
        } else {
1311
1312
          // is the query successful
1313 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1314 1
            $result[] = true;
1315
          } else {
1316
            $result[] = false;
1317
          }
1318
1319
        }
1320
1321 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1322
1323
    } else {
1324
1325
      // log the error query
1326 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1327
1328 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1329
    }
1330
1331
    // return the result only if there was a "SELECT"-query
1332 1
    if ($returnTheResult === true) {
1333 1
      return $result;
1334
    }
1335
1336
    if (
1337 1
        \count($result) > 0
1338
        &&
1339 1
        \in_array(false, $result, true) === false
1340
    ) {
1341 1
      return true;
1342
    }
1343
1344
    return false;
1345
  }
1346
1347
  /**
1348
   * Pings a server connection, or tries to reconnect
1349
   * if the connection has gone down.
1350
   *
1351
   * @return boolean
1352
   */
1353 3
  public function ping(): bool
1354
  {
1355
    if (
1356 3
        $this->link
1357
        &&
1358 3
        $this->link instanceof \mysqli
1359
    ) {
1360
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1361 2
      return (bool)@\mysqli_ping($this->link);
1362
    }
1363
1364 1
    return false;
1365
  }
1366
1367
  /**
1368
   * Get a new "Prepare"-Object for your sql-query.
1369
   *
1370
   * @param string $query
1371
   *
1372
   * @return Prepare
1373
   */
1374 2
  public function prepare(string $query): Prepare
1375
  {
1376 2
    return new Prepare($this, $query);
1377
  }
1378
1379
  /**
1380
   * Execute a sql-query and return the result-array for select-statements.
1381
   *
1382
   * @param string $query
1383
   *
1384
   * @return mixed
1385
   * @deprecated
1386
   * @throws \Exception
1387
   */
1388 1
  public static function qry(string $query)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1389
  {
1390 1
    $db = self::getInstance();
1391
1392 1
    $args = \func_get_args();
1393
    /** @noinspection SuspiciousAssignmentsInspection */
1394 1
    $query = \array_shift($args);
1395 1
    $query = \str_replace('?', '%s', $query);
1396 1
    $args = \array_map(
1397
        [
1398 1
            $db,
1399 1
            'escape',
1400
        ],
1401 1
        $args
1402
    );
1403 1
    \array_unshift($args, $query);
1404 1
    $query = \sprintf(...$args);
1405 1
    $result = $db->query($query);
1406
1407 1
    if ($result instanceof Result) {
1408 1
      return $result->fetchAllArray();
1409
    }
1410
1411 1
    return $result;
1412
  }
1413
1414
  /**
1415
   * Execute a sql-query.
1416
   *
1417
   * @param string        $sql            <p>The sql query-string.</p>
1418
   *
1419
   * @param array|boolean $params         <p>
1420
   *                                      "array" of sql-query-parameters<br/>
1421
   *                                      "false" if you don't need any parameter (default)<br/>
1422
   *                                      </p>
1423
   *
1424
   * @return bool|int|Result              <p>
1425
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1426
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1427
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1428
   *                                      "true" by e.g. "DROP"-queries<br />
1429
   *                                      "false" on error
1430
   *                                      </p>
1431
   *
1432
   * @throws QueryException
1433
   */
1434 96
  public function query(string $sql = '', $params = false)
1435
  {
1436 96
    if (!$this->isReady()) {
1437
      return false;
1438
    }
1439
1440 96 View Code Duplication
    if (!$sql || $sql === '') {
1441 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1442
1443 4
      return false;
1444
    }
1445
1446
    if (
1447 94
        $params !== false
1448
        &&
1449 94
        \is_array($params)
1450
        &&
1451 94
        \count($params) > 0
1452
    ) {
1453 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1454 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1455 13
      $sql = $parseQueryParamsByName['sql'];
1456
    }
1457
1458
    // DEBUG
1459
    // var_dump($params);
1460
    // echo $sql . "\n";
1461
1462 94
    $query_start_time = microtime(true);
1463 94
    $query_result = \mysqli_real_query($this->link, $sql);
1464 94
    $query_duration = microtime(true) - $query_start_time;
1465
1466 94
    $this->query_count++;
1467
1468 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1469 94
    if ($mysqli_field_count) {
1470 62
      $result = \mysqli_store_result($this->link);
1471
    } else {
1472 64
      $result = $query_result;
1473
    }
1474
1475 94
    if ($result instanceof \mysqli_result) {
1476
1477
      // log the select query
1478 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1479
1480
      // return query result object
1481 61
      return new Result($sql, $result);
1482
    }
1483
1484 66
    if ($query_result === true) {
1485
1486
      // "INSERT" || "REPLACE"
1487 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1488 58
        $insert_id = (int)$this->insert_id();
1489 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1490
1491 58
        return $insert_id;
1492
      }
1493
1494
      // "UPDATE" || "DELETE"
1495 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1496 12
        $affected_rows = $this->affected_rows();
1497 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1498
1499 12
        return $affected_rows;
1500
      }
1501
1502
      // log the ? query
1503 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1504
1505 27
      return true;
1506
    }
1507
1508
    // log the error query
1509 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1510
1511 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1512
  }
1513
1514
  /**
1515
   * Error-handling for the sql-query.
1516
   *
1517
   * @param string     $errorMessage
1518
   * @param int        $errorNumber
1519
   * @param string     $sql
1520
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1521
   * @param bool       $sqlMultiQuery
1522
   *
1523
   * @throws QueryException
1524
   * @throws DBGoneAwayException
1525
   *
1526
   * @return mixed|false
1527
   */
1528 13
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1529
  {
1530
    if (
1531 13
        $errorMessage === 'DB server has gone away'
1532
        ||
1533 12
        $errorMessage === 'MySQL server has gone away'
1534
        ||
1535 13
        $errorNumber === 2006
1536
    ) {
1537 1
      static $RECONNECT_COUNTER;
1538
1539
      // exit if we have more then 3 "DB server has gone away"-errors
1540 1
      if ($RECONNECT_COUNTER > 3) {
1541
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1542
        throw new DBGoneAwayException($errorMessage);
1543
      }
1544
1545 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1546
1547
      // reconnect
1548 1
      $RECONNECT_COUNTER++;
1549 1
      $this->reconnect(true);
1550
1551
      // re-run the current (non multi) query
1552 1
      if ($sqlMultiQuery === false) {
1553 1
        return $this->query($sql, $sqlParams);
1554
      }
1555
1556
      return false;
1557
    }
1558
1559 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1560
1561 12
    $force_exception_after_error = null; // auto
1562 12
    if ($this->_in_transaction === true) {
1563 4
      $force_exception_after_error = false;
1564
    }
1565
    // this query returned an error, we must display it (only for dev) !!!
1566
1567 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1568
1569 12
    return false;
1570
  }
1571
1572
  /**
1573
   * Quote && Escape e.g. a table name string.
1574
   *
1575
   * @param mixed $str
1576
   *
1577
   * @return string
1578
   */
1579 36
  public function quote_string($str): string
1580
  {
1581 36
    $str = \str_replace(
1582 36
        '`',
1583 36
        '``',
1584 36
        \trim(
1585 36
            (string)$this->escape($str, false),
1586 36
            '`'
1587
        )
1588
    );
1589
1590 36
    return '`' . $str . '`';
1591
  }
1592
1593
  /**
1594
   * Reconnect to the MySQL-Server.
1595
   *
1596
   * @param bool $checkViaPing
1597
   *
1598
   * @return bool
1599
   */
1600 3
  public function reconnect(bool $checkViaPing = false): bool
1601
  {
1602 3
    $ping = false;
1603
1604 3
    if ($checkViaPing === true) {
1605 2
      $ping = $this->ping();
1606
    }
1607
1608 3
    if ($ping !== true) {
1609 3
      $this->connected = false;
1610 3
      $this->connect();
1611
    }
1612
1613 3
    return $this->isReady();
1614
  }
1615
1616
  /**
1617
   * Execute a "replace"-query.
1618
   *
1619
   * @param string      $table
1620
   * @param array       $data
1621
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1622
   *
1623
   * @return false|int <p>false on error</p>
1624
   *
1625
   * @throws QueryException
1626
   */
1627 1
  public function replace(string $table, array $data = [], string $databaseName = null)
1628
  {
1629
    // init
1630 1
    $table = \trim($table);
1631
1632 1
    if ($table === '') {
1633 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1634
1635 1
      return false;
1636
    }
1637
1638 1
    if (\count($data) === 0) {
1639 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1640
1641 1
      return false;
1642
    }
1643
1644
    // extracting column names
1645 1
    $columns = \array_keys($data);
1646 1
    foreach ($columns as $k => $_key) {
1647
      /** @noinspection AlterInForeachInspection */
1648 1
      $columns[$k] = $this->quote_string($_key);
1649
    }
1650
1651 1
    $columns = \implode(',', $columns);
1652
1653
    // extracting values
1654 1
    foreach ($data as $k => $_value) {
1655
      /** @noinspection AlterInForeachInspection */
1656 1
      $data[$k] = $this->secure($_value);
1657
    }
1658 1
    $values = \implode(',', $data);
1659
1660 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...
1661
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1662
    }
1663
1664 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1665
1666 1
    return $this->query($sql);
1667
  }
1668
1669
  /**
1670
   * Rollback in a transaction and end the transaction.
1671
   *
1672
   * @return bool <p>Boolean true on success, false otherwise.</p>
1673
   */
1674 4 View Code Duplication
  public function rollback(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

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

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

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