Completed
Push — master ( 41ecec...58fd6d )
by Lars
01:56
created

DB::isDoctrinePDOConnection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 3
CRAP Score 4.125

Importance

Changes 0
Metric Value
dl 11
loc 11
ccs 3
cts 6
cp 0.5
rs 9.9
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 4.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
      $_connector_dummy = null;
354 72
      $_glueHelper = '';
355 72
      $_key_upper = \strtoupper($_key);
356
357 72
      if (\strpos($_key_upper, ' NOT') !== false) {
358 6
        $_connector = 'NOT';
359
      }
360
361 72
      if (\strpos($_key_upper, ' IS') !== false) {
362 3
        $_connector = 'IS';
363
      }
364
365 72
      if (\strpos($_key_upper, ' IS NOT') !== false) {
366 3
        $_connector = 'IS NOT';
367
      }
368
369 72
      if (\strpos($_key_upper, ' IN') !== false) {
370 3
        $_connector = 'IN';
371
      }
372
373 72
      if (\strpos($_key_upper, ' NOT IN') !== false) {
374 3
        $_connector = 'NOT IN';
375
      }
376
377 72
      if (\strpos($_key_upper, ' BETWEEN') !== false) {
378 3
        $_connector = 'BETWEEN';
379
      }
380
381 72
      if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
382 3
        $_connector = 'NOT BETWEEN';
383
      }
384
385 72
      if (\strpos($_key_upper, ' LIKE') !== false) {
386 6
        $_connector = 'LIKE';
387
      }
388
389 72
      if (\strpos($_key_upper, ' NOT LIKE') !== false) {
390 6
        $_connector = 'NOT LIKE';
391
      }
392
393 72 View Code Duplication
      if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
394 8
        $_connector = '>';
395
      }
396
397 72 View Code Duplication
      if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
398 3
        $_connector = '<';
399
      }
400
401 72
      if (\strpos($_key_upper, ' >=') !== false) {
402 8
        $_connector = '>=';
403
      }
404
405 72
      if (\strpos($_key_upper, ' <=') !== false) {
406 3
        $_connector = '<=';
407
      }
408
409 72
      if (\strpos($_key_upper, ' <>') !== false) {
410 3
        $_connector = '<>';
411
      }
412
413 72
      if (\strpos($_key_upper, ' OR') !== false) {
414 6
        $_glueHelper = 'OR';
415
      }
416
417 72
      if (\strpos($_key_upper, ' AND') !== false) {
418 3
        $_glueHelper = 'AND';
419
      }
420
421 72
      if (\strpos($_key_upper, ' +') !== false) {
422 1
        $_connector = '+';
423
      }
424
425 72
      if (\strpos($_key_upper, ' -') !== false) {
426 1
        $_connector = '-';
427
      }
428
429 72
      if (\is_array($_value) === true) {
430 7
        $firstKey = null;
431 7
        $firstValue = null;
432 7
        foreach ($_value as $oldKey => $oldValue) {
433 7
          $_value[$oldKey] = $this->secure($oldValue);
434
435 7
          if ($firstKey === null) {
436 7
            $firstKey = $oldKey;
437
          }
438
439 7
          if ($firstValue === null) {
440 7
            $firstValue = $_value[$oldKey];
441
          }
442
        }
443
444 7
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
445 3
          $_value = '(' . \implode(',', $_value) . ')';
446 7
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
447 3
          $_value = '(' . \implode(' AND ', $_value) . ')';
448 7
        } elseif ($_connector === '+' || $_connector === '-') {
449 1
          $_value = $firstKey . ' ' . $_connector . ' ' . $firstValue;
450 1
          $_connector_dummy = $_connector;
451 7
          $_connector = '=';
452
        }
453
454
      } else {
455 72
        $_value = $this->secure($_value);
456
      }
457
458 72
      $quoteString = $this->quote_string(
459 72
          \trim(
460 72
              \str_ireplace(
461
                  [
462 72
                      $_connector,
463 72
                      $_connector_dummy,
464 72
                      $_glueHelper,
465
                  ],
466 72
                  '',
467 72
                  $_key
468
              )
469
          )
470
      );
471
472 72
      $_value = (array)$_value;
473
474 72
      if (!$_glueHelper) {
475 72
        $_glueHelper = $glue;
476
      }
477
478 72
      $tmpCounter = 0;
479 72
      foreach ($_value as $valueInner) {
480
481 72
        $_glueHelperInner = $_glueHelper;
482
483 72
        if ($arrayPairCounter === 0) {
484
485 72
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
486 3
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
487 72
          } elseif ($tmpCounter === 0) {
488 72
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
489
          }
490
491 68
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
492 3
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
493
        }
494
495 72
        if (\is_string($valueInner) && $valueInner === '') {
496
          $valueInner = "''";
497
        }
498
499 72
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
500 72
        $tmpCounter++;
501
      }
502
503 72
      if ($_glueHelper === 'OR') {
504 6
        $sql .= ' ) ';
505
      }
506
507 72
      $arrayPairCounter++;
508
    }
509
510 72
    return $sql;
511
  }
512
513
  /**
514
   * _parseQueryParams
515
   *
516
   * @param string $sql
517
   * @param array  $params
518
   *
519
   * @return array <p>with the keys -> 'sql', 'params'</p>
520
   */
521 17
  private function _parseQueryParams(string $sql, array $params = []): array
522
  {
523 17
    $offset = \strpos($sql, '?');
524
525
    // is there anything to parse?
526
    if (
527 17
        $offset === false
528
        ||
529 17
        \count($params) === 0
530
    ) {
531 13
      return ['sql' => $sql, 'params' => $params];
532
    }
533
534 7
    foreach ($params as $key => $param) {
535
536
      // use this only for not named parameters
537 7
      if (!\is_int($key)) {
538 3
        continue;
539
      }
540
541 7
      if ($offset === false) {
542
        continue;
543
      }
544
545 7
      $replacement = $this->secure($param);
546
547 7
      unset($params[$key]);
548
549 7
      $sql = \substr_replace($sql, $replacement, $offset, 1);
550 7
      $offset = \strpos($sql, '?', $offset + \strlen((string)$replacement));
551
    }
552
553 7
    return ['sql' => $sql, 'params' => $params];
554
  }
555
556
  /**
557
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
558
   *
559
   * @param mixed $sql    <p>The SQL string.</p>
560
   * @param array $params <p>An array of key-value bindings.</p>
561
   *
562
   * @return array <p>with the keys -> 'sql', 'params'</p>
563
   */
564 20
  private function _parseQueryParamsByName(string $sql, array $params = []): array
565
  {
566
    // is there anything to parse?
567
    if (
568 20
        \strpos($sql, ':') === false
569
        ||
570 20
        \count($params) === 0
571
    ) {
572 7
      return ['sql' => $sql, 'params' => $params];
573
    }
574
575 16
    $offset = null;
576 16
    $replacement = null;
577 16
    foreach ($params as $name => $param) {
578
579
      // use this only for named parameters
580 16
      if (\is_int($name)) {
581
        continue;
582
      }
583
584
      // add ":" if needed
585 16
      if (\strpos($name, ':') !== 0) {
586 6
        $nameTmp = ':' . $name;
587
      } else {
588 10
        $nameTmp = $name;
589
      }
590
591 16
      if ($offset === null) {
592 16
        $offset = \strpos($sql, $nameTmp);
593
      } else {
594 15
        $offset = \strpos($sql, $nameTmp, $offset + \strlen((string)$replacement));
595
      }
596
597 16
      if ($offset === false) {
598 3
        continue;
599
      }
600
601 16
      $replacement = $this->secure($param);
602
603 16
      unset($params[$name]);
604
605 16
      $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
606
    }
607
608 16
    return ['sql' => $sql, 'params' => $params];
609
  }
610
611
  /**
612
   * Gets the number of affected rows in a previous MySQL operation.
613
   *
614
   * @return int
615
   */
616 28
  public function affected_rows(): int
