Completed
Push — master ( 58fd6d...05af0e )
by Lars
01:50
created

DB::setConfigExtra()   B

Complexity

Conditions 9
Paths 130

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 19.125

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 9
cts 18
cp 0.5
rs 7.8055
c 0
b 0
f 0
cc 9
nc 130
nop 1
crap 19.125
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 $mysqli_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
   * @var null|\Doctrine\DBAL\Connection
124
   */
125
  private $doctrine_connection;
126
127
  /**
128
   * @var null|int
129
   */
130
  private $affected_rows;
131
132
  /**
133
   * __construct()
134
   *
135
   * @param string $hostname
136
   * @param string $username
137
   * @param string $password
138
   * @param string $database
139
   * @param int    $port
140
   * @param string $charset
141
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
142
   *                                      Use false to disable it.</p>
143
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
144
   *                                      Use false to disable it.</p>
145
   * @param string $logger_class_name
146
   * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
147
   * @param array  $extra_config          <p>
148
   *                                      'session_to_db' => bool<br>
149
   *                                      'socket'        => 'string (path)'<br>
150
   *                                      'ssl'           => bool<br>
151
   *                                      'clientkey'     => 'string (path)'<br>
152
   *                                      'clientcert'    => 'string (path)'<br>
153
   *                                      'cacert'        => 'string (path)'<br>
154
   *                                      </p>
155
   */
156 23
  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 = [])
157
  {
158 23
    $this->debug = new Debug($this);
159
160 23
    $this->_loadConfig(
161 23
        $hostname,
162 23
        $username,
163 23
        $password,
164 23
        $database,
165 23
        $port,
166 23
        $charset,
167 23
        $exit_on_error,
168 23
        $echo_on_error,
169 23
        $logger_class_name,
170 23
        $logger_level,
171 23
        $extra_config
172
    );
173
174 14
    $this->connect();
175
176 5
    $this->mysqlDefaultTimeFunctions = [
177
      // Returns the current date.
178
      'CURDATE()',
179
      // CURRENT_DATE	| Synonyms for CURDATE()
180
      'CURRENT_DATE()',
181
      // CURRENT_TIME	| Synonyms for CURTIME()
182
      'CURRENT_TIME()',
183
      // CURRENT_TIMESTAMP | Synonyms for NOW()
184
      'CURRENT_TIMESTAMP()',
185
      // Returns the current time.
186
      'CURTIME()',
187
      // Synonym for NOW()
188
      'LOCALTIME()',
189
      // Synonym for NOW()
190
      'LOCALTIMESTAMP()',
191
      // Returns the current date and time.
192
      'NOW()',
193
      // Returns the time at which the function executes.
194
      'SYSDATE()',
195
      // Returns a UNIX timestamp.
196
      'UNIX_TIMESTAMP()',
197
      // Returns the current UTC date.
198
      'UTC_DATE()',
199
      // Returns the current UTC time.
200
      'UTC_TIME()',
201
      // Returns the current UTC date and time.
202
      'UTC_TIMESTAMP()',
203
    ];
204 5
  }
205
206
  /**
207
   * Prevent the instance from being cloned.
208
   *
209
   * @return void
210
   */
211
  private function __clone()
212
  {
213
  }
214
215
  /**
216
   * __destruct
217
   */
218
  public function __destruct()
219
  {
220
    // close the connection only if we don't save PHP-SESSION's in DB
221
    if ($this->session_to_db === false) {
222
      $this->close();
223
    }
224
  }
225
226
  /**
227
   * @param null|string $sql
228
   * @param array       $bindings
229
   *
230
   * @return bool|int|Result|DB           <p>
231
   *                                      "DB" by "$sql" === null<br />
232
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
233
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
234
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
235
   *                                      "true" by e.g. "DROP"-queries<br />
236
   *                                      "false" on error
237
   *                                      </p>
238
   */
239 4
  public function __invoke(string $sql = null, array $bindings = [])
240
  {
241 4
    return null !== $sql ? $this->query($sql, $bindings) : $this;
242
  }
243
244
  /**
245
   * __wakeup
246
   *
247
   * @return void
248
   */
249 4
  public function __wakeup()
250
  {
251 4
    $this->reconnect();
252 4
  }
253
254
  /**
255
   * Load the config from the constructor.
256
   *
257
   * @param string $hostname
258
   * @param string $username
259
   * @param string $password
260
   * @param string $database
261
   * @param int    $port                  <p>default is (int)3306</p>
262
   * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
263
   * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
264
   *                                      Use false to disable it.</p>
265
   * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
266
   *                                      Use false to disable it.</p>
267
   * @param string $logger_class_name
268
   * @param string $logger_level
269
   * @param array  $extra_config          <p>
270
   *                                      'session_to_db' => false|true<br>
271
   *                                      'socket' => 'string (path)'<br>
272
   *                                      'ssl' => 'bool'<br>
273
   *                                      'clientkey' => 'string (path)'<br>
274
   *                                      'clientcert' => 'string (path)'<br>
275
   *                                      'cacert' => 'string (path)'<br>
276
   *                                      </p>
277
   *
278
   * @return bool
279
   */
280 23
  private function _loadConfig(
281
      string $hostname,
282
      string $username,
283
      string $password,
284
      string $database,
285
      $port,
286
      string $charset,
287
      bool $exit_on_error,
288
      bool $echo_on_error,
289
      string $logger_class_name,
290
      string $logger_level,
291
      array $extra_config = []
292
  ): bool
293
  {
294 23
    $this->hostname = $hostname;
295 23
    $this->username = $username;
296 23
    $this->password = $password;
297 23
    $this->database = $database;
298
299 23
    if ($charset) {
300 23
      $this->charset = $charset;
301
    }
302
303 23
    if ($port) {
304 11
      $this->port = (int)$port;
305
    } else {
306
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
307 13
      $this->port = (int)@ini_get('mysqli.default_port');
308
    }
309
310
    // fallback
311 23
    if (!$this->port) {
312
      $this->port = 3306;
313
    }
314
315 23
    if (!$this->socket) {
316
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
317 23
      $this->socket = @ini_get('mysqli.default_socket');
318
    }
319
320 23
    $this->debug->setExitOnError($exit_on_error);
321 23
    $this->debug->setEchoOnError($echo_on_error);
322
323 23
    $this->debug->setLoggerClassName($logger_class_name);
324 23
    $this->debug->setLoggerLevel($logger_level);
325
326 23
    $this->setConfigExtra($extra_config);
327
328 23
    return $this->showConfigError();
329
  }
330
331
  /**
332
   * Parses arrays with value pairs and generates SQL to use in queries.
333
   *
334
   * @param array  $arrayPair
335
   * @param string $glue <p>This is the separator.</p>
336
   *
337
   * @return string
338
   *
339
   * @internal
340
   */
341 72
  public function _parseArrayPair(array $arrayPair, string $glue = ','): string
342
  {
343
    // init
344 72
    $sql = '';
345
346 72
    if (\count($arrayPair) === 0) {
347
      return '';
348
    }
349
350 72
    $arrayPairCounter = 0;
351 72
    foreach ($arrayPair as $_key => $_value) {
352 72
      $_connector = '=';
353 72
      $_glueHelper = '';
354 72
      $_key_upper = \strtoupper($_key);
355
356 72
      if (\strpos($_key_upper, ' NOT') !== false) {
357 6
        $_connector = 'NOT';
358
      }
359
360 72
      if (\strpos($_key_upper, ' IS') !== false) {
361 3
        $_connector = 'IS';
362
      }
363
364 72
      if (\strpos($_key_upper, ' IS NOT') !== false) {
365 3
        $_connector = 'IS NOT';
366
      }
367
368 72
      if (\strpos($_key_upper, ' IN') !== false) {
369 3
        $_connector = 'IN';
370
      }
371
372 72
      if (\strpos($_key_upper, ' NOT IN') !== false) {
373 3
        $_connector = 'NOT IN';
374
      }
375
376 72
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
377 3
        $_connector = 'BETWEEN';
378
      }
379
380 72
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
381 3
        $_connector = 'NOT BETWEEN';
382
      }
383
384 72
      if (\strpos($_key_upper, ' LIKE') !== false) {
385 6
        $_connector = 'LIKE';
386
      }
387
388 72
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
389 6
        $_connector = 'NOT LIKE';
390
      }
391
392 72 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
393 8
        $_connector = '>';
394
      }
395
396 72 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
397 3
        $_connector = '<';
398
      }
399
400 72
      if (\strpos($_key_upper, ' >=') !== false) {
401 8
        $_connector = '>=';
402
      }
403
404 72
      if (\strpos($_key_upper, ' <=') !== false) {
405 3
        $_connector = '<=';
406
      }
407
408 72
      if (\strpos($_key_upper, ' <>') !== false) {
409 3
        $_connector = '<>';
410
      }
411
412 72
      if (\strpos($_key_upper, ' OR') !== false) {
413 6
        $_glueHelper = 'OR';
414
      }
