Completed
Push — master ( b8336d...bb09fb )
by Lars
04:22
created

DB::multi_query()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 67
Code Lines 35

Duplication

Lines 5
Ratio 7.46 %

Code Coverage

Tests 26
CRAP Score 13.1868

Importance

Changes 0
Metric Value
dl 5
loc 67
ccs 26
cts 29
cp 0.8966
rs 5.8281
c 0
b 0
f 0
cc 13
eloc 35
nc 12
nop 1
crap 13.1868

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
975
    }
976
977 55
    return $var;
978
  }
979
980
  /**
981
   * Execute select/insert/update/delete sql-queries.
982
   *
983
   * @param string  $query    <p>sql-query</p>
984
   * @param bool    $useCache optional <p>use cache?</p>
985
   * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
986
   * @param DB|null $db       optional <p>the database connection</p>
987
   *
988
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
989
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
990
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
991
   *               "true" by e.g. "DROP"-queries<br />
992
   *               "false" on error
993
   *
994
   * @throws QueryException
995
   */
996 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, DB $db = null)
997
  {
998
    // init
999 3
    $cacheKey = null;
1000 3
    if (!$db) {
1001 3
      $db = self::getInstance();
1002
    }
1003
1004 3 View Code Duplication
    if ($useCache === true) {
1005 1
      $cache = new Cache(null, null, false, $useCache);
1006 1
      $cacheKey = 'sql-' . \md5($query);
1007
1008
      if (
1009 1
          $cache->getCacheIsReady() === true
1010
          &&
1011 1
          $cache->existsItem($cacheKey)
1012
      ) {
1013 1
        return $cache->getItem($cacheKey);
1014
      }
1015
1016
    } else {
1017 3
      $cache = false;
1018
    }
1019
1020 3
    $result = $db->query($query);
1021
1022 3
    if ($result instanceof Result) {
1023
1024 1
      $return = $result->fetchAllArray();
1025
1026
      // save into the cache
1027 View Code Duplication
      if (
1028 1
          $cacheKey !== null
1029
          &&
1030 1
          $useCache === true
1031
          &&
1032 1
          $cache instanceof Cache
1033
          &&
1034 1
          $cache->getCacheIsReady() === true
1035
      ) {
1036 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1037
      }
1038
1039
    } else {
1040 2
      $return = $result;
1041
    }
1042
1043 3
    return $return;
1044
  }
1045
1046
  /**
1047
   * Get all table-names via "SHOW TABLES".
1048
   *
1049
   * @return array
1050
   */
1051 1
  public function getAllTables(): array
1052
  {
1053 1
    $query = 'SHOW TABLES';
1054 1
    $result = $this->query($query);
1055
1056 1
    return $result->fetchAllArray();
1057
  }
1058
1059
  /**
1060
   * @return Debug
1061
   */
1062 9
  public function getDebugger(): Debug
1063
  {
1064 9
    return $this->_debug;
1065
  }
1066
1067
  /**
1068
   * Get errors from "$this->_errors".
1069
   *
1070
   * @return array
1071
   */
1072 1
  public function getErrors(): array
1073
  {
1074 1
    return $this->_debug->getErrors();
1075
  }
1076
1077
  /**
1078
   * getInstance()
1079
   *
1080
   * @param string $hostname
1081
   * @param string $username
1082
   * @param string $password
1083
   * @param string $database
1084
   * @param int    $port                 <p>default is (int)3306</p>
1085
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1086
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1087
   *                                     Use false to disable it.</p>
1088
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1089
   *                                     Use false to disable it.</p>
1090
   * @param string $logger_class_name
1091
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1092
   * @param array  $extra_config         <p>
1093
   *                                     're_connect'    => bool<br>
1094
   *                                     'session_to_db' => bool<br>
1095
   *                                     'socket'        => 'string (path)'<br>
1096
   *                                     'ssl'           => bool<br>
1097
   *                                     'clientkey'     => 'string (path)'<br>
1098
   *                                     'clientcert'    => 'string (path)'<br>
1099
   *                                     'cacert'        => 'string (path)'<br>
1100
   *                                     </p>
1101
   *
1102
   * @return self
1103
   */
1104 97
  public static function getInstance(
1105
      string $hostname = '',
1106
      string $username = '',
1107
      string $password = '',
1108
      string $database = '',
1109
      $port = 3306,
1110
      string $charset = 'utf8',
1111
      bool $exit_on_error = true,
1112
      bool $echo_on_error = true,
1113
      string $logger_class_name = '',
1114
      string $logger_level = '',
1115
      array $extra_config = []
1116
  ): self