617
  {
618
    if (
619 28
        $this->mysqli_link
620
        &&
621 28
        $this->mysqli_link instanceof \mysqli
622
    ) {
623 28
      return \mysqli_affected_rows($this->mysqli_link);
624
    }
625
626
    return (int)$this->affected_rows;
627
  }
628
629
  /**
630
   * Begins a transaction, by turning off auto commit.
631
   *
632
   * @return bool <p>This will return true or false indicating success of transaction</p>
633
   */
634 18
  public function beginTransaction(): bool
635
  {
636 18
    if ($this->in_transaction === true) {
637 6
      $this->debug->displayError('Error: mysql server already in transaction!', false);
638
639 6
      return false;
640
    }
641
642 18
    $this->clearErrors(); // needed for "$this->endTransaction()"
643 18
    $this->in_transaction = true;
644
645 18
    if ($this->mysqli_link) {
646 18
      $return = \mysqli_autocommit($this->mysqli_link, false);
647
    } elseif ($this->isDoctrinePDOConnection() === true) {
648
      $this->doctrine_connection->setAutoCommit(false);
649
      $this->doctrine_connection->beginTransaction();
650
651
      if ($this->doctrine_connection->isTransactionActive() === true) {
652
        $return = true;
653
      } else {
654
        $return = false;
655
      }
656
    }
657
658 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...
659
      $this->in_transaction = false;
660
    }
661
662 18
    return $return;
663
  }
664
665
  /**
666
   * Clear the errors in "_debug->_errors".
667
   *
668
   * @return bool
669
   */
670 18
  public function clearErrors(): bool
671
  {
672 18
    return $this->debug->clearErrors();
673
  }
674
675
  /**
676
   * Closes a previously opened database connection.
677
   *
678
   * @return bool
679
   *              Will return "true", if the connection was closed,
680
   *              otherwise (e.g. if the connection was already closed) "false".
681
   */
682 6
  public function close(): bool
683
  {
684 6
    $this->connected = false;
685
686
    if (
687 6
        $this->doctrine_connection
688
        &&
689 6
        $this->doctrine_connection instanceof \Doctrine\DBAL\Connection
690
    ) {
691
692
      $connectedBefore = $this->doctrine_connection->isConnected();
693
694
      $this->doctrine_connection->close();
695
696
      $this->mysqli_link = null;
697
698
      if ($connectedBefore === true) {
699
        return !$this->doctrine_connection->isConnected();
700
      }
701
702
      return false;
703
    }
704
705
    if (
706 6
        $this->mysqli_link
707
        &&
708 6
        $this->mysqli_link instanceof \mysqli
709
    ) {
710 6
      $result = \mysqli_close($this->mysqli_link);
711 6
      $this->mysqli_link = null;
712
713 6
      return $result;
714
    }
715
716 3
    $this->mysqli_link = null;
717
718 3
    return false;
719
  }
720
721
  /**
722
   * Commits the current transaction and end the transaction.
723
   *
724
   * @return bool <p>bool true on success, false otherwise.</p>
725
   */
726 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...
727
  {
728 9
    if ($this->in_transaction === false) {
729
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
730
731
      return false;
732
    }
733
734 9
    if ($this->mysqli_link) {
735 9
      $return = \mysqli_commit($this->mysqli_link);
736 9
      \mysqli_autocommit($this->mysqli_link, true);
737
    } elseif ($this->isDoctrinePDOConnection() === true) {
738
      $this->doctrine_connection->commit();
739
      $this->doctrine_connection->setAutoCommit(true);
740
741
      if ($this->doctrine_connection->isAutoCommit() === true) {
742
        $return = true;
743
      } else {
744
        $return = false;
745
      }
746
    }
747
748 9
    $this->in_transaction = false;
749
750 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...
751
  }
752
753
  /**
754
   * Open a new connection to the MySQL server.
755
   *
756
   * @return bool
757
   *
758
   * @throws DBConnectException
759
   */
760 20
  public function connect(): bool