415
416 72
      if (\strpos($_key_upper, ' AND') !== false) {
417 3
        $_glueHelper = 'AND';
418
      }
419
420 72
      if (\is_array($_value) === true) {
421 7
        $firstKey = null;
422 7
        $firstValue = null;
423 7
        foreach ($_value as $oldKey => $oldValue) {
424 7
          $_value[$oldKey] = $this->secure($oldValue);
425
426 7
          if ($firstKey === null) {
427 7
            $firstKey = $oldKey;
428
          }
429
430 7
          if ($firstValue === null) {
431 7
            $firstValue = $_value[$oldKey];
432
          }
433
        }
434
435 7
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
436 3
          $_value = '(' . \implode(',', $_value) . ')';
437 7
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
438 3
          $_value = '(' . \implode(' AND ', $_value) . ')';
439 7
        } elseif ($firstKey && $firstValue) {
440
441 1 View Code Duplication
          if (\strpos($firstKey, ' +') !== false) {
442 1
            $firstKey = \str_replace(' +', '', $firstKey);
443 1
            $_value = $firstKey . ' + ' . $firstValue;
444
          }
445
446 1 View Code Duplication
          if (\strpos($firstKey, ' -') !== false) {
447 1
            $firstKey = \str_replace(' -', '', $firstKey);
448 7
            $_value = $firstKey . ' - ' . $firstValue;
449
          }
450
        }
451
452
      } else {
453 72
        $_value = $this->secure($_value);
454
      }
455
456 72
      $quoteString = $this->quote_string(
457 72
          \trim(
458 72
              \str_ireplace(
459
                  [
460 72
                      $_connector,
461 72
                      $_glueHelper,
462
                  ],
463 72
                  '',
464 72
                  $_key
465
              )
466
          )
467
      );
468
469 72
      $_value = (array)$_value;
470
471 72
      if (!$_glueHelper) {
472 72
        $_glueHelper = $glue;
473
      }
474
475 72
      $tmpCounter = 0;
476 72
      foreach ($_value as $valueInner) {
477
478 72
        $_glueHelperInner = $_glueHelper;
479
480 72
        if ($arrayPairCounter === 0) {
481
482 72
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
483 3
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
484 72
          } elseif ($tmpCounter === 0) {
485 72
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
486
          }
487
488 68
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
489 3
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
490
        }
491
492 72
        if (\is_string($valueInner) && $valueInner === '') {
493
          $valueInner = "''";
494
        }
495
496 72
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
497 72
        $tmpCounter++;
498
      }
499
500 72
      if ($_glueHelper === 'OR') {
501 6
        $sql .= ' ) ';
502
      }
503
504 72
      $arrayPairCounter++;
505
    }
506
507 72
    return $sql;
508
  }
509
510
  /**
511
   * _parseQueryParams
512
   *
513
   * @param string $sql
514
   * @param array  $params
515
   *
516
   * @return array <p>with the keys -> 'sql', 'params'</p>
517
   */
518 17
  private function _parseQueryParams(string $sql, array $params = []): array
519
  {
520 17
    $offset = \strpos($sql, '?');
521
522
    // is there anything to parse?
523
    if (
524 17
        $offset === false
525
        ||
526 17
        \count($params) === 0
527
    ) {
528 13
      return ['sql' => $sql, 'params' => $params];
529
    }
530
531 7
    foreach ($params as $key => $param) {
532
533
      // use this only for not named parameters
534 7
      if (!\is_int($key)) {
535 3
        continue;
536
      }
537
538 7
      if ($offset === false) {
539
        continue;
540
      }
541
542 7
      $replacement = $this->secure($param);
543
544 7
      unset($params[$key]);
545
546 7
      $sql = \substr_replace($sql, $replacement, $offset, 1);
547 7
      $offset = \strpos($sql, '?', $offset + \strlen((string)$replacement));
548
    }
549
550 7
    return ['sql' => $sql, 'params' => $params];
551
  }
552
553
  /**
554
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
555
   *
556
   * @param mixed $sql    <p>The SQL string.</p>
557
   * @param array $params <p>An array of key-value bindings.</p>
558
   *
559
   * @return array <p>with the keys -> 'sql', 'params'</p>
560
   */
561 20
  private function _parseQueryParamsByName(string $sql, array $params = []): array
562
  {
563
    // is there anything to parse?
564
    if (
565 20
        \strpos($sql, ':') === false
566
        ||
567 20
        \count($params) === 0
568
    ) {
569 7
      return ['sql' => $sql, 'params' => $params];
570
    }
571
572 16
    $offset = null;
573 16
    $replacement = null;
574 16
    foreach ($params as $name => $param) {
575
576
      // use this only for named parameters
577 16
      if (\is_int($name)) {
578
        continue;
579
      }
580
581
      // add ":" if needed
582 16
      if (\strpos($name, ':') !== 0) {
583 6
        $nameTmp = ':' . $name;
584
      } else {
585 10
        $nameTmp = $name;
586
      }
587
588 16
      if ($offset === null) {
589 16
        $offset = \strpos($sql, $nameTmp);
590
      } else {
591 15
        $offset = \strpos($sql, $nameTmp, $offset + \strlen((string)$replacement));
592
      }
593
594 16
      if ($offset === false) {
595 3
        continue;
596
      }
597
598 16
      $replacement = $this->secure($param);
599
600 16
      unset($params[$name]);
601
602 16
      $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
603
    }
604
605 16
    return ['sql' => $sql, 'params' => $params];
606
  }
607
608
  /**
609
   * Gets the number of affected rows in a previous MySQL operation.
610
   *
611
   * @return int
612
   */
613 28
  public function affected_rows(): int
614
  {
615
    if (
616 28
        $this->mysqli_link
617
        &&
618 28
        $this->mysqli_link instanceof \mysqli
619
    ) {
620 28
      return \mysqli_affected_rows($this->mysqli_link);
621
    }
622
623
    return (int)$this->affected_rows;
624
  }
625
626
  /**
627
   * Begins a transaction, by turning off auto commit.
628
   *
629
   * @return bool <p>This will return true or false indicating success of transaction</p>
630
   */
631 18
  public function beginTransaction(): bool
632
  {
633 18
    if ($this->in_transaction === true) {
634 6
      $this->debug->displayError('Error: mysql server already in transaction!', false);
635
636 6
      return false;
637
    }
638
639 18
    $this->clearErrors(); // needed for "$this->endTransaction()"
640 18
    $this->in_transaction = true;
641
642 18
    if ($this->mysqli_link) {
643 18
      $return = \mysqli_autocommit($this->mysqli_link, false);
644
    } elseif ($this->isDoctrinePDOConnection() === true) {
645
      $this->doctrine_connection->setAutoCommit(false);
646
      $this->doctrine_connection->beginTransaction();
647
648
      if ($this->doctrine_connection->isTransactionActive() === true) {
649
        $return = true;
650
      } else {
651
        $return = false;
652
      }
653
    }
654
655 18
    if ($return === false) {
0 ignored issues
show
Bug introduced by
The variable $return 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...
656
      $this->in_transaction = false;
657
    }
658
659 18
    return $return;
660
  }
661
662
  /**
663
   * Clear the errors in "_debug->_errors".
664
   *
665
   * @return bool
666
   */
667 18
  public function clearErrors(): bool
668
  {
669 18
    return $this->debug->clearErrors();
670
  }
671
672
  /**
673
   * Closes a previously opened database connection.
674
   *
675
   * @return bool
676
   *              Will return "true", if the connection was closed,
677
   *              otherwise (e.g. if the connection was already closed) "false".
678
   */
679 6
  public function close(): bool
680
  {
681 6
    $this->connected = false;
682
683
    if (
684 6
        $this->doctrine_connection
685
        &&
686 6
        $this->doctrine_connection instanceof \Doctrine\DBAL\Connection
687
    ) {
688
689
      $connectedBefore = $this->doctrine_connection->isConnected();
690
691
      $this->doctrine_connection->close();
692
693
      $this->mysqli_link = null;
694
695
      if ($connectedBefore === true) {
696
        return !$this->doctrine_connection->isConnected();
697
      }
698
699
      return false;
700
    }
701
702
    if (
703 6
        $this->mysqli_link
704
        &&
705 6
        $this->mysqli_link instanceof \mysqli
706
    ) {
707 6
      $result = \mysqli_close($this->mysqli_link);
708 6
      $this->mysqli_link = null;
709
710 6
      return $result;
711
    }
712
713 3
    $this->mysqli_link = null;
714
715 3
    return false;
716
  }
717
718
  /**
719
   * Commits the current transaction and end the transaction.
720
   *
721
   * @return bool <p>bool true on success, false otherwise.</p>
722
   */