1117
  {
1118
    /**
1119
     * @var $instance self[]
1120
     */
1121 97
    static $instance = [];
1122
1123
    /**
1124
     * @var $firstInstance self
1125
     */
1126 97
    static $firstInstance = null;
1127
1128
    if (
1129 97
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1130
        &&
1131 97
        null !== $firstInstance
1132
    ) {
1133 14
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1134
        $firstInstance->reconnect(true);
1135
      }
1136
1137 14
      return $firstInstance;
1138
    }
1139
1140 97
    $extra_config_string = '';
1141 97
    if (\is_array($extra_config) === true) {
1142 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1143 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1144
      }
1145
    } else {
1146
      // only for backward compatibility
1147
      $extra_config_string = (int)$extra_config;
1148
    }
1149
1150 97
    $connection = \md5(
1151 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1152
    );
1153
1154 97
    if (!isset($instance[$connection])) {
1155 11
      $instance[$connection] = new self(
1156 11
          $hostname,
1157 11
          $username,
1158 11
          $password,
1159 11
          $database,
1160 11
          $port,
1161 11
          $charset,
1162 11
          $exit_on_error,
1163 11
          $echo_on_error,
1164 11
          $logger_class_name,
1165 11
          $logger_level,
1166 11
          $extra_config
1167
      );
1168
1169 5
      if (null === $firstInstance) {
1170 1
        $firstInstance = $instance[$connection];
1171
      }
1172
    }
1173
1174 97
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1175
      $instance[$connection]->reconnect(true);
1176
    }
1177
1178 97
    return $instance[$connection];
1179
  }
1180
1181
  /**
1182
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1183
   *
1184
   * @return \mysqli
1185
   */
1186 55
  public function getLink(): \mysqli
1187
  {
1188 55
    return $this->link;
1189
  }
1190
1191
  /**
1192
   * Get the current charset.
1193
   *
1194
   * @return string
1195
   */
1196 1
  public function get_charset(): string
1197
  {
1198 1
    return $this->charset;
1199
  }
1200
1201
  /**
1202
   * Check if we are in a transaction.
1203
   *
1204
   * @return bool
1205
   */
1206
  public function inTransaction(): bool
1207
  {
1208
    return $this->_in_transaction;
1209
  }
1210
1211
  /**
1212
   * Execute a "insert"-query.
1213
   *
1214
   * @param string      $table
1215
   * @param array       $data
1216
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1217
   *
1218
   * @return false|int <p>false on error</p>
1219
   *
1220
   * @throws QueryException
1221
   */
1222 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1223
  {
1224
    // init
1225 28
    $table = \trim($table);
1226
1227 28
    if ($table === '') {
1228 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1229
1230 2
      return false;
1231
    }
1232
1233 27
    if (\count($data) === 0) {
1234 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1235
1236 3
      return false;
1237
    }
1238
1239 25
    $SET = $this->_parseArrayPair($data);
1240
1241 25
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1676
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1677
    }
1678
1679 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
1680
1681 1
    return $this->query($sql);
1682
  }
1683
1684
  /**
1685
   * Rollback in a transaction and end the transaction.
1686
   *
1687
   * @return bool <p>Boolean true on success, false otherwise.</p>
1688
   */
1689 4 View Code Duplication
  public function rollback(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1690
  {
1691 4
    if ($this->_in_transaction === false) {
1692
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1693
1694
      return false;
1695
    }
1696
1697 4
    $return = \mysqli_rollback($this->link);
1698 4
    \mysqli_autocommit($this->link, true);
1699 4
    $this->_in_transaction = false;
1700
1701 4
    return $return;
1702
  }
1703
1704
  /**
1705
   * Try to secure a variable, so can you use it in sql-queries.
1706
   *
1707
   * <p>
1708
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1709
   * 1. parse into "int"
1710
   * </p><br />
1711
   *
1712
   * <p>
1713
   * <strong>float:</strong><br />
1714
   * 1. return "float"
1715
   * </p><br />
1716
   *
1717
   * <p>
1718
   * <strong>string:</strong><br />
1719
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1720
   * 2. trim '<br />
1721
   * 3. escape the string (and remove non utf-8 chars)<br />
1722
   * 4. trim ' again (because we maybe removed some chars)<br />
1723
   * 5. add ' around the new string<br />
1724
   * </p><br />
1725
   *
1726
   * <p>
1727
   * <strong>array:</strong><br />
1728
   * 1. return null
1729
   * </p><br />
1730
   *
1731
   * <p>
1732
   * <strong>object:</strong><br />
1733
   * 1. return false
1734
   * </p><br />
1735
   *
1736
   * <p>
1737
   * <strong>null:</strong><br />
1738
   * 1. return null
1739
   * </p>
1740
   *
1741
   * @param mixed $var
1742
   *
1743
   * @return mixed
1744
   */
1745 45
  public function secure($var)
1746
  {
1747 45
    if ($var === '') {
1748 2
      return "''";
1749
    }
1750
1751 45
    if ($var === "''") {
1752 1
      return "''";
1753
    }
1754
1755 45
    if ($var === null) {
1756 2
      if ($this->_convert_null_to_empty_string === true) {
1757 1
        return "''";
1758
      }
1759
1760 2
      return 'NULL';
1761
    }
1762
1763 44
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1764 1
      return $var;
1765
    }
1766
1767 44
    if (\is_string($var)) {
1768 35
      $var = \trim($var, "'");
1769
    }
1770
1771 44
    $var = $this->escape($var, false, false, null);
1772
1773 43
    if (\is_string($var)) {
1774 35
      $var = "'" . \trim($var, "'") . "'";
1775
    }
1776
1777 43
    return $var;
1778
  }