761
  {
762 20
    if ($this->isReady()) {
763 3
      return true;
764
    }
765
766 20
    if ($this->doctrine_connection) {
767
      $this->doctrine_connection->connect();
768
769
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
770
771 View Code Duplication
      if ($this->isDoctrineMySQLiConnection() === true) {
772
        /* @var $doctrineWrappedConnection \Doctrine\DBAL\Driver\Mysqli\MysqliConnection */
773
774
        $this->mysqli_link = $doctrineWrappedConnection->getWrappedResourceHandle();
775
776
        $this->connected = $this->doctrine_connection->isConnected();
777
778
        if (!$this->connected) {
779
          $error = 'Error connecting to mysql server: ' . $this->doctrine_connection->errorInfo();
780
          $this->debug->displayError($error, false);
781
          throw new DBConnectException($error, 101);
782
        }
783
784
        $this->set_charset($this->charset);
785
786
        return $this->isReady();
787
      }
788
789 View Code Duplication
      if ($this->isDoctrinePDOConnection() === true) {
790
791
        $this->mysqli_link = null;
792
793
        $this->connected = $this->doctrine_connection->isConnected();
794
795
        if (!$this->connected) {
796
          $error = 'Error connecting to mysql server: ' . $this->doctrine_connection->errorInfo();
797
          $this->debug->displayError($error, false);
798
          throw new DBConnectException($error, 101);
799
        }
800
801
        $this->set_charset($this->charset);
802
803
        return $this->isReady();
804
      }
805
    }
806
807 20
    $flags = null;
808
809 20
    \mysqli_report(MYSQLI_REPORT_STRICT);
810
    try {
811 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...
812
813 20
      if (Helper::isMysqlndIsUsed() === true) {
814 20
        \mysqli_options($this->mysqli_link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
815
      }
816
817 20
      if ($this->ssl === true) {
818
819
        if (empty($this->clientcert)) {
820
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
821
        }
822
823
        if (empty($this->clientkey)) {
824
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
825
        }
826
827
        if (empty($this->cacert)) {
828
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
829
        }
830
831
        \mysqli_options($this->mysqli_link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
832
833
        /** @noinspection PhpParamsInspection */
834
        \mysqli_ssl_set(
835
            $this->mysqli_link,
836
            $this->clientkey,
837
            $this->clientcert,
838
            $this->cacert,
839
            null,
840
            null
841
        );
842
843
        $flags = MYSQLI_CLIENT_SSL;
844
      }
845
846
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
847 20
      $this->connected = @\mysqli_real_connect(
848 20
          $this->mysqli_link,
849 20
          $this->hostname,
850 20
          $this->username,
851 20
          $this->password,
852 20
          $this->database,
853 20
          $this->port,
854 20
          $this->socket,
855 20
          (int)$flags
856
      );
857
858 9
    } catch (\Exception $e) {
859 9
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
860 9
      $this->debug->displayError($error, false);
861 9
      throw new DBConnectException($error, 100, $e);
862
    }
863 11
    \mysqli_report(MYSQLI_REPORT_OFF);
864
865 11
    $errno = \mysqli_connect_errno();
866 11
    if (!$this->connected || $errno) {
867
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
868
      $this->debug->displayError($error, false);
869
      throw new DBConnectException($error, 101);
870
    }
871
872 11
    $this->set_charset($this->charset);
873
874 11
    return $this->isReady();
875
  }
876
877
  /**
878
   * Execute a "delete"-query.
879
   *
880
   * @param string       $table
881
   * @param string|array $where
882
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
883
   *
884
   * @return false|int <p>false on error</p>
885
   *
886
   * @throws QueryException
887
   */
888 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...
889
  {
890
    // init
891 4
    $table = \trim($table);
892
893 4
    if ($table === '') {
894 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
895
896 3
      return false;
897
    }
898
899 4
    if (\is_string($where)) {
900 3
      $WHERE = $this->escape($where, false);
901 4
    } elseif (\is_array($where)) {
902 4
      $WHERE = $this->_parseArrayPair($where, 'AND');
903
    } else {
904 3
      $WHERE = '';
905
    }
906
907 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...
908
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
909
    }
910
911 4
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
912
913 4
    return $this->query($sql);
914
  }
915
916
  /**
917
   * Ends a transaction and commits if no errors, then ends autocommit.
918
   *
919
   * @return bool <p>This will return true or false indicating success of transactions.</p>
920
   */
921 12
  public function endTransaction(): bool
922
  {
923 12
    if ($this->in_transaction === false) {
924
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
925
926
      return false;
927
    }
928
929 12
    if (!$this->errors()) {
930 3
      $return = $this->commit();
931
    } else {
932 9
      $this->rollback();
933 9
      $return = false;
934
    }
935
936 12
    if ($this->mysqli_link) {
937 12
      \mysqli_autocommit($this->mysqli_link, true);
938
    } elseif ($this->isDoctrinePDOConnection() === true) {
939
      $this->doctrine_connection->setAutoCommit(true);
940
941
      if ($this->doctrine_connection->isAutoCommit() === true) {
942
        $return = true;
943
      } else {
944
        $return = false;
945
      }
946
    }
947
948 12
    $this->in_transaction = false;
949
950 12
    return $return;
951
  }
952
953
  /**
954
   * Get all errors from "$this->errors".
955
   *
956
   * @return array|false <p>false === on errors</p>
957
   */
958 12
  public function errors()
959
  {
960 12
    $errors = $this->debug->getErrors();
961
962 12
    return \count($errors) > 0 ? $errors : false;
963
  }
964
965
  /**
966
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
967
   *
968
   * @param mixed     $var           bool: convert into "integer"<br />
969
   *                                 int: int (don't change it)<br />
970
   *                                 float: float (don't change it)<br />
971
   *                                 null: null (don't change it)<br />
972
   *                                 array: run escape() for every key => value<br />
973
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
974
   * @param bool      $stripe_non_utf8
975
   * @param bool      $html_entity_decode
976
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
977
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
978
   *                                 <strong>null</strong> => Convert the array into null, every time.
979
   *
980
   * @return mixed
981
   */
982 119
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
983
  {
984
    // [empty]
985 119
    if ($var === '') {
986 6
      return '';
987
    }
988
989
    // ''
990 119
    if ($var === "''") {
991
      return "''";
992
    }
993
994
    // check the type
995 119
    $type = \gettype($var);
996
997 119
    if ($type === 'object') {
998 9
      if ($var instanceof \DateTime) {
999 9
        $var = $var->format('Y-m-d H:i:s');
1000 9
        $type = 'string';
1001 6
      } elseif (\method_exists($var, '__toString')) {
1002 6
        $var = (string)$var;
1003 6
        $type = 'string';
1004
      }
1005
    }
1006
1007
    switch ($type) {
1008 119
      case 'boolean':
1009 9
        $var = (int)$var;
1010 9
        break;
1011
1012 119
      case 'double':
1013 119
      case 'integer':
1014 70
        break;
1015
1016 113
      case 'string':
1017 113
        if ($stripe_non_utf8 === true) {
1018 23
          $var = UTF8::cleanup($var);
1019
        }
1020
1021 113
        if ($html_entity_decode === true) {
1022 3
          $var = UTF8::html_entity_decode($var);
1023
        }
1024
1025 113
        $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
1026
1027
        if (
1028 113
            $this->mysqli_link
1029
            &&
1030 113
            $this->mysqli_link instanceof \mysqli
1031
        ) {
1032 113
          $var = \mysqli_real_escape_string($this->mysqli_link, $var);
1033
        } elseif ($this->isDoctrinePDOConnection() === true) {
1034
          $var = $this->getDoctrinePDOConnection()->quote($var);
1035
          $var = \substr($var, 1, -1);
1036
        }
1037
1038 113
        break;
1039
1040 9
      case 'array':
1041 6
        if ($convert_array === null) {
1042
1043 3
          if ($this->convert_null_to_empty_string === true) {
1044
            $var = "''";
1045
          } else {
1046 3
            $var = 'NULL';
1047
          }
1048
1049
        } else {
1050
1051 6
          $varCleaned = [];
1052 6
          foreach ((array)$var as $key => $value) {
1053
1054 6
            $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
1055 6
            $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
1056
1057
            /** @noinspection OffsetOperationsInspection */
1058 6
            $varCleaned[$key] = $value;
1059
          }
1060
1061 6 View Code Duplication
          if ($convert_array === true) {
1062 3
            $varCleaned = \implode(',', $varCleaned);
1063
1064 3
            $var = $varCleaned;
1065
          } else {
1066 6
            $var = $varCleaned;
1067
          }
1068
1069
        }
1070 6
        break;
1071
1072 9
      case 'NULL':
1073 6
        if ($this->convert_null_to_empty_string === true) {
1074
          $var = "''";
1075
        } else {
1076 6
          $var = 'NULL';
1077
        }
1078 6
        break;
1079
1080
      default:
1081 6
        throw new \InvalidArgumentException(sprintf('Not supported value "%s" of type %s.', print_r($var, true), $type));
1082
        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...
1083
    }
1084
1085 119
    return $var;
1086
  }
1087
1088
  /**
1089
   * Execute select/insert/update/delete sql-queries.
1090
   *
1091
   * @param string  $query    <p>sql-query</p>
1092
   * @param bool    $useCache optional <p>use cache?</p>
1093
   * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
1094
   * @param DB|null $db       optional <p>the database connection</p>
1095
   *
1096
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
1097
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
1098
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1099
   *               "true" by e.g. "DROP"-queries<br />
1100
   *               "false" on error
1101
   *
1102
   * @throws QueryException
1103
   */
1104 9
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1105
  {
1106
    // init
1107 9
    $cacheKey = null;
1108 9
    if (!$db) {
1109 9
      $db = self::getInstance();
1110
    }
1111
1112 9 View Code Duplication
    if ($useCache === true) {
1113 3
      $cache = new Cache(null, null, false, $useCache);
1114 3
      $cacheKey = 'sql-' . \md5($query);
1115
1116
      if (
1117 3
          $cache->getCacheIsReady() === true
1118
          &&
1119 3
          $cache->existsItem($cacheKey)
1120
      ) {
1121 3
        return $cache->getItem($cacheKey);
1122
      }
1123
1124
    } else {
1125 9
      $cache = false;
1126
    }
1127
1128 9
    $result = $db->query($query);
1129
1130 9
    if ($result instanceof Result) {
1131
1132 3
      $return = $result->fetchAllArray();
1133
1134
      // save into the cache
1135 View Code Duplication
      if (
1136 3
          $cacheKey !== null
1137
          &&
1138 3
          $useCache === true
1139
          &&
1140 3
          $cache instanceof Cache
1141
          &&
1142 3
          $cache->getCacheIsReady() === true
1143
      ) {
1144 3
        $cache->setItem($cacheKey, $return, $cacheTTL);
1145
      }
1146
1147
    } else {
1148 6
      $return = $result;
1149
    }
1150
1151 9
    return $return;
1152
  }
1153
1154
  /**
1155
   * Get all table-names via "SHOW TABLES".
1156
   *
1157
   * @return array
1158
   */
1159 3
  public function getAllTables(): array
1160
  {
1161 3
    $query = 'SHOW TABLES';
1162 3
    $result = $this->query($query);
1163
1164 3
    return $result->fetchAllArray();
1165
  }
1166
1167
  /**
1168
   * @return array
1169
   */
1170 8
  public function getConfig()
1171
  {
1172
    $config = [
1173 8
        'hostname'   => $this->hostname,
1174 8
        'username'   => $this->username,
1175 8
        'password'   => $this->password,
1176 8
        'port'       => $this->port,
1177 8
        'database'   => $this->database,
1178 8
        'socket'     => $this->socket,
1179 8
        'charset'    => $this->charset,
1180 8
        'cacert'     => $this->cacert,
1181 8
        'clientcert' => $this->clientcert,
1182 8
        'clientkey'  => $this->clientkey,
1183
    ];
1184
1185 8
    if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1186
      $config += $this->doctrine_connection->getParams();
1187
    }
1188
1189 8
    return $config;
1190
  }
1191
1192
  /**
1193
   * @return Debug
1194
   */
1195 9
  public function getDebugger(): Debug
1196
  {
1197 9
    return $this->debug;
1198
  }