723 9 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...
724
  {
725 9
    if ($this->in_transaction === false) {
726
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
727
728
      return false;
729
    }
730
731 9
    if ($this->mysqli_link) {
732 9
      $return = \mysqli_commit($this->mysqli_link);
733 9
      \mysqli_autocommit($this->mysqli_link, true);
734
    } elseif ($this->isDoctrinePDOConnection() === true) {
735
      $this->doctrine_connection->commit();
736
      $this->doctrine_connection->setAutoCommit(true);
737
738
      if ($this->doctrine_connection->isAutoCommit() === true) {
739
        $return = true;
740
      } else {
741
        $return = false;
742
      }
743
    }
744
745 9
    $this->in_transaction = false;
746
747 9
    return $return;
0 ignored issues
show
Bug introduced by
The variable $return 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...
748
  }
749
750
  /**
751
   * Open a new connection to the MySQL server.
752
   *
753
   * @return bool
754
   *
755
   * @throws DBConnectException
756
   */
757 20
  public function connect(): bool
758
  {
759 20
    if ($this->isReady()) {
760 3
      return true;
761
    }
762
763 20
    if ($this->doctrine_connection) {
764
      $this->doctrine_connection->connect();
765
766
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
767
768 View Code Duplication
      if ($this->isDoctrineMySQLiConnection() === true) {
769
        /* @var $doctrineWrappedConnection \Doctrine\DBAL\Driver\Mysqli\MysqliConnection */
770
771
        $this->mysqli_link = $doctrineWrappedConnection->getWrappedResourceHandle();
772
773
        $this->connected = $this->doctrine_connection->isConnected();
774
775
        if (!$this->connected) {
776
          $error = 'Error connecting to mysql server: ' . $this->doctrine_connection->errorInfo();
777
          $this->debug->displayError($error, false);
778
          throw new DBConnectException($error, 101);
779
        }
780
781
        $this->set_charset($this->charset);
782
783
        return $this->isReady();
784
      }
785
786 View Code Duplication
      if ($this->isDoctrinePDOConnection() === true) {
787
788
        $this->mysqli_link = null;
789
790
        $this->connected = $this->doctrine_connection->isConnected();
791
792
        if (!$this->connected) {
793
          $error = 'Error connecting to mysql server: ' . $this->doctrine_connection->errorInfo();
794
          $this->debug->displayError($error, false);
795
          throw new DBConnectException($error, 101);
796
        }
797
798
        $this->set_charset($this->charset);
799
800
        return $this->isReady();
801
      }
802
    }
803
804 20
    $flags = null;
805
806 20
    \mysqli_report(MYSQLI_REPORT_STRICT);
807
    try {
808 20
      $this->mysqli_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 $mysqli_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...
809
810 20
      if (Helper::isMysqlndIsUsed() === true) {
811 20
        \mysqli_options($this->mysqli_link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
812
      }
813
814 20
      if ($this->ssl === true) {
815
816
        if (empty($this->clientcert)) {
817
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
818
        }
819
820
        if (empty($this->clientkey)) {
821
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
822
        }
823
824
        if (empty($this->cacert)) {
825
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
826
        }
827
828
        \mysqli_options($this->mysqli_link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
829
830
        /** @noinspection PhpParamsInspection */
831
        \mysqli_ssl_set(
832
            $this->mysqli_link,
833
            $this->clientkey,
834
            $this->clientcert,
835
            $this->cacert,
836
            null,
837
            null
838
        );
839
840
        $flags = MYSQLI_CLIENT_SSL;
841
      }
842
843
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
844 20
      $this->connected = @\mysqli_real_connect(
845 20
          $this->mysqli_link,
846 20
          $this->hostname,
847 20
          $this->username,
848 20
          $this->password,
849 20
          $this->database,
850 20
          $this->port,
851 20
          $this->socket,
852 20
          (int)$flags
853
      );
854
855 9
    } catch (\Exception $e) {
856 9
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
857 9
      $this->debug->displayError($error, false);
858 9
      throw new DBConnectException($error, 100, $e);
859
    }
860 11
    \mysqli_report(MYSQLI_REPORT_OFF);
861
862 11
    $errno = \mysqli_connect_errno();
863 11
    if (!$this->connected || $errno) {
864
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
865
      $this->debug->displayError($error, false);
866
      throw new DBConnectException($error, 101);
867
    }
868
869 11
    $this->set_charset($this->charset);
870
871 11
    return $this->isReady();
872
  }
873
874
  /**
875
   * Execute a "delete"-query.
876
   *
877
   * @param string       $table
878
   * @param string|array $where
879
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
880
   *
881
   * @return false|int <p>false on error</p>
882
   *
883
   * @throws QueryException
884
   */
885 4 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...
886
  {
887
    // init
888 4
    $table = \trim($table);
889
890 4
    if ($table === '') {
891 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
892
893 3
      return false;
894
    }
895
896 4
    if (\is_string($where)) {
897 3
      $WHERE = $this->escape($where, false);
898 4
    } elseif (\is_array($where)) {
899 4
      $WHERE = $this->_parseArrayPair($where, 'AND');
900
    } else {
901 3
      $WHERE = '';
902
    }
903
904 4
    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...
905
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
906
    }
907
908 4
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
909
910 4
    return $this->query($sql);
911
  }
912
913
  /**
914
   * Ends a transaction and commits if no errors, then ends autocommit.
915
   *
916
   * @return bool <p>This will return true or false indicating success of transactions.</p>
917
   */
918 12
  public function endTransaction(): bool
919
  {
920 12
    if ($this->in_transaction === false) {
921
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
922
923
      return false;
924
    }
925
926 12
    if (!$this->errors()) {
927 3
      $return = $this->commit();
928
    } else {
929 9
      $this->rollback();
930 9
      $return = false;
931
    }
932
933 12
    if ($this->mysqli_link) {
934 12
      \mysqli_autocommit($this->mysqli_link, true);
935
    } elseif ($this->isDoctrinePDOConnection() === true) {
936
      $this->doctrine_connection->setAutoCommit(true);
937
938
      if ($this->doctrine_connection->isAutoCommit() === true) {
939
        $return = true;
940
      } else {
941
        $return = false;
942
      }
943
    }
944
945 12
    $this->in_transaction = false;
946
947 12
    return $return;
948
  }
949
950
  /**
951
   * Get all errors from "$this->errors".
952
   *
953
   * @return array|false <p>false === on errors</p>
954
   */
955 12
  public function errors()
956
  {
957 12
    $errors = $this->debug->getErrors();
958
959 12
    return \count($errors) > 0 ? $errors : false;
960
  }
961
962
  /**
963
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
964
   *
965
   * @param mixed     $var           bool: convert into "integer"<br />
966
   *                                 int: int (don't change it)<br />
967
   *                                 float: float (don't change it)<br />
968
   *                                 null: null (don't change it)<br />
969
   *                                 array: run escape() for every key => value<br />
970
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
971
   * @param bool      $stripe_non_utf8
972
   * @param bool      $html_entity_decode
973
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
974
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
975
   *                                 <strong>null</strong> => Convert the array into null, every time.
976
   *
977
   * @return mixed
978
   */
979 119
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
980
  {
981
    // [empty]
982 119
    if ($var === '') {
983 6
      return '';
984
    }
985
986
    // ''
987 119
    if ($var === "''") {
988
      return "''";
989
    }
990
991
    // check the type
992 119
    $type = \gettype($var);
993
994 119
    if ($type === 'object') {
995 9
      if ($var instanceof \DateTime) {
996 9
        $var = $var->format('Y-m-d H:i:s');
997 9
        $type = 'string';
998 6
      } elseif (\method_exists($var, '__toString')) {
999 6
        $var = (string)$var;
1000 6
        $type = 'string';
1001
      }
1002
    }
1003
1004
    switch ($type) {
1005 119
      case 'boolean':
1006 9
        $var = (int)$var;
1007 9
        break;
1008
1009 119
      case 'double':
1010 119
      case 'integer':
1011 70
        break;
1012
1013 113
      case 'string':
1014 113
        if ($stripe_non_utf8 === true) {
1015 23
          $var = UTF8::cleanup($var);
1016
        }
1017
1018 113
        if ($html_entity_decode === true) {
1019 3
          $var = UTF8::html_entity_decode($var);
1020
        }
1021
1022 113
        $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
1023
1024
        if (
1025 113
            $this->mysqli_link
1026
            &&
1027 113
            $this->mysqli_link instanceof \mysqli
1028
        ) {
1029 113
          $var = \mysqli_real_escape_string($this->mysqli_link, $var);
1030
        } elseif ($this->isDoctrinePDOConnection() === true) {
1031
          $var = $this->getDoctrinePDOConnection()->quote($var);
1032
          $var = \substr($var, 1, -1);
1033
        }
1034
1035 113
        break;
1036
1037 9
      case 'array':
1038 6
        if ($convert_array === null) {
1039
1040 3
          if ($this->convert_null_to_empty_string === true) {
1041
            $var = "''";
1042
          } else {
1043 3
            $var = 'NULL';
1044
          }
1045
1046
        } else {
1047
1048 6
          $varCleaned = [];
1049 6
          foreach ((array)$var as $key => $value) {
1050
1051 6
            $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
1052 6
            $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
1053
1054
            /** @noinspection OffsetOperationsInspection */
1055 6
            $varCleaned[$key] = $value;
1056
          }
1057
1058 6 View Code Duplication
          if ($convert_array === true) {
1059 3
            $varCleaned = \implode(',', $varCleaned);
1060
1061 3
            $var = $varCleaned;
1062
          } else {
1063 6
            $var = $varCleaned;
1064
          }
1065
1066
        }
1067 6
        break;
1068
1069 9
      case 'NULL':
1070 6
        if ($this->convert_null_to_empty_string === true) {
1071
          $var = "''";
1072
        } else {
1073 6
          $var = 'NULL';
1074
        }
1075 6
        break;
1076
1077
      default:
1078 6
        throw new \InvalidArgumentException(sprintf('Not supported value "%s" of type %s.', print_r($var, true), $type));
1079
        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...
1080
    }
1081
1082 119
    return $var;
1083
  }
1084
1085
  /**
1086
   * Execute select/insert/update/delete sql-queries.
1087
   *
1088
   * @param string  $query    <p>sql-query</p>
1089
   * @param bool    $useCache optional <p>use cache?</p>
1090
   * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
1091
   * @param DB|null $db       optional <p>the database connection</p>
1092
   *
1093
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
1094
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
1095
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1096
   *               "true" by e.g. "DROP"-queries<br />
1097
   *               "false" on error
1098
   *
1099
   * @throws QueryException
1100
   */
1101 9
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1102
  {
1103
    // init
1104 9
    $cacheKey = null;
1105 9
    if (!$db) {
1106 9
      $db = self::getInstance();
1107
    }
1108
1109 9 View Code Duplication
    if ($useCache === true) {
1110 3
      $cache = new Cache(null, null, false, $useCache);
1111 3
      $cacheKey = 'sql-' . \md5($query);
1112
1113
      if (
1114 3
          $cache->getCacheIsReady() === true
1115
          &&
1116 3
          $cache->existsItem($cacheKey)
1117
      ) {
1118 3
        return $cache->getItem($cacheKey);
1119
      }
1120
1121
    } else {
1122 9
      $cache = false;
1123
    }
1124
1125 9
    $result = $db->query($query);
1126
1127 9
    if ($result instanceof Result) {
1128
1129 3
      $return = $result->fetchAllArray();
1130
1131
      // save into the cache
1132 View Code Duplication
      if (
1133 3
          $cacheKey !== null
1134
          &&
1135 3
          $useCache === true
1136
          &&
1137 3
          $cache instanceof Cache
1138
          &&
1139 3
          $cache->getCacheIsReady() === true
1140
      ) {
1141 3
        $cache->setItem($cacheKey, $return, $cacheTTL);
1142
      }
1143
1144
    } else {
1145 6
      $return = $result;
1146
    }
1147
1148 9
    return $return;
1149
  }
1150
1151
  /**
1152
   * Get all table-names via "SHOW TABLES".
1153
   *
1154
   * @return array
1155
   */
1156 3
  public function getAllTables(): array
1157
  {
1158 3
    $query = 'SHOW TABLES';
1159 3
    $result = $this->query($query);
1160
1161 3
    return $result->fetchAllArray();
1162
  }
1163
1164
  /**
1165
   * @return array
1166
   */
1167 8
  public function getConfig()
1168
  {
1169
    $config = [
1170 8
        'hostname'   => $this->hostname,
1171 8
        'username'   => $this->username,
1172 8
        'password'   => $this->password,
1173 8
        'port'       => $this->port,
1174 8
        'database'   => $this->database,
1175 8
        'socket'     => $this->socket,
1176 8
        'charset'    => $this->charset,
1177 8
        'cacert'     => $this->cacert,
1178 8
        'clientcert' => $this->clientcert,
1179 8
        'clientkey'  => $this->clientkey,
1180
    ];
1181
1182 8
    if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1183
      $config += $this->doctrine_connection->getParams();
1184
    }
1185
1186 8
    return $config;
1187
  }