1779
1780
  /**
1781
   * Execute a "select"-query.
1782
   *
1783
   * @param string       $table
1784
   * @param string|array $where
1785
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1786
   *
1787
   * @return false|Result <p>false on error</p>
1788
   *
1789
   * @throws QueryException
1790
   */
1791 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...
1792
  {
1793
    // init
1794 24
    $table = \trim($table);
1795
1796 24
    if ($table === '') {
1797 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1798
1799 1
      return false;
1800
    }
1801
1802 24
    if (\is_string($where)) {
1803 8
      $WHERE = $this->escape($where, false);
1804 17
    } elseif (\is_array($where)) {
1805 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1806
    } else {
1807 1
      $WHERE = '';
1808
    }
1809
1810 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...
1811
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1812
    }
1813
1814 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
1815
1816 24
    return $this->query($sql);
1817
  }
1818
1819
  /**
1820
   * Selects a different database than the one specified on construction.
1821
   *
1822
   * @param string $database <p>Database name to switch to.</p>
1823
   *
1824
   * @return bool <p>Boolean true on success, false otherwise.</p>
1825
   */
1826
  public function select_db(string $database): bool
1827
  {
1828
    if (!$this->isReady()) {
1829
      return false;
1830
    }
1831
1832
    return mysqli_select_db($this->link, $database);
1833
  }
1834
1835
  /**
1836
   * Set the current charset.
1837
   *
1838
   * @param string $charset
1839
   *
1840
   * @return bool
1841
   */
1842 8
  public function set_charset(string $charset): bool
1843
  {
1844 8
    $charsetLower = strtolower($charset);
1845 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1846 6
      $charset = 'utf8';
1847
    }
1848 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1849 6
      $charset = 'utf8mb4';
1850
    }
1851
1852 8
    $this->charset = $charset;
1853
1854 8
    $return = mysqli_set_charset($this->link, $charset);
1855
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1856 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1857
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1858 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...
1859
1860 8
    return $return;
1861
  }
1862
1863
  /**
1864
   * Set the option to convert null to "''" (empty string).
1865
   *
1866
   * Used in secure() => select(), insert(), update(), delete()
1867
   *
1868
   * @deprecated It's not recommended to convert NULL into an empty string!
1869
   *
1870
   * @param bool $bool
1871
   *
1872
   * @return self
1873
   */
1874 1
  public function set_convert_null_to_empty_string(bool $bool): self
1875
  {
1876 1
    $this->_convert_null_to_empty_string = $bool;
1877
1878 1
    return $this;
1879
  }
1880
1881
  /**
1882
   * Enables or disables internal report functions
1883
   *
1884
   * @link http://php.net/manual/en/function.mysqli-report.php
1885
   *
1886
   * @param int $flags <p>
1887
   *                   <table>
1888
   *                   Supported flags
1889
   *                   <tr valign="top">
1890
   *                   <td>Name</td>
1891
   *                   <td>Description</td>
1892
   *                   </tr>
1893
   *                   <tr valign="top">
1894
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1895
   *                   <td>Turns reporting off</td>
1896
   *                   </tr>
1897
   *                   <tr valign="top">
1898
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1899
   *                   <td>Report errors from mysqli function calls</td>
1900
   *                   </tr>
1901
   *                   <tr valign="top">
1902
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1903
   *                   <td>
1904
   *                   Throw <b>mysqli_sql_exception</b> for errors
1905
   *                   instead of warnings
1906
   *                   </td>
1907
   *                   </tr>
1908
   *                   <tr valign="top">
1909
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1910
   *                   <td>Report if no index or bad index was used in a query</td>
1911
   *                   </tr>
1912
   *                   <tr valign="top">
1913
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1914
   *                   <td>Set all options (report all)</td>
1915
   *                   </tr>
1916
   *                   </table>
1917
   *                   </p>
1918
   *
1919
   * @return bool
1920
   */