1199
1200
  /**
1201
   * @return null|\Doctrine\DBAL\Connection|null
1202
   */
1203 2
  public function getDoctrineConnection()
1204
  {
1205 2
    return $this->doctrine_connection;
1206
  }
1207
1208
  /**
1209
   * @return false|\Doctrine\DBAL\Driver\Connection
1210
   */
1211 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...
1212
  {
1213
    if ($this->doctrine_connection) {
1214
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1215
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1216
        return $doctrineWrappedConnection;
1217
      }
1218
    }
1219
1220
    return false;
1221
  }
1222
1223
  /**
1224
   * Get errors from "$this->errors".
1225
   *
1226
   * @return array
1227
   */
1228 3
  public function getErrors(): array
1229
  {
1230 3
    return $this->debug->getErrors();
1231
  }
1232
1233
  /**
1234
   * @param string $hostname             <p>Hostname of the mysql server</p>
1235
   * @param string $username             <p>Username for the mysql connection</p>
1236
   * @param string $password             <p>Password for the mysql connection</p>
1237
   * @param string $database             <p>Database for the mysql connection</p>
1238
   * @param int    $port                 <p>default is (int)3306</p>
1239
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1240
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1241
   *                                     Use false to disable it.</p>
1242
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1243
   *                                     Use false to disable it.</p>
1244
   * @param string $logger_class_name
1245
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1246
   * @param array  $extra_config         <p>
1247
   *                                     're_connect'    => bool<br>
1248
   *                                     'session_to_db' => bool<br>
1249
   *                                     'doctrine'      => \Doctrine\DBAL\Connection<br>
1250
   *                                     'socket'        => 'string (path)'<br>
1251
   *                                     'ssl'           => bool<br>
1252
   *                                     'clientkey'     => 'string (path)'<br>
1253
   *                                     'clientcert'    => 'string (path)'<br>
1254
   *                                     'cacert'        => 'string (path)'<br>
1255
   *                                     </p>
1256
   *
1257
   * @return self
1258
   */
1259 231
  public static function getInstance(
1260
      string $hostname = '',
1261
      string $username = '',
1262
      string $password = '',
1263
      string $database = '',
1264
      $port = 3306,
1265
      string $charset = 'utf8',
1266
      bool $exit_on_error = true,
1267
      bool $echo_on_error = true,
1268
      string $logger_class_name = '',
1269
      string $logger_level = '',
1270
      array $extra_config = []
1271
  ): self
1272
  {
1273
    /**
1274
     * @var $instance self[]
1275
     */
1276 231
    static $instance = [];
1277
1278
    /**
1279
     * @var $firstInstance self
1280
     */
1281 231
    static $firstInstance = null;
1282
1283
    // fallback
1284 231
    if (!$charset) {
1285 121
      $charset = 'utf8';
1286
    }
1287
1288
    if (
1289 231
        '' . $hostname . $username . $password . $database . $port . $charset == '' . $port . $charset
1290
        &&
1291 231
        null !== $firstInstance
1292
    ) {
1293 143
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1294
        $firstInstance->reconnect(true);
1295
      }
1296
1297 143
      return $firstInstance;
1298
    }
1299
1300 150
    $extra_config_string = '';
1301 150
    foreach ($extra_config as $extra_config_key => $extra_config_value) {
1302
      if (\is_object($extra_config_value)) {
1303
        $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1304
      } else {
1305
        $extra_config_value_tmp = (string)$extra_config_value;
1306
      }
1307
      $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1308
    }
1309
1310 150
    $connection = \md5(
1311 150
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1312
    );
1313
1314 150
    if (!isset($instance[$connection])) {
1315 23
      $instance[$connection] = new self(
1316 23
          $hostname,
1317 23
          $username,
1318 23
          $password,
1319 23
          $database,
1320 23
          $port,
1321 23
          $charset,
1322 23
          $exit_on_error,
1323 23
          $echo_on_error,
1324 23
          $logger_class_name,
1325 23
          $logger_level,
1326 23
          $extra_config
1327
      );
1328
1329 5
      if (null === $firstInstance) {
1330 1
        $firstInstance = $instance[$connection];
1331
      }
1332
    }
1333
1334 138
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1335
      $instance[$connection]->reconnect(true);
1336
    }
1337
1338 138
    return $instance[$connection];
1339
  }
1340
1341
  /**
1342
   * @param \Doctrine\DBAL\Connection $doctrine
1343
   * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1344
   * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1345
   *                                                 return 'false'. Use false to disable it.</p>
1346
   * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1347
   *                                                 Use false to disable it.</p>
1348
   * @param string                    $logger_class_name
1349
   * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1350
   * @param array                     $extra_config  <p>
1351
   *                                                 're_connect'    => bool<br>
1352
   *                                                 'session_to_db' => bool<br>
1353
   *                                                 'doctrine'      => \Doctrine\DBAL\Connection<br>
1354
   *                                                 'socket'        => 'string (path)'<br>
1355
   *                                                 'ssl'           => bool<br>
1356
   *                                                 'clientkey'     => 'string (path)'<br>
1357
   *                                                 'clientcert'    => 'string (path)'<br>
1358
   *                                                 'cacert'        => 'string (path)'<br>
1359
   *                                                 </p>
1360
   *
1361
   * @return self
1362
   */
1363 55
  public static function getInstanceDoctrineHelper(
1364
      \Doctrine\DBAL\Connection $doctrine,
1365
      string $charset = 'utf8',
1366
      bool $exit_on_error = true,
1367
      bool $echo_on_error = true,
1368
      string $logger_class_name = '',
1369
      string $logger_level = '',
1370
      array $extra_config = []
1371
  ): self
1372
  {
1373 55
    $extra_config['doctrine'] = $doctrine;
1374
1375 55
    return self::getInstance(
1376 55
        '',
1377 55
        '',
1378 55
        '',
1379 55
        '',
1380 55
        3306,
1381 55
        $charset,
1382 55
        $exit_on_error,
1383 55
        $echo_on_error,
1384 55
        $logger_class_name,
1385 55
        $logger_level,
1386 55
        $extra_config
1387
    );
1388
  }
1389
1390
  /**
1391
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1392
   *
1393
   * @return null|\mysqli
1394
   */
1395 53
  public function getLink()
1396
  {
1397 53
    return $this->mysqli_link;
1398
  }
1399
1400
  /**
1401
   * Get the current charset.
1402
   *
1403
   * @return string
1404
   */
1405 3
  public function get_charset(): string
1406
  {
1407 3
    return $this->charset;
1408
  }
1409
1410
  /**
1411
   * Check if we are in a transaction.
1412
   *
1413
   * @return bool
1414
   */
1415
  public function inTransaction(): bool
1416
  {
1417
    return $this->in_transaction;
1418
  }
1419
1420
  /**
1421
   * Execute a "insert"-query.
1422
   *
1423
   * @param string      $table
1424
   * @param array       $data
1425
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1426
   *
1427
   * @return false|int <p>false on error</p>
1428
   *
1429
   * @throws QueryException
1430
   */
1431 74
  public function insert(string $table, array $data = [], string $databaseName = null)
1432
  {
1433
    // init
1434 74
    $table = \trim($table);
1435
1436 74
    if ($table === '') {
1437 6
      $this->debug->displayError('Invalid table name, table name in empty.', false);
1438
1439 6
      return false;
1440
    }
1441
1442 71
    if (\count($data) === 0) {
1443 9
      $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1444
1445 9
      return false;
1446
    }
1447
1448 65
    $SET = $this->_parseArrayPair($data);
1449
1450 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...
1451
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1452
    }
1453
1454 65
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET";
1455
1456 65
    return $this->query($sql);
1457
  }
1458
1459
  /**
1460
   * Returns the auto generated id used in the last query.
1461
   *
1462
   * @return int|string
1463
   */
1464 104
  public function insert_id()
1465
  {
1466 104
    if ($this->mysqli_link) {
1467 104
      return \mysqli_insert_id($this->mysqli_link);
1468
    }
1469
1470
    if ($this->getDoctrinePDOConnection()) {
1471
      return $this->getDoctrinePDOConnection()->lastInsertId();
1472
    }
1473
  }