1188
1189
  /**
1190
   * @return Debug
1191
   */
1192 9
  public function getDebugger(): Debug
1193
  {
1194 9
    return $this->debug;
1195
  }
1196
1197
  /**
1198
   * @return null|\Doctrine\DBAL\Connection|null
1199
   */
1200 2
  public function getDoctrineConnection()
1201
  {
1202 2
    return $this->doctrine_connection;
1203
  }
1204
1205
  /**
1206
   * @return false|\Doctrine\DBAL\Driver\Connection
1207
   */
1208 View Code Duplication
  private function getDoctrinePDOConnection()
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...
1209
  {
1210
    if ($this->doctrine_connection) {
1211
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1212
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1213
        return $doctrineWrappedConnection;
1214
      }
1215
    }
1216
1217
    return false;
1218
  }
1219
1220
  /**
1221
   * Get errors from "$this->errors".
1222
   *
1223
   * @return array
1224
   */
1225 3
  public function getErrors(): array
1226
  {
1227 3
    return $this->debug->getErrors();
1228
  }
1229
1230
  /**
1231
   * @param string $hostname             <p>Hostname of the mysql server</p>
1232
   * @param string $username             <p>Username for the mysql connection</p>
1233
   * @param string $password             <p>Password for the mysql connection</p>
1234
   * @param string $database             <p>Database for the mysql connection</p>
1235
   * @param int    $port                 <p>default is (int)3306</p>
1236
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1237
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1238
   *                                     Use false to disable it.</p>
1239
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1240
   *                                     Use false to disable it.</p>
1241
   * @param string $logger_class_name
1242
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1243
   * @param array  $extra_config         <p>
1244
   *                                     're_connect'    => bool<br>
1245
   *                                     'session_to_db' => bool<br>
1246
   *                                     'doctrine'      => \Doctrine\DBAL\Connection<br>
1247
   *                                     'socket'        => 'string (path)'<br>
1248
   *                                     'ssl'           => bool<br>
1249
   *                                     'clientkey'     => 'string (path)'<br>
1250
   *                                     'clientcert'    => 'string (path)'<br>
1251
   *                                     'cacert'        => 'string (path)'<br>
1252
   *                                     </p>
1253
   *
1254
   * @return self
1255
   */
1256 231
  public static function getInstance(
1257
      string $hostname = '',
1258
      string $username = '',
1259
      string $password = '',
1260
      string $database = '',
1261
      $port = 3306,
1262
      string $charset = 'utf8',
1263
      bool $exit_on_error = true,
1264
      bool $echo_on_error = true,
1265
      string $logger_class_name = '',
1266
      string $logger_level = '',
1267
      array $extra_config = []
1268
  ): self
1269
  {
1270
    /**
1271
     * @var $instance self[]
1272
     */
1273 231
    static $instance = [];
1274
1275
    /**
1276
     * @var $firstInstance self
1277
     */
1278 231
    static $firstInstance = null;
1279
1280
    // fallback
1281 231
    if (!$charset) {
1282 121
      $charset = 'utf8';
1283
    }
1284
1285
    if (
1286 231
        '' . $hostname . $username . $password . $database . $port . $charset == '' . $port . $charset
1287
        &&
1288 231
        null !== $firstInstance
1289
    ) {
1290 143
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1291
        $firstInstance->reconnect(true);
1292
      }
1293
1294 143
      return $firstInstance;
1295
    }
1296
1297 150
    $extra_config_string = '';
1298 150
    foreach ($extra_config as $extra_config_key => $extra_config_value) {
1299
      if (\is_object($extra_config_value)) {
1300
        $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1301
      } else {
1302
        $extra_config_value_tmp = (string)$extra_config_value;
1303
      }
1304
      $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1305
    }
1306
1307 150
    $connection = \md5(
1308 150
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1309
    );
1310
1311 150
    if (!isset($instance[$connection])) {
1312 23
      $instance[$connection] = new self(
1313 23
          $hostname,
1314 23
          $username,
1315 23
          $password,
1316 23
          $database,
1317 23
          $port,
1318 23
          $charset,
1319 23
          $exit_on_error,
1320 23
          $echo_on_error,
1321 23
          $logger_class_name,
1322 23
          $logger_level,
1323 23
          $extra_config
1324
      );
1325
1326 5
      if (null === $firstInstance) {
1327 1
        $firstInstance = $instance[$connection];
1328
      }
1329
    }
1330
1331 138
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1332
      $instance[$connection]->reconnect(true);
1333
    }
1334
1335 138
    return $instance[$connection];
1336
  }