1921
  public function set_mysqli_report(int $flags): bool
1922
  {
1923
    return \mysqli_report($flags);
1924
  }
1925
1926
  /**
1927
   * Show config errors by throw exceptions.
1928
   *
1929
   * @return bool
1930
   *
1931
   * @throws \InvalidArgumentException
1932
   */
1933 11
  public function showConfigError(): bool
1934
  {
1935
1936
    if (
1937 11
        !$this->hostname
1938
        ||
1939 10
        !$this->username
1940
        ||
1941 11
        !$this->database
1942
    ) {
1943
1944 3
      if (!$this->hostname) {
1945 1
        throw new \InvalidArgumentException('no-sql-hostname');
1946
      }
1947
1948 2
      if (!$this->username) {
1949 1
        throw new \InvalidArgumentException('no-sql-username');
1950
      }
1951
1952 1
      if (!$this->database) {
1953 1
        throw new \InvalidArgumentException('no-sql-database');
1954
      }
1955
1956
      return false;
1957
    }
1958
1959 8
    return true;
1960
  }
1961
1962
  /**
1963
   * alias: "beginTransaction()"
1964
   */
1965 1
  public function startTransaction(): bool
1966
  {
1967 1
    return $this->beginTransaction();
1968
  }
1969
1970
  /**
1971
   * Execute a callback inside a transaction.
1972
   *
1973
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1974
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1975
   *
1976
   * @return bool <p>Boolean true on success, false otherwise.</p>
1977
   */
1978 1
  public function transact($callback): bool
1979
  {
1980
    try {
1981
1982 1
      $beginTransaction = $this->beginTransaction();
1983 1
      if ($beginTransaction === false) {
1984 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1985
1986 1
        return false;
1987
      }
1988
1989 1
      $result = $callback($this);
1990 1
      if ($result === false) {
1991
        /** @noinspection ThrowRawExceptionInspection */
1992 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1993
      }
1994
1995 1
      return $this->commit();
1996
1997 1
    } catch (\Exception $e) {
1998
1999 1
      $this->rollback();
2000
2001 1
      return false;
2002
    }
2003
  }
2004
2005
  /**
2006
   * Execute a "update"-query.
2007
   *
2008
   * @param string       $table
2009
   * @param array        $data
2010
   * @param array|string $where
2011
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2012
   *
2013
   * @return false|int <p>false on error</p>
2014
   *
2015
   * @throws QueryException
2016
   */
2017 7
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2018
  {
2019
    // init
2020 7
    $table = \trim($table);
2021
2022 7
    if ($table === '') {
2023 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2024
2025 1
      return false;
2026
    }
2027
2028 7
    if (\count($data) === 0) {
2029 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2030
2031 2
      return false;
2032
    }
2033
2034 7
    $SET = $this->_parseArrayPair($data);
2035
2036 7
    if (\is_string($where)) {
2037 2
      $WHERE = $this->escape($where, false);
2038 6
    } elseif (\is_array($where)) {
2039 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2040
    } else {
2041 1
      $WHERE = '';
2042
    }
2043
2044 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...
2045
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2046
    }
2047
2048 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2049
2050 7
    return $this->query($sql);
2051
  }
2052
2053
  /**
2054
   * Determine if database table exists
2055
   *
2056
   * @param string $table
2057
   *
2058
   * @return bool
2059
   */
2060 1
  public function table_exists(string $table): bool
2061
  {
2062 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2063
2064 1
    return $check !== false
2065
           &&
2066 1
           $check instanceof Result
2067
           &&
2068 1
           $check->num_rows > 0;
2069
  }
2070
2071
  /**
2072
   * Count number of rows found matching a specific query.
2073
   *
2074
   * @param string $query
2075
   *
2076
   * @return int
2077
   */
2078 1
  public function num_rows(string $query): int
2079
  {
2080 1
    $check = $this->query($query);
2081
2082
    if (
2083 1
        $check === false
2084
        ||
2085 1
        !$check instanceof Result
2086
    ) {
2087
      return 0;
2088
    }
2089
2090 1
    return $check->num_rows;
2091
  }
2092
}
2093