1474
1475
  /**
1476
   * @return bool
1477
   */
1478
  public function isDoctrineMySQLiConnection(): bool
1479
  {
1480
    if ($this->doctrine_connection) {
1481
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1482
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1483
        return true;
1484
      }
1485
    }
1486
1487
    return false;
1488
  }
1489
1490
  /**
1491
   * @return bool
1492
   */
1493 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...
1494
  {
1495 9
    if ($this->doctrine_connection) {
1496
      $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1497
      if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1498
        return true;
1499
      }
1500
    }
1501
1502 9
    return false;
1503
  }
1504
1505
  /**
1506
   * Check if db-connection is ready.
1507
   *
1508
   * @return bool
1509
   */
1510 184
  public function isReady(): bool
1511
  {
1512 184
    return $this->connected ? true : false;
1513
  }
1514
1515
  /**
1516
   * Get the last sql-error.
1517
   *
1518
   * @return string|false <p>false === there was no error</p>
1519
   */
1520 3
  public function lastError()
1521
  {
1522 3
    $errors = $this->debug->getErrors();
1523
1524 3
    return \count($errors) > 0 ? end($errors) : false;
1525
  }
1526
1527
  /**
1528
   * Execute a sql-multi-query.
1529
   *
1530
   * @param string $sql
1531
   *
1532
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1533
   *                        "bool" by only "<b>INSERT</b>"-queries<br />
1534
   *                        "bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1535
   *                        "bool" by only by e.g. "DROP"-queries<br />
1536
   *
1537
   * @throws QueryException
1538
   */
1539 3
  public function multi_query(string $sql)
1540
  {
1541 3
    if (!$this->isReady()) {
1542
      return false;
1543
    }
1544
1545 3 View Code Duplication
    if (!$sql || $sql === '') {
1546 3
      $this->debug->displayError('Can not execute an empty query.', false);
1547
1548 3
      return false;
1549
    }
1550
1551 3
    if ($this->isDoctrinePDOConnection() === true) {
1552
1553
      $query_start_time = microtime(true);
1554
      $queryException = null;
1555
      $query_result_doctrine = false;
1556
1557
      try {
1558
        $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1559
        $resultTmp = $query_result_doctrine->execute();
1560
        $mysqli_field_count = $query_result_doctrine->columnCount();
1561
      } catch (\Exception $e) {
1562
        $resultTmp = false;
1563
        $mysqli_field_count = null;
1564
1565
        $queryException = $e;
1566
      }
1567
1568
      $query_duration = microtime(true) - $query_start_time;
1569
1570
      $this->debug->logQuery($sql, $query_duration, 0);
1571
1572
      $returnTheResult = false;
1573
      $result = [];
1574
1575
      if ($resultTmp) {
1576
1577
        if ($mysqli_field_count) {
1578
1579
          if (
1580
              $query_result_doctrine
1581
              &&
1582
              $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1583
          ) {
1584
            $result = $query_result_doctrine;
1585
          }
1586
1587
        } else {
1588
          $result = $resultTmp;
1589
        }
1590
1591
        if (
1592
            $result instanceof \Doctrine\DBAL\Statement
1593
            &&
1594
            $result->columnCount() > 0
1595
        ) {
1596
          $returnTheResult = true;
1597
1598
          // return query result object
1599
          $result = [new Result($sql, $result)];
1600
        } else {
1601
          $result = [$result];
1602
        }
1603
1604
      } else {
1605
1606
        // log the error query
1607
        $this->debug->logQuery($sql, $query_duration, 0, true);
1608
1609
        if (
1610
            isset($queryException)
1611
            &&
1612
            $queryException instanceof \Doctrine\DBAL\Query\QueryException
1613
        ) {
1614
          return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1615
        }
1616
      }
1617
1618
    } else {
1619
1620 3
      $query_start_time = microtime(true);
1621 3
      $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1622 3
      $query_duration = microtime(true) - $query_start_time;
1623
1624 3
      $this->debug->logQuery($sql, $query_duration, 0);
1625
1626 3
      $returnTheResult = false;
1627 3
      $result = [];
1628
1629 3
      if ($resultTmp) {
1630
        do {
1631
1632 3
          $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1633
1634 3
          if ($resultTmpInner instanceof \mysqli_result) {
1635
1636 3
            $returnTheResult = true;
1637 3
            $result[] = new Result($sql, $resultTmpInner);
1638
1639
          } elseif (
1640 3
              $resultTmpInner === true
1641
              ||
1642 3
              !\mysqli_errno($this->mysqli_link)
1643
          ) {
1644
1645 3
            $result[] = true;
1646
1647
          } else {
1648
1649
            $result[] = false;
1650
1651
          }
1652
1653 3
        } while (\mysqli_more_results($this->mysqli_link) === true ? \mysqli_next_result($this->mysqli_link) : false);
1654
1655
      } else {
1656
1657
        // log the error query
1658 3
        $this->debug->logQuery($sql, $query_duration, 0, true);
1659
1660 3
        return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1661
      }
1662
1663
    }
1664
1665
    // return the result only if there was a "SELECT"-query
1666 3
    if ($returnTheResult === true) {
1667 3
      return $result;
1668
    }
1669
1670
    if (
1671 3
        \count($result) > 0
1672
        &&
1673 3
        \in_array(false, $result, true) === false
1674
    ) {
1675 3
      return true;
1676
    }
1677
1678
    return false;
1679
  }
1680
1681
  /**
1682
   * Count number of rows found matching a specific query.
1683
   *
1684
   * @param string $query
1685
   *
1686
   * @return int
1687
   */
1688 3
  public function num_rows(string $query): int
1689
  {
1690 3
    $check = $this->query($query);
1691
1692
    if (
1693 3
        $check === false
1694
        ||
1695 3
        !$check instanceof Result
1696
    ) {
1697
      return 0;
1698
    }
1699
1700 3
    return $check->num_rows;
1701
  }
1702
1703
  /**
1704
   * Pings a server connection, or tries to reconnect
1705
   * if the connection has gone down.
1706
   *
1707
   * @return bool
1708
   */
1709 9
  public function ping(): bool
1710
  {
1711 9
    if ($this->connected === false) {
1712 3
      return false;
1713
    }
1714
1715 6
    if ($this->isDoctrinePDOConnection() === true) {
1716
      return $this->doctrine_connection->ping();
1717
    }
1718
1719
    if (
1720 6
        $this->mysqli_link
1721
        &&
1722 6
        $this->mysqli_link instanceof \mysqli
1723
    ) {
1724 6
      return \mysqli_ping($this->mysqli_link);
1725
    }
1726
1727
    return false;
1728
  }
1729
1730
  /**
1731
   * Get a new "Prepare"-Object for your sql-query.
1732
   *
1733
   * @param string $query
1734
   *
1735
   * @return Prepare
1736
   */
1737 2
  public function prepare(string $query): Prepare
1738
  {
1739 2
    return new Prepare($this, $query);
1740
  }
1741
1742
  /**
1743
   * Execute a sql-query and return the result-array for select-statements.
1744
   *
1745
   * @param string $query
1746
   *
1747
   * @return mixed
1748
   * @deprecated
1749
   * @throws \Exception
1750
   */
1751 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...
1752
  {
1753 3
    $db = self::getInstance();
1754
1755 3
    $args = \func_get_args();
1756
    /** @noinspection SuspiciousAssignmentsInspection */
1757 3
    $query = \array_shift($args);
1758 3
    $query = \str_replace('?', '%s', $query);
1759 3
    $args = \array_map(
1760
        [
1761 3
            $db,
1762 3
            'escape',
1763
        ],
1764 3
        $args
1765
    );
1766 3
    \array_unshift($args, $query);
1767 3
    $query = \sprintf(...$args);
1768 3
    $result = $db->query($query);
1769
1770 3
    if ($result instanceof Result) {
1771 3
      return $result->fetchAllArray();
1772
    }
1773
1774 3
    return $result;
1775
  }