1337
1338
  /**
1339
   * @param \Doctrine\DBAL\Connection $doctrine
1340
   * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1341
   * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1342
   *                                                 return 'false'. Use false to disable it.</p>
1343
   * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1344
   *                                                 Use false to disable it.</p>
1345
   * @param string                    $logger_class_name
1346
   * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1347
   * @param array                     $extra_config  <p>
1348
   *                                                 're_connect'    => bool<br>
1349
   *                                                 'session_to_db' => bool<br>
1350
   *                                                 'doctrine'      => \Doctrine\DBAL\Connection<br>
1351
   *                                                 'socket'        => 'string (path)'<br>
1352
   *                                                 'ssl'           => bool<br>
1353
   *                                                 'clientkey'     => 'string (path)'<br>
1354
   *                                                 'clientcert'    => 'string (path)'<br>
1355
   *                                                 'cacert'        => 'string (path)'<br>
1356
   *                                                 </p>
1357
   *
1358
   * @return self
1359
   */
1360 55
  public static function getInstanceDoctrineHelper(
1361
      \Doctrine\DBAL\Connection $doctrine,
1362
      string $charset = 'utf8',
1363
      bool $exit_on_error = true,
1364
      bool $echo_on_error = true,
1365
      string $logger_class_name = '',
1366
      string $logger_level = '',
1367
      array $extra_config = []
1368
  ): self
1369
  {
1370 55
    $extra_config['doctrine'] = $doctrine;
1371
1372 55
    return self::getInstance(
1373 55
        '',
1374 55
        '',
1375 55
        '',
1376 55
        '',
1377 55
        3306,
1378 55
        $charset,
1379 55
        $exit_on_error,
1380 55
        $echo_on_error,
1381 55
        $logger_class_name,
1382 55
        $logger_level,
1383 55
        $extra_config
1384
    );
1385
  }
1386
1387
  /**
1388
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1389
   *
1390
   * @return null|\mysqli
1391
   */
1392 53
  public function getLink()
1393
  {
1394 53
    return $this->mysqli_link;
1395
  }
1396
1397
  /**
1398
   * Get the current charset.
1399
   *
1400
   * @return string
1401
   */
1402 3
  public function get_charset(): string
1403
  {
1404 3
    return $this->charset;
1405
  }
1406
1407
  /**
1408
   * Check if we are in a transaction.
1409
   *
1410
   * @return bool
1411
   */
1412
  public function inTransaction(): bool
1413
  {
1414
    return $this->in_transaction;
1415
  }
1416
1417
  /**
1418
   * Execute a "insert"-query.
1419
   *
1420
   * @param string      $table
1421
   * @param array       $data
1422
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1423
   *
1424
   * @return false|int <p>false on error</p>
1425
   *
1426
   * @throws QueryException
1427
   */
1428 74
  public function insert(string $table, array $data = [], string $databaseName = null)
1429
  {
1430
    // init
1431 74
    $table = \trim($table);
1432
1433 74
    if ($table === '') {
1434 6
      $this->debug->displayError('Invalid table name, table name in empty.', false);
1435
1436 6
      return false;
1437
    }
1438
1439 71
    if (\count($data) === 0) {
1440 9
      $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1441
1442 9
      return false;
1443
    }
1444
1445 65
    $SET = $this->_parseArrayPair($data);
1446
1447 65
    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...
1448
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1449
    }
1450
1451 65
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET";
1452
1453 65
    return $this->query($sql);
1454
  }
1455
1456
  /**
1457
   * Returns the auto generated id used in the last query.
1458
   *
1459
   * @return int|string
1460
   */
1461 104
  public function insert_id()
1462
  {
1463 104
    if ($this->mysqli_link) {
1464 104
      return \mysqli_insert_id($this->mysqli_link);
1465
    }
1466
1467
    if ($this->getDoctrinePDOConnection()) {
1468
      return $this->getDoctrinePDOConnection()->lastInsertId();
1469
    }
1470
  }
1471
1472
  /**
1473
   * @return bool
1474
   */
1475
  public function isDoctrineMySQLiConnection(): bool
1476
  {
1477
    if ($this->doctrine_connection) {
1478
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1479
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1480
        return true;
1481
      }
1482
    }
1483
1484
    return false;
1485
  }
1486
1487
  /**
1488
   * @return bool
1489
   */
1490 9 View Code Duplication
  public function isDoctrinePDOConnection(): 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...
1491
  {
1492 9
    if ($this->doctrine_connection) {
1493
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1494
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1495
        return true;
1496
      }
1497
    }
1498
1499 9
    return false;
1500
  }
1501
1502
  /**
1503
   * Check if db-connection is ready.
1504
   *
1505
   * @return bool
1506
   */
1507 184
  public function isReady(): bool
1508
  {
1509 184
    return $this->connected ? true : false;
1510
  }
1511
1512
  /**
1513
   * Get the last sql-error.
1514
   *
1515
   * @return string|false <p>false === there was no error</p>
1516
   */
1517 3
  public function lastError()
1518
  {
1519 3
    $errors = $this->debug->getErrors();
1520
1521 3
    return \count($errors) > 0 ? end($errors) : false;
1522
  }
1523
1524
  /**
1525
   * Execute a sql-multi-query.
1526
   *
1527
   * @param string $sql
1528
   *
1529
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1530
   *                        "bool" by only "<b>INSERT</b>"-queries<br />
1531
   *                        "bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1532
   *                        "bool" by only by e.g. "DROP"-queries<br />
1533
   *
1534
   * @throws QueryException
1535
   */
1536 3
  public function multi_query(string $sql)
1537
  {
1538 3
    if (!$this->isReady()) {
1539
      return false;
1540
    }
1541
1542 3 View Code Duplication
    if (!$sql || $sql === '') {
1543 3
      $this->debug->displayError('Can not execute an empty query.', false);
1544
1545 3
      return false;
1546
    }
1547
1548 3
    if ($this->isDoctrinePDOConnection() === true) {
1549
1550
      $query_start_time = microtime(true);
1551
      $queryException = null;
1552
      $query_result_doctrine = false;
1553
1554
      try {
1555
        $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1556
        $resultTmp = $query_result_doctrine->execute();
1557
        $mysqli_field_count = $query_result_doctrine->columnCount();
1558
      } catch (\Exception $e) {
1559
        $resultTmp = false;
1560
        $mysqli_field_count = null;
1561
1562
        $queryException = $e;
1563
      }
1564
1565
      $query_duration = microtime(true) - $query_start_time;
1566
1567
      $this->debug->logQuery($sql, $query_duration, 0);
1568
1569
      $returnTheResult = false;
1570
      $result = [];
1571
1572
      if ($resultTmp) {
1573
1574
        if ($mysqli_field_count) {
1575
1576
          if (
1577
              $query_result_doctrine
1578
              &&
1579
              $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1580
          ) {
1581
            $result = $query_result_doctrine;
1582
          }
1583
1584
        } else {
1585
          $result = $resultTmp;
1586
        }
1587
1588
        if (
1589
            $result instanceof \Doctrine\DBAL\Statement
1590
            &&
1591
            $result->columnCount() > 0
1592
        ) {
1593
          $returnTheResult = true;
1594
1595
          // return query result object
1596
          $result = [new Result($sql, $result)];
1597
        } else {
1598
          $result = [$result];
1599
        }
1600
1601
      } else {
1602
1603
        // log the error query
1604
        $this->debug->logQuery($sql, $query_duration, 0, true);
1605
1606
        if (
1607
            isset($queryException)
1608
            &&
1609
            $queryException instanceof \Doctrine\DBAL\Query\QueryException
1610
        ) {
1611
          return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1612
        }
1613
      }
1614
1615
    } else {
1616
1617 3
      $query_start_time = microtime(true);
1618 3
      $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1619 3
      $query_duration = microtime(true) - $query_start_time;
1620
1621 3
      $this->debug->logQuery($sql, $query_duration, 0);
1622
1623 3
      $returnTheResult = false;
1624 3
      $result = [];
1625
1626 3
      if ($resultTmp) {
1627
        do {
1628
1629 3
          $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1630
1631 3
          if ($resultTmpInner instanceof \mysqli_result) {
1632
1633 3
            $returnTheResult = true;
1634 3
            $result[] = new Result($sql, $resultTmpInner);
1635
1636
          } elseif (
1637 3
              $resultTmpInner === true
1638
              ||
1639 3
              !\mysqli_errno($this->mysqli_link)
1640
          ) {
1641
1642 3
            $result[] = true;
1643
1644
          } else {
1645
1646
            $result[] = false;
1647
1648
          }
1649
1650 3
        } while (\mysqli_more_results($this->mysqli_link) === true ? \mysqli_next_result($this->mysqli_link) : false);
1651
1652
      } else {
1653
1654
        // log the error query
1655 3
        $this->debug->logQuery($sql, $query_duration, 0, true);
1656
1657 3
        return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1658
      }
1659
1660
    }
1661
1662
    // return the result only if there was a "SELECT"-query
1663 3
    if ($returnTheResult === true) {
1664 3
      return $result;
1665
    }
1666
1667
    if (
1668 3
        \count($result) > 0
1669
        &&
1670 3
        \in_array(false, $result, true) === false
1671
    ) {
1672 3
      return true;
1673
    }
1674
1675
    return false;
1676
  }
1677
1678
  /**
1679
   * Count number of rows found matching a specific query.
1680
   *
1681
   * @param string $query
1682
   *
1683
   * @return int
1684
   */
1685 3
  public function num_rows(string $query): int
1686
  {
1687 3
    $check = $this->query($query);
1688
1689
    if (
1690 3
        $check === false
1691
        ||
1692 3
        !$check instanceof Result
1693
    ) {
1694
      return 0;
1695
    }
1696
1697 3
    return $check->num_rows;
1698
  }
1699
1700
  /**
1701
   * Pings a server connection, or tries to reconnect
1702
   * if the connection has gone down.
1703
   *
1704
   * @return bool
1705
   */
1706 9
  public function ping(): bool
1707
  {
1708 9
    if ($this->connected === false) {
1709 3
      return false;
1710
    }
1711
1712 6
    if ($this->isDoctrinePDOConnection() === true) {
1713
      return $this->doctrine_connection->ping();
1714
    }
1715
1716
    if (
1717 6
        $this->mysqli_link
1718
        &&
1719 6
        $this->mysqli_link instanceof \mysqli
1720
    ) {
1721 6
      return \mysqli_ping($this->mysqli_link);
1722
    }
1723
1724
    return false;
1725
  }
1726
1727
  /**
1728
   * Get a new "Prepare"-Object for your sql-query.
1729
   *
1730
   * @param string $query
1731
   *
1732
   * @return Prepare
1733
   */
1734 2
  public function prepare(string $query): Prepare
1735
  {
1736 2
    return new Prepare($this, $query);
1737
  }
1738
1739
  /**
1740
   * Execute a sql-query and return the result-array for select-statements.
1741
   *
1742
   * @param string $query
1743
   *
1744
   * @return mixed
1745
   * @deprecated
1746
   * @throws \Exception
1747
   */
1748 3
  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...
1749
  {
1750 3
    $db = self::getInstance();
1751
1752 3
    $args = \func_get_args();
1753
    /** @noinspection SuspiciousAssignmentsInspection */
1754 3
    $query = \array_shift($args);
1755 3
    $query = \str_replace('?', '%s', $query);
1756 3
    $args = \array_map(
1757
        [
1758 3
            $db,
1759 3
            'escape',
1760
        ],
1761 3
        $args
1762
    );
1763 3
    \array_unshift($args, $query);
1764 3
    $query = \sprintf(...$args);
1765 3
    $result = $db->query($query);
1766
1767 3
    if ($result instanceof Result) {
1768 3
      return $result->fetchAllArray();
1769
    }
1770
1771 3
    return $result;
1772
  }
1773
1774
  /**
1775
   * Execute a sql-query.
1776
   *
1777
   * example:
1778
   * <code>
1779
   * $sql = "INSERT INTO TABLE_NAME_HERE
1780
   *   SET
1781
   *     foo = :foo,
1782
   *     bar = :bar
1783
   * ";
1784
   * $insert_id = $db->query(
1785
   *   $sql,
1786
   *   [
1787
   *     'foo' => 1.1,
1788
   *     'bar' => 1,
1789
   *   ]
1790
   * );
1791
   * </code>
1792
   *
1793
   * @param string     $sql               <p>The sql query-string.</p>
1794
   *
1795
   * @param array|bool $params            <p>
1796
   *                                      "array" of sql-query-parameters<br/>
1797
   *                                      "false" if you don't need any parameter (default)<br/>
1798
   *                                      </p>
1799
   *
1800
   * @return bool|int|Result              <p>
1801
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1802
   *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1803
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1804
   *                                      "true" by e.g. "DROP"-queries<br />
1805
   *                                      "false" on error
1806
   *                                      </p>
1807
   *
1808
   * @throws QueryException
1809
   */
1810 164
  public function query(string $sql = '', $params = false)
1811
  {
1812 164
    if (!$this->isReady()) {
1813
      return false;
1814
    }
1815
1816 164 View Code Duplication
    if (!$sql || $sql === '') {
1817 12
      $this->debug->displayError('Can not execute an empty query.', false);
1818
1819 12
      return false;
1820
    }
1821
1822
    if (
1823 158
        $params !== false
1824
        &&
1825 158
        \is_array($params)
1826
        &&
1827 158
        \count($params) > 0
1828
    ) {
1829 17
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1830 17
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1831 17
      $sql = $parseQueryParamsByName['sql'];
1832
    }
1833
1834
    // DEBUG
1835
    // var_dump($params);
1836
    // echo $sql . "\n";
1837
1838 158
    $query_start_time = \microtime(true);
1839 158
    $queryException = null;
1840 158
    $query_result_doctrine = false;
1841
1842 158
    if ($this->doctrine_connection) {
1843
1844
      try {
1845
        $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1846
        $query_result = $query_result_doctrine->execute();
1847
        $mysqli_field_count = $query_result_doctrine->columnCount();
1848
      } catch (\Exception $e) {
1849
        $query_result = false;
1850
        $mysqli_field_count = null;
1851
1852
        $queryException = $e;
1853
      }
1854
1855
    } else {
1856
1857 158
      $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1858 158
      $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1859
1860
    }
1861
1862 158
    $query_duration = \microtime(true) - $query_start_time;
1863
1864 158
    $this->query_count++;
1865
1866 158
    if ($mysqli_field_count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mysqli_field_count of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1867
1868 116
      if ($this->doctrine_connection) {
1869
1870
        $result = false;
1871
        if (
1872
            $query_result_doctrine
1873
            &&
1874
            $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1875
        ) {
1876
          $result = $query_result_doctrine;
1877
        }
1878
1879
      } else {
1880
1881 116
        $result = \mysqli_store_result($this->mysqli_link);
1882
1883
      }
1884
    } else {
1885 114
      $result = $query_result;
1886
    }
1887
1888
    if (
1889 158
        $result instanceof \Doctrine\DBAL\Statement
1890
        &&
1891 158
        $result->columnCount() > 0
1892
    ) {
1893
1894
      // log the select query
1895
      $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1896
1897
      // return query result object
1898
      return new Result($sql, $result);
1899
    }
1900
1901 158
    if ($result instanceof \mysqli_result) {
1902
1903
      // log the select query
1904 113
      $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1905
1906
      // return query result object
1907 113
      return new Result($sql, $result);
1908
    }
1909
1910 120
    if ($query_result === true) {
1911
1912
      // "INSERT" || "REPLACE"
1913 111
      if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1914
1915 104
        $insert_id = $this->insert_id();
1916
1917 104
        $this->debug->logQuery($sql, $query_duration, $insert_id);
1918
1919 104
        return $insert_id;
1920
      }
1921
1922
      // "UPDATE" || "DELETE"
1923 54
      if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1924
1925 28
        if ($this->mysqli_link) {
1926 28
          $this->affected_rows = $this->affected_rows();
1927
        } elseif ($query_result_doctrine) {
1928
          $this->affected_rows = $query_result_doctrine->rowCount();
1929
        }
1930
1931 28
        $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1932
1933 28
        return $this->affected_rows;
1934
      }
1935
1936
      // log the ? query
1937 27
      $this->debug->logQuery($sql, $query_duration, 0);
1938
1939 27
      return true;
1940
    }
1941
1942
    // log the error query
1943 33
    $this->debug->logQuery($sql, $query_duration, 0, true);
1944
1945 33
    if ($queryException) {
1946
      return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1947
    }
1948
1949 33
    if ($this->mysqli_link) {
1950 33
      return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
1951
    }
1952
1953
    return false;
1954
  }
1955
1956
  /**
1957
   * Error-handling for the sql-query.
1958
   *
1959
   * @param string     $errorMessage
1960
   * @param int        $errorNumber
1961
   * @param string     $sql
1962
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1963
   * @param bool       $sqlMultiQuery
1964
   *
1965
   * @return mixed|false
1966
   *
1967
   * @throws QueryException
1968
   * @throws DBGoneAwayException
1969
   */
1970 39
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1971
  {
1972
    if (
1973 39
        $errorMessage === 'DB server has gone away'
1974
        ||
1975 36
        $errorMessage === 'MySQL server has gone away'
1976
        ||
1977 39
        $errorNumber === 2006
1978
    ) {
1979 3
      static $RECONNECT_COUNTER;
1980
1981
      // exit if we have more then 3 "DB server has gone away"-errors
1982 3
      if ($RECONNECT_COUNTER > 3) {
1983
        $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1984
        throw new DBGoneAwayException($errorMessage);
1985
      }
1986
1987 3
      $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1988
1989
      // reconnect
1990 3
      $RECONNECT_COUNTER++;
1991 3
      $this->reconnect(true);
1992
1993
      // re-run the current (non multi) query
1994 3
      if ($sqlMultiQuery === false) {
1995 3
        return $this->query($sql, $sqlParams);
1996
      }
1997
1998
      return false;
1999
    }
2000
2001 36
    $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2002
2003 36
    $force_exception_after_error = null; // auto
2004 36
    if ($this->in_transaction === true) {
2005 12
      $force_exception_after_error = false;
2006
    }
2007
    // this query returned an error, we must display it (only for dev) !!!
2008
2009 36
    $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2010
2011 36
    return false;
2012
  }