1776
1777
  /**
1778
   * Execute a sql-query.
1779
   *
1780
   * example:
1781
   * <code>
1782
   * $sql = "INSERT INTO TABLE_NAME_HERE
1783
   *   SET
1784
   *     foo = :foo,
1785
   *     bar = :bar
1786
   * ";
1787
   * $insert_id = $db->query(
1788
   *   $sql,
1789
   *   [
1790
   *     'foo' => 1.1,
1791
   *     'bar' => 1,
1792
   *   ]
1793
   * );
1794
   * </code>
1795
   *
1796
   * @param string     $sql               <p>The sql query-string.</p>
1797
   *
1798
   * @param array|bool $params            <p>
1799
   *                                      "array" of sql-query-parameters<br/>
1800
   *                                      "false" if you don't need any parameter (default)<br/>
1801
   *                                      </p>
1802
   *
1803
   * @return bool|int|Result              <p>
1804
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1805
   *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1806
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1807
   *                                      "true" by e.g. "DROP"-queries<br />
1808
   *                                      "false" on error
1809
   *                                      </p>
1810
   *
1811
   * @throws QueryException
1812
   */
1813 164
  public function query(string $sql = '', $params = false)
1814
  {
1815 164
    if (!$this->isReady()) {
1816
      return false;
1817
    }
1818
1819 164 View Code Duplication
    if (!$sql || $sql === '') {
1820 12
      $this->debug->displayError('Can not execute an empty query.', false);
1821
1822 12
      return false;
1823
    }
1824
1825
    if (
1826 158
        $params !== false
1827
        &&
1828 158
        \is_array($params)
1829
        &&
1830 158
        \count($params) > 0
1831
    ) {
1832 17
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1833 17
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1834 17
      $sql = $parseQueryParamsByName['sql'];
1835
    }
1836
1837
    // DEBUG
1838
    // var_dump($params);
1839
    // echo $sql . "\n";
1840
1841 158
    $query_start_time = \microtime(true);
1842 158
    $queryException = null;
1843 158
    $query_result_doctrine = false;
1844
1845 158
    if ($this->doctrine_connection) {
1846
1847
      try {
1848
        $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1849
        $query_result = $query_result_doctrine->execute();
1850
        $mysqli_field_count = $query_result_doctrine->columnCount();
1851
      } catch (\Exception $e) {
1852
        $query_result = false;
1853
        $mysqli_field_count = null;
1854
1855
        $queryException = $e;
1856
      }
1857
1858
    } else {
1859
1860 158
      $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1861 158
      $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1862
1863
    }
1864
1865 158
    $query_duration = \microtime(true) - $query_start_time;
1866
1867 158
    $this->query_count++;
1868
1869 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...
1870
1871 116
      if ($this->doctrine_connection) {
1872
1873
        $result = false;
1874
        if (
1875
            $query_result_doctrine
1876
            &&
1877
            $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1878
        ) {
1879
          $result = $query_result_doctrine;
1880
        }
1881
1882
      } else {
1883
1884 116
        $result = \mysqli_store_result($this->mysqli_link);
1885
1886
      }
1887
    } else {
1888 114
      $result = $query_result;
1889
    }
1890
1891
    if (
1892 158
        $result instanceof \Doctrine\DBAL\Statement
1893
        &&
1894 158
        $result->columnCount() > 0
1895
    ) {
1896
1897
      // log the select query
1898
      $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1899
1900
      // return query result object
1901
      return new Result($sql, $result);
1902
    }
1903
1904 158
    if ($result instanceof \mysqli_result) {
1905
1906
      // log the select query
1907 113
      $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1908
1909
      // return query result object
1910 113
      return new Result($sql, $result);
1911
    }
1912
1913 120
    if ($query_result === true) {
1914
1915
      // "INSERT" || "REPLACE"
1916 111
      if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1917
1918 104
        $insert_id = $this->insert_id();
1919
1920 104
        $this->debug->logQuery($sql, $query_duration, $insert_id);
1921
1922 104
        return $insert_id;
1923
      }
1924
1925
      // "UPDATE" || "DELETE"
1926 54
      if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1927
1928 28
        if ($this->mysqli_link) {
1929 28
          $this->affected_rows = $this->affected_rows();
1930
        } elseif ($query_result_doctrine) {
1931
          $this->affected_rows = $query_result_doctrine->rowCount();
1932
        }
1933
1934 28
        $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1935
1936 28
        return $this->affected_rows;
1937
      }
1938
1939
      // log the ? query
1940 27
      $this->debug->logQuery($sql, $query_duration, 0);
1941
1942 27
      return true;
1943
    }
1944
1945
    // log the error query
1946 33
    $this->debug->logQuery($sql, $query_duration, 0, true);
1947
1948 33
    if ($queryException) {
1949
      return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1950
    }
1951
1952 33
    if ($this->mysqli_link) {
1953 33
      return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
1954
    }
1955
1956
    return false;
1957
  }
1958
1959
  /**
1960
   * Error-handling for the sql-query.
1961
   *
1962
   * @param string     $errorMessage
1963
   * @param int        $errorNumber
1964
   * @param string     $sql
1965
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1966
   * @param bool       $sqlMultiQuery
1967
   *
1968
   * @return mixed|false
1969
   *
1970
   * @throws QueryException
1971
   * @throws DBGoneAwayException
1972
   */
1973 39
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1974
  {
1975
    if (
1976 39
        $errorMessage === 'DB server has gone away'
1977
        ||
1978 36
        $errorMessage === 'MySQL server has gone away'
1979
        ||
1980 39
        $errorNumber === 2006
1981
    ) {
1982 3
      static $RECONNECT_COUNTER;
1983
1984
      // exit if we have more then 3 "DB server has gone away"-errors
1985 3
      if ($RECONNECT_COUNTER > 3) {
1986
        $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1987
        throw new DBGoneAwayException($errorMessage);
1988
      }
1989
1990 3
      $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1991
1992
      // reconnect
1993 3
      $RECONNECT_COUNTER++;
1994 3
      $this->reconnect(true);
1995
1996
      // re-run the current (non multi) query
1997 3
      if ($sqlMultiQuery === false) {
1998 3
        return $this->query($sql, $sqlParams);
1999
      }
2000
2001
      return false;
2002
    }
2003
2004 36
    $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2005
2006 36
    $force_exception_after_error = null; // auto
2007 36
    if ($this->in_transaction === true) {
2008 12
      $force_exception_after_error = false;
2009
    }
2010
    // this query returned an error, we must display it (only for dev) !!!
2011
2012 36
    $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2013
2014 36
    return false;
2015
  }
2016
2017
  /**
2018
   * Quote && Escape e.g. a table name string.
2019
   *
2020
   * @param mixed $str
2021
   *
2022
   * @return string
2023
   */
2024 86
  public function quote_string($str): string
2025
  {
2026 86
    $str = \str_replace(
2027 86
        '`',
2028 86
        '``',
2029 86
        \trim(
2030 86
            (string)$this->escape($str, false),
2031 86
            '`'
2032
        )
2033
    );
2034
2035 86
    return '`' . $str . '`';
2036
  }
2037
2038
  /**
2039
   * Reconnect to the MySQL-Server.
2040
   *
2041
   * @param bool $checkViaPing
2042
   *
2043
   * @return bool
2044
   */
2045 7
  public function reconnect(bool $checkViaPing = false): bool
2046
  {
2047 7
    $ping = false;
2048 7
    if ($checkViaPing === true) {
2049 6
      $ping = $this->ping();
2050
    }
2051
2052 7
    if ($ping === false) {
2053 7
      $this->connected = false;
2054 7
      $this->connect();
2055
    }
2056
2057 7
    return $this->isReady();
2058
  }
2059
2060
  /**
2061
   * Execute a "replace"-query.
2062
   *
2063
   * @param string      $table
2064
   * @param array       $data
2065
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2066
   *
2067
   * @return false|int <p>false on error</p>
2068
   *
2069
   * @throws QueryException
2070
   */
2071 3
  public function replace(string $table, array $data = [], string $databaseName = null)