2013
2014
  /**
2015
   * Quote && Escape e.g. a table name string.
2016
   *
2017
   * @param mixed $str
2018
   *
2019
   * @return string
2020
   */
2021 86
  public function quote_string($str): string
2022
  {
2023 86
    $str = \str_replace(
2024 86
        '`',
2025 86
        '``',
2026 86
        \trim(
2027 86
            (string)$this->escape($str, false),
2028 86
            '`'
2029
        )
2030
    );
2031
2032 86
    return '`' . $str . '`';
2033
  }
2034
2035
  /**
2036
   * Reconnect to the MySQL-Server.
2037
   *
2038
   * @param bool $checkViaPing
2039
   *
2040
   * @return bool
2041
   */
2042 7
  public function reconnect(bool $checkViaPing = false): bool
2043
  {
2044 7
    $ping = false;
2045 7
    if ($checkViaPing === true) {
2046 6
      $ping = $this->ping();
2047
    }
2048
2049 7
    if ($ping === false) {
2050 7
      $this->connected = false;
2051 7
      $this->connect();
2052
    }
2053
2054 7
    return $this->isReady();
2055
  }
2056
2057
  /**
2058
   * Execute a "replace"-query.
2059
   *
2060
   * @param string      $table
2061
   * @param array       $data
2062
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2063
   *
2064
   * @return false|int <p>false on error</p>
2065
   *
2066
   * @throws QueryException
2067
   */
2068 3
  public function replace(string $table, array $data = [], string $databaseName = null)
2069
  {
2070
    // init
2071 3
    $table = \trim($table);
2072
2073 3
    if ($table === '') {
2074 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2075
2076 3
      return false;
2077
    }
2078
2079 3
    if (\count($data) === 0) {
2080 3
      $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2081
2082 3
      return false;
2083
    }
2084
2085
    // extracting column names
2086 3
    $columns = \array_keys($data);
2087 3
    foreach ($columns as $k => $_key) {
2088
      /** @noinspection AlterInForeachInspection */
2089 3
      $columns[$k] = $this->quote_string($_key);
2090
    }
2091
2092 3
    $columns = \implode(',', $columns);
2093
2094
    // extracting values
2095 3
    foreach ($data as $k => $_value) {
2096
      /** @noinspection AlterInForeachInspection */
2097 3
      $data[$k] = $this->secure($_value);
2098
    }
2099 3
    $values = \implode(',', $data);
2100
2101 3
    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...
2102
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2103
    }
2104
2105 3
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
2106
2107 3
    return $this->query($sql);
2108
  }
2109
2110
  /**
2111
   * Rollback in a transaction and end the transaction.
2112
   *
2113
   * @return bool <p>bool true on success, false otherwise.</p>
2114
   */
2115 12 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...
2116
  {
2117 12
    if ($this->in_transaction === false) {
2118
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
2119
2120
      return false;
2121
    }
2122
2123 12
    if ($this->mysqli_link) {
2124 12
      $return = \mysqli_rollback($this->mysqli_link);
2125 12
      \mysqli_autocommit($this->mysqli_link, true);
2126
    } elseif ($this->isDoctrinePDOConnection() === true) {
2127
      $this->doctrine_connection->rollBack();
2128
      $this->doctrine_connection->setAutoCommit(true);
2129
2130
      if ($this->doctrine_connection->isAutoCommit() === true) {
2131
        $return = true;
2132
      } else {
2133
        $return = false;
2134
      }
2135
    }
2136
2137 12
    $this->in_transaction = false;
2138
2139 12
    return $return;
0 ignored issues
show
Bug introduced by
The variable $return 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...
2140
  }
2141
2142
  /**
2143
   * Try to secure a variable, so can you use it in sql-queries.
2144
   *
2145
   * <p>
2146
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
2147
   * 1. parse into "int"
2148
   * </p><br />
2149
   *
2150
   * <p>
2151
   * <strong>float:</strong><br />
2152
   * 1. return "float"
2153
   * </p><br />
2154
   *
2155
   * <p>
2156
   * <strong>string:</strong><br />
2157
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2158
   * 2. trim '<br />
2159
   * 3. escape the string (and remove non utf-8 chars)<br />
2160
   * 4. trim ' again (because we maybe removed some chars)<br />
2161
   * 5. add ' around the new string<br />
2162
   * </p><br />
2163
   *
2164
   * <p>
2165
   * <strong>array:</strong><br />
2166
   * 1. return null
2167
   * </p><br />
2168
   *
2169
   * <p>
2170
   * <strong>object:</strong><br />
2171
   * 1. return false
2172
   * </p><br />
2173
   *
2174
   * <p>
2175
   * <strong>null:</strong><br />
2176
   * 1. return null
2177
   * </p>
2178
   *
2179
   * @param mixed     $var
2180
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2181
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2182
   *                                 <strong>null</strong> => Convert the array into null, every time.
2183
   *
2184
   * @return mixed
2185
   */
2186 97
  public function secure($var, $convert_array = true)
2187
  {
2188 97
    if (\is_array($var)) {
2189 6
      if ($convert_array === null) {
2190
2191
        if ($this->convert_null_to_empty_string === true) {
2192
          $var = "''";
2193
        } else {
2194
          $var = 'NULL';
2195
        }
2196
2197
      } else {
2198
2199 6
        $varCleaned = [];
2200 6
        foreach ((array)$var as $key => $value) {
2201
2202 6
          $key = $this->escape($key, false, false, $convert_array);
2203 6
          $value = $this->secure($value);
2204
2205
          /** @noinspection OffsetOperationsInspection */
2206 6
          $varCleaned[$key] = $value;
2207
        }
2208
2209 6 View Code Duplication
        if ($convert_array === true) {
2210 6
          $varCleaned = \implode(',', $varCleaned);
2211
2212 6
          $var = $varCleaned;
2213
        } else {
2214
          $var = $varCleaned;
2215
        }
2216
2217
      }
2218
2219 6
      return $var;
2220
    }
2221
2222 97
    if ($var === '') {
2223 6
      return "''";
2224
    }
2225
2226 97
    if ($var === "''") {
2227 3
      return "''";
2228
    }
2229
2230 97
    if ($var === null) {
2231 4
      if ($this->convert_null_to_empty_string === true) {
2232 3
        return "''";
2233
      }
2234
2235 4
      return 'NULL';
2236
    }
2237
2238 96
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2239 3
      return $var;
2240
    }
2241
2242 96
    if (\is_string($var)) {
2243 83
      $var = \trim($var, "'");
2244
    }
2245
2246 96
    $var = $this->escape($var, false, false, null);
2247
2248 93
    if (\is_string($var)) {
2249 83
      $var = "'" . \trim($var, "'") . "'";
2250
    }
2251
2252 93
    return $var;
2253
  }
2254
2255
  /**
2256
   * Execute a "select"-query.
2257
   *
2258
   * @param string       $table
2259
   * @param string|array $where
2260
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2261
   *
2262
   * @return false|Result <p>false on error</p>
2263
   *
2264
   * @throws QueryException
2265
   */
2266 62 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...
2267
  {
2268
    // init
2269 62
    $table = \trim($table);
2270
2271 62
    if ($table === '') {
2272 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2273
2274 3
      return false;
2275
    }
2276
2277 62
    if (\is_string($where)) {
2278 24
      $WHERE = $this->escape($where, false);
2279 41
    } elseif (\is_array($where)) {
2280 41
      $WHERE = $this->_parseArrayPair($where, 'AND');
2281
    } else {
2282 3
      $WHERE = '';
2283
    }
2284
2285 62
    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...
2286
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2287
    }
2288
2289 62
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
2290
2291 62
    return $this->query($sql);
2292
  }
2293
2294
  /**
2295
   * Selects a different database than the one specified on construction.
2296
   *
2297
   * @param string $database <p>Database name to switch to.</p>
2298
   *
2299
   * @return bool <p>bool true on success, false otherwise.</p>
2300
   */
2301
  public function select_db(string $database): bool
2302
  {
2303
    if (!$this->isReady()) {
2304
      return false;
2305
    }
2306
2307
    if ($this->mysqli_link) {
2308
      return \mysqli_select_db($this->mysqli_link, $database);
2309
    }
2310
2311
    if ($this->isDoctrinePDOConnection()) {
2312
      return $this->query('use :database', ['database' => $database]);
2313
    }
2314
2315
    return false;
2316
  }
2317
2318
  /**
2319
   * @param array $extra_config   <p>
2320
   *                              'session_to_db' => false|true<br>
2321
   *                              'socket' => 'string (path)'<br>
2322
   *                              'ssl' => 'bool'<br>
2323
   *                              'clientkey' => 'string (path)'<br>
2324
   *                              'clientcert' => 'string (path)'<br>
2325
   *                              'cacert' => 'string (path)'<br>
2326
   *                              </p>
2327
   */