2072
  {
2073
    // init
2074 3
    $table = \trim($table);
2075
2076 3
    if ($table === '') {
2077 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2078
2079 3
      return false;
2080
    }
2081
2082 3
    if (\count($data) === 0) {
2083 3
      $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2084
2085 3
      return false;
2086
    }
2087
2088
    // extracting column names
2089 3
    $columns = \array_keys($data);
2090 3
    foreach ($columns as $k => $_key) {
2091
      /** @noinspection AlterInForeachInspection */
2092 3
      $columns[$k] = $this->quote_string($_key);
2093
    }
2094
2095 3
    $columns = \implode(',', $columns);
2096
2097
    // extracting values
2098 3
    foreach ($data as $k => $_value) {
2099
      /** @noinspection AlterInForeachInspection */
2100 3
      $data[$k] = $this->secure($_value);
2101
    }
2102 3
    $values = \implode(',', $data);
2103
2104 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...
2105
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2106
    }
2107
2108 3
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
2109
2110 3
    return $this->query($sql);
2111
  }
2112
2113
  /**
2114
   * Rollback in a transaction and end the transaction.
2115
   *
2116
   * @return bool <p>bool true on success, false otherwise.</p>
2117
   */
2118 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...
2119
  {
2120 12
    if ($this->in_transaction === false) {
2121
      $this->debug->displayError('Error: mysql server is not in transaction!', false);
2122
2123
      return false;
2124
    }
2125
2126 12
    if ($this->mysqli_link) {
2127 12
      $return = \mysqli_rollback($this->mysqli_link);
2128 12
      \mysqli_autocommit($this->mysqli_link, true);
2129
    } elseif ($this->isDoctrinePDOConnection() === true) {
2130
      $this->doctrine_connection->rollBack();
2131
      $this->doctrine_connection->setAutoCommit(true);
2132
2133
      if ($this->doctrine_connection->isAutoCommit() === true) {
2134
        $return = true;
2135
      } else {
2136
        $return = false;
2137
      }
2138
    }
2139
2140 12
    $this->in_transaction = false;
2141
2142 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...
2143
  }
2144
2145
  /**
2146
   * Try to secure a variable, so can you use it in sql-queries.
2147
   *
2148
   * <p>
2149
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
2150
   * 1. parse into "int"
2151
   * </p><br />
2152
   *
2153
   * <p>
2154
   * <strong>float:</strong><br />
2155
   * 1. return "float"
2156
   * </p><br />
2157
   *
2158
   * <p>
2159
   * <strong>string:</strong><br />
2160
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2161
   * 2. trim '<br />
2162
   * 3. escape the string (and remove non utf-8 chars)<br />
2163
   * 4. trim ' again (because we maybe removed some chars)<br />
2164
   * 5. add ' around the new string<br />
2165
   * </p><br />
2166
   *
2167
   * <p>
2168
   * <strong>array:</strong><br />
2169
   * 1. return null
2170
   * </p><br />
2171
   *
2172
   * <p>
2173
   * <strong>object:</strong><br />
2174
   * 1. return false
2175
   * </p><br />
2176
   *
2177
   * <p>
2178
   * <strong>null:</strong><br />
2179
   * 1. return null
2180
   * </p>
2181
   *
2182
   * @param mixed     $var
2183
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2184
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2185
   *                                 <strong>null</strong> => Convert the array into null, every time.
2186
   *
2187
   * @return mixed
2188
   */
2189 97
  public function secure($var, $convert_array = true)
2190
  {
2191 97
    if (\is_array($var)) {
2192 6
      if ($convert_array === null) {
2193
2194
        if ($this->convert_null_to_empty_string === true) {
2195
          $var = "''";
2196
        } else {
2197
          $var = 'NULL';
2198
        }
2199
2200
      } else {
2201
2202 6
        $varCleaned = [];
2203 6
        foreach ((array)$var as $key => $value) {
2204
2205 6
          $key = $this->escape($key, false, false, $convert_array);
2206 6
          $value = $this->secure($value);
2207
2208
          /** @noinspection OffsetOperationsInspection */
2209 6
          $varCleaned[$key] = $value;
2210
        }
2211
2212 6 View Code Duplication
        if ($convert_array === true) {
2213 6
          $varCleaned = \implode(',', $varCleaned);
2214
2215 6
          $var = $varCleaned;
2216
        } else {
2217
          $var = $varCleaned;
2218
        }
2219
2220
      }
2221
2222 6
      return $var;
2223
    }
2224
2225 97
    if ($var === '') {
2226 6
      return "''";
2227
    }
2228
2229 97
    if ($var === "''") {
2230 3
      return "''";
2231
    }
2232
2233 97
    if ($var === null) {
2234 4
      if ($this->convert_null_to_empty_string === true) {
2235 3
        return "''";
2236
      }
2237
2238 4
      return 'NULL';
2239
    }
2240
2241 96
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2242 3
      return $var;
2243
    }
2244
2245 96
    if (\is_string($var)) {
2246 83
      $var = \trim($var, "'");
2247
    }
2248
2249 96
    $var = $this->escape($var, false, false, null);
2250
2251 93
    if (\is_string($var)) {
2252 83
      $var = "'" . \trim($var, "'") . "'";
2253
    }
2254
2255 93
    return $var;
2256
  }
2257
2258
  /**
2259
   * Execute a "select"-query.
2260
   *
2261
   * @param string       $table
2262
   * @param string|array $where
2263
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2264
   *
2265
   * @return false|Result <p>false on error</p>
2266
   *
2267
   * @throws QueryException
2268
   */
2269 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...
2270
  {
2271
    // init
2272 62
    $table = \trim($table);
2273
2274 62
    if ($table === '') {
2275 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2276
2277 3
      return false;
2278
    }
2279
2280 62
    if (\is_string($where)) {
2281 24
      $WHERE = $this->escape($where, false);
2282 41
    } elseif (\is_array($where)) {
2283 41
      $WHERE = $this->_parseArrayPair($where, 'AND');
2284
    } else {
2285 3
      $WHERE = '';
2286
    }
2287
2288 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...
2289
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2290
    }
2291
2292 62
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
2293
2294 62
    return $this->query($sql);
2295
  }
2296
2297
  /**
2298
   * Selects a different database than the one specified on construction.
2299
   *
2300
   * @param string $database <p>Database name to switch to.</p>
2301
   *
2302
   * @return bool <p>bool true on success, false otherwise.</p>
2303
   */
2304
  public function select_db(string $database): bool
2305
  {
2306
    if (!$this->isReady()) {
2307
      return false;
2308
    }
2309
2310
    if ($this->mysqli_link) {
2311
      return \mysqli_select_db($this->mysqli_link, $database);
2312
    }
2313
2314
    if ($this->isDoctrinePDOConnection()) {
2315
      return $this->query('use :database', ['database' => $database]);
2316
    }
2317
2318
    return false;
2319
  }
2320
2321
  /**
2322
   * @param array $extra_config   <p>
2323
   *                              'session_to_db' => false|true<br>
2324
   *                              'socket' => 'string (path)'<br>
2325
   *                              'ssl' => 'bool'<br>
2326
   *                              'clientkey' => 'string (path)'<br>
2327
   *                              'clientcert' => 'string (path)'<br>
2328
   *                              'cacert' => 'string (path)'<br>
2329
   *                              </p>
2330
   */
2331 23
  public function setConfigExtra(array $extra_config)
2332
  {
2333 23
    if (isset($extra_config['session_to_db'])) {
2334
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
2335
    }
2336
2337 23
    if (isset($extra_config['doctrine'])) {
2338
      if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2339
        $this->doctrine_connection = $extra_config['doctrine'];
2340
      } else {
2341
        throw new DBConnectException('Error "doctrine"-connection is not valid');
2342
      }
2343
    }
2344
2345 23
    if (isset($extra_config['socket'])) {
2346
      $this->socket = $extra_config['socket'];
2347
    }
2348
2349 23
    if (isset($extra_config['ssl'])) {
2350
      $this->ssl = $extra_config['ssl'];
2351
    }
2352
2353 23
    if (isset($extra_config['clientkey'])) {
2354
      $this->clientkey = $extra_config['clientkey'];
2355
    }
2356
2357 23
    if (isset($extra_config['clientcert'])) {
2358
      $this->clientcert = $extra_config['clientcert'];
2359
    }
2360
2361 23
    if (isset($extra_config['cacert'])) {
2362
      $this->cacert = $extra_config['cacert'];
2363
    }
2364 23
  }
2365
2366
  /**
2367
   * Set the current charset.
2368
   *
2369
   * @param string $charset
2370
   *
2371
   * @return bool
2372
   */
2373 14
  public function set_charset(string $charset): bool
2374
  {
2375 14
    $charsetLower = strtolower($charset);
2376 14
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2377 8
      $charset = 'utf8';
2378
    }
2379 14
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
2380 8
      $charset = 'utf8mb4';
2381
    }
2382
2383 14
    $this->charset = $charset;
2384
2385
    if (
2386 14
        $this->mysqli_link
2387
        &&
2388 14
        $this->mysqli_link instanceof \mysqli
2389
    ) {
2390
2391 14
      $return = mysqli_set_charset($this->mysqli_link, $charset);
2392
2393
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
2394 14
      @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2395
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
2396 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...
2397
2398
    } elseif ($this->isDoctrinePDOConnection() === true) {
2399
2400
      $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2401
2402
      $doctrineWrappedConnection->exec('SET CHARACTER SET ' . $charset);
2403
      $doctrineWrappedConnection->exec("SET NAMES '" . $charset . "'");
2404
2405
      $return = true;
2406
2407
    } else {
2408
2409
      throw new DBConnectException('Can not set the charset');
2410
2411
    }
2412
2413 14
    return $return;
2414
  }
2415
2416
  /**
2417
   * Set the option to convert null to "''" (empty string).
2418
   *
2419
   * Used in secure() => select(), insert(), update(), delete()
2420
   *
2421
   * @deprecated It's not recommended to convert NULL into an empty string!
2422
   *
2423
   * @param bool $bool
2424
   *
2425
   * @return self
2426
   */
2427 3
  public function set_convert_null_to_empty_string(bool $bool): self
2428
  {
2429 3
    $this->convert_null_to_empty_string = $bool;
2430
2431 3
    return $this;
2432
  }
2433
2434
  /**
2435
   * Enables or disables internal report functions
2436
   *
2437
   * @link http://php.net/manual/en/function.mysqli-report.php
2438
   *
2439
   * @param int $flags <p>
2440
   *                   <table>
2441
   *                   Supported flags
2442
   *                   <tr valign="top">
2443
   *                   <td>Name</td>
2444
   *                   <td>Description</td>
2445
   *                   </tr>
2446
   *                   <tr valign="top">
2447
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2448
   *                   <td>Turns reporting off</td>
2449
   *                   </tr>
2450
   *                   <tr valign="top">
2451
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2452
   *                   <td>Report errors from mysqli function calls</td>
2453
   *                   </tr>
2454
   *                   <tr valign="top">
2455
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2456
   *                   <td>
2457
   *                   Throw <b>mysqli_sql_exception</b> for errors
2458
   *                   instead of warnings
2459
   *                   </td>
2460
   *                   </tr>
2461
   *                   <tr valign="top">
2462
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2463
   *                   <td>Report if no index or bad index was used in a query</td>
2464
   *                   </tr>
2465
   *                   <tr valign="top">
2466
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2467
   *                   <td>Set all options (report all)</td>
2468
   *                   </tr>
2469
   *                   </table>
2470
   *                   </p>
2471
   *
2472
   * @return bool
2473
   */
2474
  public function set_mysqli_report(int $flags): bool
2475
  {
2476
    if (
2477
        $this->mysqli_link
2478
        &&
2479
        $this->mysqli_link instanceof \mysqli
2480
    ) {
2481
      return \mysqli_report($flags);
2482
    }
2483
2484
    return false;
2485
  }
2486
2487
  /**
2488
   * Show config errors by throw exceptions.
2489
   *
2490
   * @return bool
2491
   *
2492
   * @throws \InvalidArgumentException
2493
   */
2494 23
  public function showConfigError(): bool
2495
  {
2496
    // check if a doctrine connection is already open, first
2497
    if (
2498 23
        $this->doctrine_connection
2499
        &&
2500 23
        $this->doctrine_connection->isConnected()
2501
    ) {
2502
      return true;
2503
    }
2504
2505
    if (
2506 23
        !$this->hostname
2507
        ||
2508 20
        !$this->username
2509
        ||
2510 23
        !$this->database
2511
    ) {
2512
2513 9
      if (!$this->hostname) {
2514 3
        throw new \InvalidArgumentException('no-sql-hostname');
2515
      }
2516
2517 6
      if (!$this->username) {
2518 3
        throw new \InvalidArgumentException('no-sql-username');
2519
      }
2520
2521 3
      if (!$this->database) {
2522 3
        throw new \InvalidArgumentException('no-sql-database');
2523
      }
2524
2525
      return false;
2526
    }
2527
2528 14
    return true;
2529
  }
2530
2531
  /**
2532
   * alias: "beginTransaction()"
2533
   */
2534 3
  public function startTransaction(): bool
2535
  {
2536 3
    return $this->beginTransaction();
2537
  }
2538
2539
  /**
2540
   * Determine if database table exists
2541
   *
2542
   * @param string $table
2543
   *
2544
   * @return bool
2545
   */
2546 3
  public function table_exists(string $table): bool
2547
  {
2548 3
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2549
2550 3
    return $check !== false
2551
           &&
2552 3
           $check instanceof Result
2553
           &&
2554 3
           $check->num_rows > 0;
2555
  }
2556
2557
  /**
2558
   * Execute a callback inside a transaction.
2559
   *
2560
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2561
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2562
   *
2563
   * @return bool <p>bool true on success, false otherwise.</p>
2564
   */
2565 3
  public function transact($callback): bool
2566
  {
2567
    try {
2568
2569 3
      $beginTransaction = $this->beginTransaction();
2570 3
      if ($beginTransaction === false) {
2571 3
        $this->debug->displayError('Error: transact -> can not start transaction!', false);
2572
2573 3
        return false;
2574
      }
2575
2576 3
      $result = $callback($this);
2577 3
      if ($result === false) {
2578
        /** @noinspection ThrowRawExceptionInspection */
2579 3
        throw new \Exception('call_user_func [' . $callback . '] === false');
2580
      }
2581
2582 3
      return $this->commit();
2583
2584 3
    } catch (\Exception $e) {
2585
2586 3
      $this->rollback();
2587
2588 3
      return false;
2589
    }
2590
  }
2591
2592
  /**
2593
   * Execute a "update"-query.
2594
   *
2595
   * @param string       $table
2596
   * @param array        $data
2597
   * @param array|string $where
2598
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2599
   *
2600
   * @return false|int <p>false on error</p>
2601
   *
2602
   * @throws QueryException
2603
   */
2604 21
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2605
  {
2606
    // init
2607 21
    $table = \trim($table);
2608
2609 21
    if ($table === '') {
2610 3
      $this->debug->displayError('Invalid table name, table name in empty.', false);
2611
2612 3
      return false;
2613
    }
2614
2615 21
    if (\count($data) === 0) {
2616 6
      $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2617
2618 6
      return false;
2619
    }
2620
2621
    // DEBUG
2622
    //var_dump($data);
2623
2624 21
    $SET = $this->_parseArrayPair($data);
2625
2626
    // DEBUG
2627
    //var_dump($SET);
2628
2629 21
    if (\is_string($where)) {
2630 6
      $WHERE = $this->escape($where, false);
2631 18
    } elseif (\is_array($where)) {
2632 15
      $WHERE = $this->_parseArrayPair($where, 'AND');
2633
    } else {
2634 3
      $WHERE = '';
2635
    }
2636
2637 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...
2638
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2639
    }
2640
2641 21
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2642
2643 21
    return $this->query($sql);
2644
  }
2645
2646
}
2647