2328 23
  public function setConfigExtra(array $extra_config)
2329
  {
2330 23
    if (isset($extra_config['session_to_db'])) {
2331
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
2332
    }
2333
2334 23
    if (isset($extra_config['doctrine'])) {
2335
      if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2336
        $this->doctrine_connection = $extra_config['doctrine'];
2337
      } else {
2338
        throw new DBConnectException('Error "doctrine"-connection is not valid');
2339
      }
2340
    }
2341
2342 23
    if (isset($extra_config['socket'])) {
2343
      $this->socket = $extra_config['socket'];
2344
    }
2345
2346 23
    if (isset($extra_config['ssl'])) {
2347
      $this->ssl = $extra_config['ssl'];
2348
    }
2349
2350 23
    if (isset($extra_config['clientkey'])) {
2351
      $this->clientkey = $extra_config['clientkey'];
2352
    }
2353
2354 23
    if (isset($extra_config['clientcert'])) {
2355
      $this->clientcert = $extra_config['clientcert'];
2356
    }
2357
2358 23
    if (isset($extra_config['cacert'])) {
2359
      $this->cacert = $extra_config['cacert'];
2360
    }
2361 23
  }
2362
2363
  /**
2364
   * Set the current charset.
2365
   *
2366
   * @param string $charset
2367
   *
2368
   * @return bool
2369
   */
2370 14
  public function set_charset(string $charset): bool
2371
  {
2372 14
    $charsetLower = strtolower($charset);
2373 14
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2374 8
      $charset = 'utf8';
2375
    }
2376 14
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
2377 8
      $charset = 'utf8mb4';
2378
    }
2379
2380 14
    $this->charset = $charset;
2381
2382
    if (
2383 14
        $this->mysqli_link
2384
        &&
2385 14
        $this->mysqli_link instanceof \mysqli
2386
    ) {
2387
2388 14
      $return = mysqli_set_charset($this->mysqli_link, $charset);
2389
2390
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
2391 14
      @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2392
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
2393 14
      @\mysqli_query($this->mysqli_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...
2394
2395
    } elseif ($this->isDoctrinePDOConnection() === true) {
2396
2397
      $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2398
2399
      $doctrineWrappedConnection->exec('SET CHARACTER SET ' . $charset);
2400
      $doctrineWrappedConnection->exec("SET NAMES '" . $charset . "'");
2401
2402
      $return = true;
2403
2404
    } else {
2405
2406
      throw new DBConnectException('Can not set the charset');
2407
2408
    }
2409
2410 14
    return $return;
2411
  }
2412
2413
  /**
2414
   * Set the option to convert null to "''" (empty string).
2415
   *
2416
   * Used in secure() => select(), insert(), update(), delete()
2417
   *
2418
   * @deprecated It's not recommended to convert NULL into an empty string!
2419
   *
2420
   * @param bool $bool
2421
   *
2422
   * @return self
2423
   */
2424 3
  public function set_convert_null_to_empty_string(bool $bool): self
2425
  {
2426 3
    $this->convert_null_to_empty_string = $bool;
2427
2428 3
    return $this;
2429
  }
2430
2431
  /**
2432
   * Enables or disables internal report functions
2433
   *
2434
   * @link http://php.net/manual/en/function.mysqli-report.php
2435
   *
2436
   * @param int $flags <p>
2437
   *                   <table>
2438
   *                   Supported flags
2439
   *                   <tr valign="top">
2440
   *                   <td>Name</td>
2441
   *                   <td>Description</td>
2442
   *                   </tr>
2443
   *                   <tr valign="top">
2444
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2445
   *                   <td>Turns reporting off</td>
2446
   *                   </tr>
2447
   *                   <tr valign="top">
2448
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2449
   *                   <td>Report errors from mysqli function calls</td>
2450
   *                   </tr>
2451
   *                   <tr valign="top">
2452
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2453
   *                   <td>
2454
   *                   Throw <b>mysqli_sql_exception</b> for errors
2455
   *                   instead of warnings
2456
   *                   </td>
2457
   *                   </tr>
2458
   *                   <tr valign="top">
2459
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2460
   *                   <td>Report if no index or bad index was used in a query</td>
2461
   *                   </tr>
2462
   *                   <tr valign="top">
2463
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2464
   *                   <td>Set all options (report all)</td>
2465
   *                   </tr>
2466
   *                   </table>
2467
   *                   </p>
2468
   *
2469
   * @return bool
2470
   */
2471
  public function set_mysqli_report(int $flags): bool
2472
  {
2473
    if (
2474
        $this->mysqli_link
2475
        &&
2476
        $this->mysqli_link instanceof \mysqli
2477
    ) {
2478
      return \mysqli_report($flags);
2479
    }
2480
2481
    return false;
2482
  }
2483
2484
  /**
2485
   * Show config errors by throw exceptions.
2486
   *
2487
   * @return bool
2488
   *
2489
   * @throws \InvalidArgumentException
2490
   */
2491 23
  public function showConfigError(): bool
2492
  {
2493
    // check if a doctrine connection is already open, first
2494
    if (
2495 23
        $this->doctrine_connection
2496
        &&
2497 23
        $this->doctrine_connection->isConnected()
2498
    ) {
2499
      return true;
2500
    }
2501
2502
    if (
2503 23
        !$this->hostname
2504
        ||
2505 20
        !$this->username
2506
        ||
2507 23
        !$this->database
2508
    ) {
2509
2510 9
      if (!$this->hostname) {
2511 3
        throw new \InvalidArgumentException('no-sql-hostname');
2512
      }
2513
2514 6
      if (!$this->username) {
2515 3
        throw new \InvalidArgumentException('no-sql-username');
2516
      }
2517
2518 3
      if (!$this->database) {
2519 3
        throw new \InvalidArgumentException('no-sql-database');
2520
      }
2521
2522
      return false;
2523
    }
2524
2525 14
    return true;
2526
  }
2527
2528
  /**
2529
   * alias: "beginTransaction()"
2530
   */
2531 3
  public function startTransaction(): bool
2532
  {
2533 3
    return $this->beginTransaction();
2534
  }
2535
2536
  /**
2537
   * Determine if database table exists
2538
   *
2539
   * @param string $table
2540
   *
2541
   * @return bool
2542
   */
2543 3
  public function table_exists(string $table): bool
2544
  {
2545 3
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2546
2547 3
    return $check !== false
2548
           &&
2549 3
           $check instanceof Result
2550
           &&
2551 3
           $check->num_rows > 0;
2552
  }
2553
2554
  /**
2555
   * Execute a callback inside a transaction.
2556
   *
2557
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2558
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2559
   *
2560
   * @return bool <p>bool true on success, false otherwise.</p>
2561
   */
2562 3
  public function transact($callback): bool
2563
  {
2564
    try {
2565
2566 3
      $beginTransaction = $this->beginTransaction();
2567 3
      if ($beginTransaction === false) {
2568 3
        $this->debug->displayError('Error: transact -> can not start transaction!', false);
2569
2570 3
        return false;
2571
      }
2572
2573 3
      $result = $callback($this);
2574 3
      if ($result === false) {
2575
        /** @noinspection ThrowRawExceptionInspection */
2576 3
        throw new \Exception('call_user_func [' . $callback . '] === false');
2577
      }
2578
2579 3
      return $this->commit();
2580
2581 3
    } catch (\Exception $e) {
2582
2583 3
      $this->rollback();
2584
2585 3
      return false;
2586
    }
2587
  }
2588
2589
  /**
2590
   * Execute a "update"-query.
2591
   *
2592
   * @param string       $table
2593
   * @param array        $data
2594
   * @param array|string $where
2595
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2596
   *
2597
   * @return false|int <p>false on error</p>
2598
   *
2599
   * @throws QueryException
2600
   */
2601 21
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2602
  {
2603
    // init
2604 21
    $table = \trim($table);
2605
2606 21
    if ($table === '') {
2607 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2608
2609 3
      return false;
2610
    }
2611
2612 21
    if (\count($data) === 0) {
2613 6
      $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2614
2615 6
      return false;
2616
    }
2617
2618
    // DEBUG
2619
    //var_dump($data);
2620
2621 21
    $SET = $this->_parseArrayPair($data);
2622
2623
    // DEBUG
2624
    //var_dump($SET);
2625
2626 21
    if (\is_string($where)) {
2627 6
      $WHERE = $this->escape($where, false);
2628 18
    } elseif (\is_array($where)) {
2629 15
      $WHERE = $this->_parseArrayPair($where, 'AND');
2630
    } else {
2631 3
      $WHERE = '';
2632
    }
2633
2634 21
    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...
2635
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2636
    }
2637
2638 21
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2639
2640 21
    return $this->query($sql);
2641
  }
2642
2643
}
2644