Completed
Push — master ( a781c6...24e936 )
by Lars
06:09
created

DB::set_convert_null_to_empty_string()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

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

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

Loading history...
607
  {
608 12
    if ($this->_in_transaction === true) {
609 4
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
610
611 4
      return false;
612
    }
613
614 12
    $this->clearErrors(); // needed for "$this->endTransaction()"
615 12
    $this->_in_transaction = true;
616 12
    $return = \mysqli_autocommit($this->link, false);
617 12
    if ($return === false) {
618
      $this->_in_transaction = false;
619
    }
620
621 12
    return $return;
622
  }
623
624
  /**
625
   * Clear the errors in "_debug->_errors".
626
   *
627
   * @return bool
628
   */
629 12
  public function clearErrors(): bool
630
  {
631 12
    return $this->_debug->clearErrors();
632
  }
633
634
  /**
635
   * Closes a previously opened database connection.
636
   */
637 4
  public function close(): bool
638
  {
639 4
    $this->connected = false;
640
641 4
    if ($this->_doctrine_connection) {
642
643 2
      $connectedBefore = $this->_doctrine_connection->isConnected();
644
645 2
      $this->_doctrine_connection->close();
646 2
      $this->link = null;
647
648 2
      return ($connectedBefore === true ? !$this->_doctrine_connection->isConnected() : false);
649
    }
650
651
    if (
652 2
        $this->link
653
        &&
654 2
        $this->link instanceof \mysqli
655
    ) {
656 2
      $result = \mysqli_close($this->link);
657 2
      $this->link = null;
658
659 2
      return $result;
660
    }
661
662 1
    $this->link = null;
663
664 1
    return false;
665
  }
666
667
  /**
668
   * Commits the current transaction and end the transaction.
669
   *
670
   * @return bool <p>Boolean true on success, false otherwise.</p>
671
   */
672 4 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...
673
  {
674 4
    if ($this->_in_transaction === false) {
675
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
676
677
      return false;
678
    }
679
680 4
    $return = \mysqli_commit($this->link);
681 4
    \mysqli_autocommit($this->link, true);
682 4
    $this->_in_transaction = false;
683
684 4
    return $return;
685
  }
686
687
  /**
688
   * Open a new connection to the MySQL server.
689
   *
690
   * @return bool
691
   *
692
   * @throws DBConnectException
693
   */
694 64
  public function connect(): bool
695
  {
696 64
    if ($this->isReady()) {
697 2
      return true;
698
    }
699
700 64
    if ($this->_doctrine_connection) {
701 54
      $this->_doctrine_connection->connect();
702
703 54
      if (method_exists($this->_doctrine_connection, 'getWrappedConnection')) {
704 54
        $doctrineMySQLi = $this->_doctrine_connection->getWrappedConnection();
705 54
        if ($doctrineMySQLi instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
706 54
          $this->link = $doctrineMySQLi->getWrappedResourceHandle();
707
708 54
          $this->connected = $this->_doctrine_connection->isConnected();
709
710 54
          if (!$this->connected) {
711
            $error = 'Error connecting to mysql server: ' . $this->_doctrine_connection->errorInfo();
712
            $this->_debug->displayError($error, false);
713
            throw new DBConnectException($error, 101);
714
          }
715
716 54
          $this->set_charset($this->charset);
717
718 54
          return $this->isReady();
719
        }
720
      }
721
    }
722
723 14
    $flags = null;
724
725 14
    \mysqli_report(MYSQLI_REPORT_STRICT);
726
    try {
727 14
      $this->link = \mysqli_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like \mysqli_init() of type object<mysql> is incompatible with the declared type object<mysqli>|null of property $link.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
728
729 14
      if (Helper::isMysqlndIsUsed() === true) {
730 14
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
731
      }
732
733 14
      if ($this->_ssl === true) {
734
735
        if (empty($this->clientcert)) {
0 ignored issues
show
Bug introduced by
The property clientcert does not seem to exist. Did you mean _clientcert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
736
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
737
        }
738
739
        if (empty($this->clientkey)) {
0 ignored issues
show
Bug introduced by
The property clientkey does not seem to exist. Did you mean _clientkey?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
740
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
741
        }
742
743
        if (empty($this->cacert)) {
0 ignored issues
show
Bug introduced by
The property cacert does not seem to exist. Did you mean _cacert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
744
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
745
        }
746
747
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
748
749
        /** @noinspection PhpParamsInspection */
750
        \mysqli_ssl_set(
751
            $this->link,
752
            $this->_clientkey,
753
            $this->_clientcert,
754
            $this->_cacert,
755
            null,
756
            null
757
        );
758
759
        $flags = MYSQLI_CLIENT_SSL;
760
      }
761
762
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
763 14
      $this->connected = @\mysqli_real_connect(
764 14
          $this->link,
765 14
          $this->hostname,
766 14
          $this->username,
767 14
          $this->password,
768 14
          $this->database,
769 14
          $this->port,
770 14
          $this->socket,
771 14
          (int)$flags
772
      );
773
774 6
    } catch (\Exception $e) {
775 6
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
776 6
      $this->_debug->displayError($error, false);
777 6
      throw new DBConnectException($error, 100, $e);
778
    }
779 8
    \mysqli_report(MYSQLI_REPORT_OFF);
780
781 8
    $errno = \mysqli_connect_errno();
782 8
    if (!$this->connected || $errno) {
783
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
784
      $this->_debug->displayError($error, false);
785
      throw new DBConnectException($error, 101);
786
    }
787
788 8
    $this->set_charset($this->charset);
789
790 8
    return $this->isReady();
791
  }
792
793
  /**
794
   * Execute a "delete"-query.
795
   *
796
   * @param string       $table
797
   * @param string|array $where
798
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
799
   *
800
   * @return false|int <p>false on error</p>
801
   *
802
   * @throws QueryException
803
   */
804 3 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...
805
  {
806
    // init
807 3
    $table = \trim($table);
808
809 3
    if ($table === '') {
810 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
811
812 2
      return false;
813
    }
814
815 3
    if (\is_string($where)) {
816 2
      $WHERE = $this->escape($where, false);
817 3
    } elseif (\is_array($where)) {
818 3
      $WHERE = $this->_parseArrayPair($where, 'AND');
819
    } else {
820 2
      $WHERE = '';
821
    }
822
823 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...
824
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
825
    }
826
827 3
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
828
829 3
    return $this->query($sql);
830
  }
831
832
  /**
833
   * Ends a transaction and commits if no errors, then ends autocommit.
834
   *
835
   * @return bool <p>This will return true or false indicating success of transactions.</p>
836
   */
837 8 View Code Duplication
  public function endTransaction(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
838
  {
839 8
    if ($this->_in_transaction === false) {
840
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
841
842
      return false;
843
    }
844
845 8
    if (!$this->errors()) {
846 2
      $return = \mysqli_commit($this->link);
847
    } else {
848 6
      $this->rollback();
849 6
      $return = false;
850
    }
851
852 8
    \mysqli_autocommit($this->link, true);
853 8
    $this->_in_transaction = false;
854
855 8
    return $return;
856
  }
857
858
  /**
859
   * Get all errors from "$this->_errors".
860
   *
861
   * @return array|false <p>false === on errors</p>
862
   */
863 8
  public function errors()
864
  {
865 8
    $errors = $this->_debug->getErrors();
866
867 8
    return \count($errors) > 0 ? $errors : false;
868
  }
869
870
  /**
871
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
872
   *
873
   * @param mixed     $var           boolean: convert into "integer"<br />
874
   *                                 int: int (don't change it)<br />
875
   *                                 float: float (don't change it)<br />
876
   *                                 null: null (don't change it)<br />
877
   *                                 array: run escape() for every key => value<br />
878
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
879
   * @param bool      $stripe_non_utf8
880
   * @param bool      $html_entity_decode
881
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
882
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
883
   *                                 <strong>null</strong> => Convert the array into null, every time.
884
   *
885
   * @return mixed
886
   */
887 87
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
888
  {
889
    // [empty]
890 87
    if ($var === '') {
891 4
      return '';
892
    }
893
894
    // ''
895 87
    if ($var === "''") {
896
      return "''";
897
    }
898
899
    // check the type
900 87
    $type = gettype($var);
901
902 87
    if ($type === 'object') {
903 6
      if ($var instanceof \DateTime) {
904 6
        $var = $var->format('Y-m-d H:i:s');
905 6
        $type = 'string';
906 4
      } elseif (\method_exists($var, '__toString')) {
907 4
        $var = (string)$var;
908 4
        $type = 'string';
909
      }
910
    }
911
912 87
    switch ($type) {
913 87
      case 'boolean':
914 6
        $var = (int)$var;
915 6
        break;
916
917 87
      case 'double':
918 87
      case 'integer':
919 51
        break;
920
921 82
      case 'string':
922 82
        if ($stripe_non_utf8 === true) {
923 17
          $var = UTF8::cleanup($var);
924
        }
925
926 82
        if ($html_entity_decode === true) {
927 2
          $var = UTF8::html_entity_decode($var);
928
        }
929
930 82
        $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
931
932 82
        $var = \mysqli_real_escape_string($this->link, $var);
933
934 82
        break;
935
936 8
      case 'array':
937 8
        if ($convert_array === null) {
938
939 6
          if ($this->_convert_null_to_empty_string === true) {
940
            $var = "''";
941
          } else {
942 6
            $var = 'NULL';
943
          }
944
945
        } else {
946
947 4
          $varCleaned = [];
948 4
          foreach ((array)$var as $key => $value) {
949
950 4
            $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
951 4
            $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
952
953
            /** @noinspection OffsetOperationsInspection */
954 4
            $varCleaned[$key] = $value;
955
          }
956
957 4
          if ($convert_array === true) {
958 2
            $varCleaned = \implode(',', $varCleaned);
959
960 2
            $var = $varCleaned;
961
          } else {
962 4
            $var = $varCleaned;
963
          }
964
965
        }
966 8
        break;
967
968 6
      case 'NULL':
969 4
        if ($this->_convert_null_to_empty_string === true) {
970
          $var = "''";
971
        } else {
972 4
          $var = 'NULL';
973
        }
974 4
        break;
975
976
      default:
977 4
        throw new \InvalidArgumentException(sprintf('Not supported value "%s" of type %s.', print_r($var, true), $type));
978
        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...
979
    }
980
981 87
    return $var;
982
  }
983
984
  /**
985
   * Execute select/insert/update/delete sql-queries.
986
   *
987
   * @param string  $query    <p>sql-query</p>
988
   * @param bool    $useCache optional <p>use cache?</p>
989
   * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
990
   * @param DB|null $db       optional <p>the database connection</p>
991
   *
992
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
993
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
994
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
995
   *               "true" by e.g. "DROP"-queries<br />
996
   *               "false" on error
997
   *
998
   * @throws QueryException
999
   */
1000 6
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1001
  {
1002
    // init
1003 6
    $cacheKey = null;
1004 6
    if (!$db) {
1005 6
      $db = self::getInstance();
1006
    }
1007
1008 6 View Code Duplication
    if ($useCache === true) {
1009 2
      $cache = new Cache(null, null, false, $useCache);
1010 2
      $cacheKey = 'sql-' . \md5($query);
1011
1012
      if (
1013 2
          $cache->getCacheIsReady() === true
1014
          &&
1015 2
          $cache->existsItem($cacheKey)
1016
      ) {
1017 2
        return $cache->getItem($cacheKey);
1018
      }
1019
1020
    } else {
1021 6
      $cache = false;
1022
    }
1023
1024 6
    $result = $db->query($query);
1025
1026 6
    if ($result instanceof Result) {
1027
1028 2
      $return = $result->fetchAllArray();
1029
1030
      // save into the cache
1031 View Code Duplication
      if (
1032 2
          $cacheKey !== null
1033
          &&
1034 2
          $useCache === true
1035
          &&
1036 2
          $cache instanceof Cache
1037
          &&
1038 2
          $cache->getCacheIsReady() === true
1039
      ) {
1040 2
        $cache->setItem($cacheKey, $return, $cacheTTL);
1041
      }
1042
1043
    } else {
1044 4
      $return = $result;
1045
    }
1046
1047 6
    return $return;
1048
  }
1049
1050
  /**
1051
   * Get all table-names via "SHOW TABLES".
1052
   *
1053
   * @return array
1054
   */
1055 2
  public function getAllTables(): array
1056
  {
1057 2
    $query = 'SHOW TABLES';
1058 2
    $result = $this->query($query);
1059
1060 2
    return $result->fetchAllArray();
1061
  }
1062
1063
  /**
1064
   * @return Debug
1065
   */
1066 9
  public function getDebugger(): Debug
1067
  {
1068 9
    return $this->_debug;
1069
  }
1070
1071
  /**
1072
   * @return \Doctrine\DBAL\Connection|null
1073
   */
1074
  public function getDoctrineConnection()
1075
  {
1076
    return $this->_doctrine_connection;
1077
  }
1078
1079
  /**
1080
   * Get errors from "$this->_errors".
1081
   *
1082
   * @return array
1083
   */
1084
  public function getErrors(): array
1085
  {
1086
    return $this->_debug->getErrors();
1087
  }
1088
1089
  /**
1090
   * getInstance()
1091
   *
1092
   * @param string $hostname
1093
   * @param string $username
1094
   * @param string $password
1095
   * @param string $database
1096
   * @param int    $port                 <p>default is (int)3306</p>
1097
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1098
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1099
   *                                     Use false to disable it.</p>
1100
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1101
   *                                     Use false to disable it.</p>
1102
   * @param string $logger_class_name
1103
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1104
   * @param array  $extra_config         <p>
1105
   *                                     're_connect'    => bool<br>
1106
   *                                     'session_to_db' => bool<br>
1107
   *                                     'doctrine'      => \Doctrine\DBAL\Connection<br>
1108
   *                                     'socket'        => 'string (path)'<br>
1109
   *                                     'ssl'           => bool<br>
1110
   *                                     'clientkey'     => 'string (path)'<br>
1111
   *                                     'clientcert'    => 'string (path)'<br>
1112
   *                                     'cacert'        => 'string (path)'<br>
1113
   *                                     </p>
1114
   *
1115
   * @return self
1116
   */
1117 150
  public static function getInstance(
1118
      string $hostname = '',
1119
      string $username = '',
1120
      string $password = '',
1121
      string $database = '',
1122
      $port = 3306,
1123
      string $charset = 'utf8',
1124
      bool $exit_on_error = true,
1125
      bool $echo_on_error = true,
1126
      string $logger_class_name = '',
1127
      string $logger_level = '',
1128
      array $extra_config = []
1129
  ): self
1130
  {
1131
    /**
1132
     * @var $instance self[]
1133
     */
1134 150
    static $instance = [];
1135
1136
    /**
1137
     * @var $firstInstance self
1138
     */
1139 150
    static $firstInstance = null;
1140
1141
    // fallback
1142 150
    if (!$charset) {
1143 65
      $charset = 'utf8';
1144
    }
1145
1146
    if (
1147 150
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1148
        &&
1149 150
        null !== $firstInstance
1150
    ) {
1151 21
      if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1152
        $firstInstance->reconnect(true);
1153
      }
1154
1155 21
      return $firstInstance;
1156
    }
1157
1158 150
    $extra_config_string = '';
1159 150
    if (\is_array($extra_config) === true) {
1160 150
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1161 54
        if (\is_object($extra_config_value)) {
1162 54
          $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1163
        } else {
1164
          $extra_config_value_tmp = (string)$extra_config_value;
1165
        }
1166 150
        $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1167
      }
1168
    } else {
1169
      // only for backward compatibility
1170
      $extra_config_string = (int)$extra_config;
1171
    }
1172
1173 150
    $connection = \md5(
1174 150
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1175
    );
1176
1177 150
    if (!isset($instance[$connection])) {
1178 65
      $instance[$connection] = new self(
1179 65
          $hostname,
1180 65
          $username,
1181 65
          $password,
1182 65
          $database,
1183 65
          $port,
1184 65
          $charset,
1185 65
          $exit_on_error,
1186 65
          $echo_on_error,
1187 65
          $logger_class_name,
1188 65
          $logger_level,
1189 65
          $extra_config
1190
      );
1191
1192 59
      if (null === $firstInstance) {
1193 1
        $firstInstance = $instance[$connection];
1194
      }
1195
    }
1196
1197 150
    if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1198
      $instance[$connection]->reconnect(true);
1199
    }
1200
1201 150
    return $instance[$connection];
1202
  }
1203
1204
  /**
1205
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1206
   *
1207
   * @return \mysqli
1208
   */
1209 38
  public function getLink(): \mysqli
1210
  {
1211 38
    return $this->link;
1212
  }
1213
1214
  /**
1215
   * Get the current charset.
1216
   *
1217
   * @return string
1218
   */
1219 2
  public function get_charset(): string
1220
  {
1221 2
    return $this->charset;
1222
  }
1223
1224
  /**
1225
   * Check if we are in a transaction.
1226
   *
1227
   * @return bool
1228
   */
1229
  public function inTransaction(): bool
1230
  {
1231
    return $this->_in_transaction;
1232
  }
1233
1234
  /**
1235
   * Execute a "insert"-query.
1236
   *
1237
   * @param string      $table
1238
   * @param array       $data
1239
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1240
   *
1241
   * @return false|int <p>false on error</p>
1242
   *
1243
   * @throws QueryException
1244
   */
1245 51
  public function insert(string $table, array $data = [], string $databaseName = null)
1246
  {
1247
    // init
1248 51
    $table = \trim($table);
1249
1250 51
    if ($table === '') {
1251 4
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1252
1253 4
      return false;
1254
    }
1255
1256 49
    if (\count($data) === 0) {
1257 6
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1258
1259 6
      return false;
1260
    }
1261
1262 45
    $SET = $this->_parseArrayPair($data);
1263
1264 45
    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...
1265
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1266
    }
1267
1268 45
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET";
1269
1270 45
    return $this->query($sql);
1271
  }
1272
1273
  /**
1274
   * Returns the auto generated id used in the last query.
1275
   *
1276
   * @return int|string
1277
   */
1278 81
  public function insert_id()
1279
  {
1280 81
    return \mysqli_insert_id($this->link);
1281
  }
1282
1283
  /**
1284
   * Check if db-connection is ready.
1285
   *
1286
   * @return boolean
1287
   */
1288 160
  public function isReady(): bool
1289
  {
1290 160
    return $this->connected ? true : false;
1291
  }
1292
1293
  /**
1294
   * Get the last sql-error.
1295
   *
1296
   * @return string|false <p>false === there was no error</p>
1297
   */
1298
  public function lastError()
1299
  {
1300
    $errors = $this->_debug->getErrors();
1301
1302
    return \count($errors) > 0 ? end($errors) : false;
1303
  }
1304
1305
  /**
1306
   * Execute a sql-multi-query.
1307
   *
1308
   * @param string $sql
1309
   *
1310
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1311
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1312
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1313
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1314
   *
1315
   * @throws QueryException
1316
   */
1317 2
  public function multi_query(string $sql)
1318
  {
1319 2
    if (!$this->isReady()) {
1320
      return false;
1321
    }
1322
1323 2 View Code Duplication
    if (!$sql || $sql === '') {
1324 2
      $this->_debug->displayError('Can not execute an empty query.', false);
1325
1326 2
      return false;
1327
    }
1328
1329 2
    $query_start_time = microtime(true);
1330 2
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1331 2
    $query_duration = microtime(true) - $query_start_time;
1332
1333 2
    $this->_debug->logQuery($sql, $query_duration, 0);
1334
1335 2
    $returnTheResult = false;
1336 2
    $result = [];
1337
1338 2
    if ($resultTmp) {
1339
      do {
1340
1341 2
        $resultTmpInner = \mysqli_store_result($this->link);
1342
1343 2
        if ($resultTmpInner instanceof \mysqli_result) {
1344
1345 2
          $returnTheResult = true;
1346 2
          $result[] = new Result($sql, $resultTmpInner);
1347
1348
        } else {
1349
1350
          // is the query successful
1351 2
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1352 2
            $result[] = true;
1353
          } else {
1354
            $result[] = false;
1355
          }
1356
1357
        }
1358
1359 2
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1360
1361
    } else {
1362
1363
      // log the error query
1364 2
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1365
1366 2
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1367
    }
1368
1369
    // return the result only if there was a "SELECT"-query
1370 2
    if ($returnTheResult === true) {
1371 2
      return $result;
1372
    }
1373
1374
    if (
1375 2
        \count($result) > 0
1376
        &&
1377 2
        \in_array(false, $result, true) === false
1378
    ) {
1379 2
      return true;
1380
    }
1381
1382
    return false;
1383
  }
1384
1385
  /**
1386
   * Count number of rows found matching a specific query.
1387
   *
1388
   * @param string $query
1389
   *
1390
   * @return int
1391
   */
1392 2
  public function num_rows(string $query): int
1393
  {
1394 2
    $check = $this->query($query);
1395
1396
    if (
1397 2
        $check === false
1398
        ||
1399 2
        !$check instanceof Result
1400
    ) {
1401
      return 0;
1402
    }
1403
1404 2
    return $check->num_rows;
1405
  }
1406
1407
  /**
1408
   * Pings a server connection, or tries to reconnect
1409
   * if the connection has gone down.
1410
   *
1411
   * @return boolean
1412
   */
1413 6
  public function ping(): bool
1414
  {
1415 6
    if ($this->_doctrine_connection) {
1416
1417
      // this check is needed, but I don't know why "ping" isn't working?
1418 3
      if (!$this->_doctrine_connection->isConnected()) {
1419 1
        return false;
1420
      }
1421
1422 2
      return $this->_doctrine_connection->ping();
1423
    }
1424
1425
    if (
1426 3
        $this->link
1427
        &&
1428 3
        $this->link instanceof \mysqli
1429
    ) {
1430
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1431 2
      return (bool)@\mysqli_ping($this->link);
1432
    }
1433
1434 1
    return false;
1435
  }
1436
1437
  /**
1438
   * Get a new "Prepare"-Object for your sql-query.
1439
   *
1440
   * @param string $query
1441
   *
1442
   * @return Prepare
1443
   */
1444 2
  public function prepare(string $query): Prepare
1445
  {
1446 2
    return new Prepare($this, $query);
1447
  }
1448
1449
  /**
1450
   * Execute a sql-query and return the result-array for select-statements.
1451
   *
1452
   * @param string $query
1453
   *
1454
   * @return mixed
1455
   * @deprecated
1456
   * @throws \Exception
1457
   */
1458 2
  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...
1459
  {
1460 2
    $db = self::getInstance();
1461
1462 2
    $args = \func_get_args();
1463
    /** @noinspection SuspiciousAssignmentsInspection */
1464 2
    $query = \array_shift($args);
1465 2
    $query = \str_replace('?', '%s', $query);
1466 2
    $args = \array_map(
1467
        [
1468 2
            $db,
1469 2
            'escape',
1470
        ],
1471 2
        $args
1472
    );
1473 2
    \array_unshift($args, $query);
1474 2
    $query = \sprintf(...$args);
1475 2
    $result = $db->query($query);
1476
1477 2
    if ($result instanceof Result) {
1478 2
      return $result->fetchAllArray();
1479
    }
1480
1481 2
    return $result;
1482
  }
1483
1484
  /**
1485
   * Execute a sql-query.
1486
   *
1487
   * @param string        $sql            <p>The sql query-string.</p>
1488
   *
1489
   * @param array|boolean $params         <p>
1490
   *                                      "array" of sql-query-parameters<br/>
1491
   *                                      "false" if you don't need any parameter (default)<br/>
1492
   *                                      </p>
1493
   *
1494
   * @return bool|int|Result              <p>
1495
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1496
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1497
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1498
   *                                      "true" by e.g. "DROP"-queries<br />
1499
   *                                      "false" on error
1500
   *                                      </p>
1501
   *
1502
   * @throws QueryException
1503
   */
1504 130
  public function query(string $sql = '', $params = false)
1505
  {
1506 130
    if (!$this->isReady()) {
1507
      return false;
1508
    }
1509
1510 130 View Code Duplication
    if (!$sql || $sql === '') {
1511 8
      $this->_debug->displayError('Can not execute an empty query.', false);
1512
1513 8
      return false;
1514
    }
1515
1516
    if (
1517 126
        $params !== false
1518
        &&
1519 126
        \is_array($params)
1520
        &&
1521 126
        \count($params) > 0
1522
    ) {
1523 15
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1524 15
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1525 15
      $sql = $parseQueryParamsByName['sql'];
1526
    }
1527
1528
    // DEBUG
1529
    // var_dump($params);
1530
    // echo $sql . "\n";
1531
1532 126
    $query_start_time = microtime(true);
1533 126
    $queryException = null;
1534
1535 126
    if ($this->_doctrine_connection) {
1536
1537
      try {
1538 28
        $query_result_doctrine = $this->_doctrine_connection->prepare($sql);
1539 28
        $query_result = $query_result_doctrine->execute();
1540 28
        $mysqli_field_count = $query_result_doctrine->columnCount();
1541 8
      } catch (\Exception $e) {
1542 8
        $query_result = false;
1543 8
        $mysqli_field_count = null;
1544
1545 28
        $queryException = $e;
1546
      }
1547
1548
    } else {
1549
1550 100
      $query_result = \mysqli_real_query($this->link, $sql);
1551 100
      $mysqli_field_count = \mysqli_field_count($this->link);
1552
1553
    }
1554
1555 126
    $query_duration = microtime(true) - $query_start_time;
1556
1557 126
    $this->query_count++;
1558
1559 126
    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...
1560
1561 89
      if ($this->_doctrine_connection) {
1562
1563 24
        $result = false;
1564 24
        if ($query_result_doctrine) {
1565 24
          if ($query_result_doctrine instanceof \Doctrine\DBAL\Statement) {
1566 24
            $result = $query_result_doctrine;
0 ignored issues
show
Bug introduced by
The variable $query_result_doctrine 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...
1567
          }
1568
        }
1569
1570
      } else {
1571
1572 89
        $result = \mysqli_store_result($this->link);
1573
1574
      }
1575
    } else {
1576 91
      $result = $query_result;
1577
    }
1578
1579
    if (
1580 126
        $result instanceof \Doctrine\DBAL\Statement
1581
        &&
1582 126
        $result->columnCount() > 0
1583
    ) {
1584
1585
      // log the select query
1586 24
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1587
1588
      // return query result object
1589 24
      return new Result($sql, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object<Doctrine\DBAL\Statement>, but the function expects a object<mysqli_result>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1590
    }
1591
1592 122
    if ($result instanceof \mysqli_result) {
1593
1594
      // log the select query
1595 63
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1596
1597
      // return query result object
1598 63
      return new Result($sql, $result);
1599
    }
1600
1601 93
    if ($query_result === true) {
1602
1603
      // "INSERT" || "REPLACE"
1604 87 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1605 81
        $insert_id = (int)$this->insert_id();
1606 81
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1607
1608 81
        return $insert_id;
1609
      }
1610
1611
      // "UPDATE" || "DELETE"
1612 46 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1613 20
        $affected_rows = $this->affected_rows();
1614 20
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1615
1616 20
        return $affected_rows;
1617
      }
1618
1619
      // log the ? query
1620 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1621
1622 27
      return true;
1623
    }
1624
1625
    // log the error query
1626 22
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1627
1628 22
    if ($queryException) {
1629 8
      return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1630
    }
1631
1632 14
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1633
  }
1634
1635
  /**
1636
   * Error-handling for the sql-query.
1637
   *
1638
   * @param string     $errorMessage
1639
   * @param int        $errorNumber
1640
   * @param string     $sql
1641
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1642
   * @param bool       $sqlMultiQuery
1643
   *
1644
   * @return mixed|false
1645
   *
1646
   * @throws QueryException
1647
   * @throws DBGoneAwayException
1648
   */
1649 26
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1650
  {
1651
    if (
1652 26
        $errorMessage === 'DB server has gone away'
1653
        ||
1654 24
        $errorMessage === 'MySQL server has gone away'
1655
        ||
1656 26
        $errorNumber === 2006
1657
    ) {
1658 2
      static $RECONNECT_COUNTER;
1659
1660
      // exit if we have more then 3 "DB server has gone away"-errors
1661 2
      if ($RECONNECT_COUNTER > 3) {
1662
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1663
        throw new DBGoneAwayException($errorMessage);
1664
      }
1665
1666 2
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1667
1668
      // reconnect
1669 2
      $RECONNECT_COUNTER++;
1670 2
      $this->reconnect(true);
1671
1672
      // re-run the current (non multi) query
1673 2
      if ($sqlMultiQuery === false) {
1674 2
        return $this->query($sql, $sqlParams);
1675
      }
1676
1677
      return false;
1678
    }
1679
1680 24
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1681
1682 24
    $force_exception_after_error = null; // auto
1683 24
    if ($this->_in_transaction === true) {
1684 8
      $force_exception_after_error = false;
1685
    }
1686
    // this query returned an error, we must display it (only for dev) !!!
1687
1688 24
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1689
1690 24
    return false;
1691
  }
1692
1693
  /**
1694
   * Quote && Escape e.g. a table name string.
1695
   *
1696
   * @param mixed $str
1697
   *
1698
   * @return string
1699
   */
1700 61
  public function quote_string($str): string
1701
  {
1702 61
    $str = \str_replace(
1703 61
        '`',
1704 61
        '``',
1705 61
        \trim(
1706 61
            (string)$this->escape($str, false),
1707 61
            '`'
1708
        )
1709
    );
1710
1711 61
    return '`' . $str . '`';
1712
  }
1713
1714
  /**
1715
   * Reconnect to the MySQL-Server.
1716
   *
1717
   * @param bool $checkViaPing
1718
   *
1719
   * @return bool
1720
   */
1721 5
  public function reconnect(bool $checkViaPing = false): bool
1722
  {
1723 5
    $ping = false;
1724 5
    if ($checkViaPing === true) {
1725 4
      $ping = $this->ping();
1726
    }
1727
1728 5
    if ($ping !== true) {
1729 5
      $this->connected = false;
1730 5
      $this->connect();
1731
    }
1732
1733 5
    return $this->isReady();
1734
  }
1735
1736
  /**
1737
   * Execute a "replace"-query.
1738
   *
1739
   * @param string      $table
1740
   * @param array       $data
1741
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1742
   *
1743
   * @return false|int <p>false on error</p>
1744
   *
1745
   * @throws QueryException
1746
   */
1747 2
  public function replace(string $table, array $data = [], string $databaseName = null)
1748
  {
1749
    // init
1750 2
    $table = \trim($table);
1751
1752 2
    if ($table === '') {
1753 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1754
1755 2
      return false;
1756
    }
1757
1758 2
    if (\count($data) === 0) {
1759 2
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1760
1761 2
      return false;
1762
    }
1763
1764
    // extracting column names
1765 2
    $columns = \array_keys($data);
1766 2
    foreach ($columns as $k => $_key) {
1767
      /** @noinspection AlterInForeachInspection */
1768 2
      $columns[$k] = $this->quote_string($_key);
1769
    }
1770
1771 2
    $columns = \implode(',', $columns);
1772
1773
    // extracting values
1774 2
    foreach ($data as $k => $_value) {
1775
      /** @noinspection AlterInForeachInspection */
1776 2
      $data[$k] = $this->secure($_value);
1777
    }
1778 2
    $values = \implode(',', $data);
1779
1780 2
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1781
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1782
    }
1783
1784 2
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values)";
1785
1786 2
    return $this->query($sql);
1787
  }
1788
1789
  /**
1790
   * Rollback in a transaction and end the transaction.
1791
   *
1792
   * @return bool <p>Boolean true on success, false otherwise.</p>
1793
   */
1794 8 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...
1795
  {
1796 8
    if ($this->_in_transaction === false) {
1797
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1798
1799
      return false;
1800
    }
1801
1802 8
    $return = \mysqli_rollback($this->link);
1803 8
    \mysqli_autocommit($this->link, true);
1804 8
    $this->_in_transaction = false;
1805
1806 8
    return $return;
1807
  }
1808
1809
  /**
1810
   * Try to secure a variable, so can you use it in sql-queries.
1811
   *
1812
   * <p>
1813
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1814
   * 1. parse into "int"
1815
   * </p><br />
1816
   *
1817
   * <p>
1818
   * <strong>float:</strong><br />
1819
   * 1. return "float"
1820
   * </p><br />
1821
   *
1822
   * <p>
1823
   * <strong>string:</strong><br />
1824
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1825
   * 2. trim '<br />
1826
   * 3. escape the string (and remove non utf-8 chars)<br />
1827
   * 4. trim ' again (because we maybe removed some chars)<br />
1828
   * 5. add ' around the new string<br />
1829
   * </p><br />
1830
   *
1831
   * <p>
1832
   * <strong>array:</strong><br />
1833
   * 1. return null
1834
   * </p><br />
1835
   *
1836
   * <p>
1837
   * <strong>object:</strong><br />
1838
   * 1. return false
1839
   * </p><br />
1840
   *
1841
   * <p>
1842
   * <strong>null:</strong><br />
1843
   * 1. return null
1844
   * </p>
1845
   *
1846
   * @param mixed $var
1847
   *
1848
   * @return mixed
1849
   */
1850 71
  public function secure($var)
1851
  {
1852 71
    if ($var === '') {
1853 4
      return "''";
1854
    }
1855
1856 71
    if ($var === "''") {
1857 2
      return "''";
1858
    }
1859
1860 71
    if ($var === null) {
1861 3
      if ($this->_convert_null_to_empty_string === true) {
1862 2
        return "''";
1863
      }
1864
1865 3
      return 'NULL';
1866
    }
1867
1868 70
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1869 2
      return $var;
1870
    }
1871
1872 70
    if (\is_string($var)) {
1873 59
      $var = \trim($var, "'");
1874
    }
1875
1876 70
    $var = $this->escape($var, false, false, null);
1877
1878 68
    if (\is_string($var)) {
1879 59
      $var = "'" . \trim($var, "'") . "'";
1880
    }
1881
1882 68
    return $var;
1883
  }
1884
1885
  /**
1886
   * Execute a "select"-query.
1887
   *
1888
   * @param string       $table
1889
   * @param string|array $where
1890
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1891
   *
1892
   * @return false|Result <p>false on error</p>
1893
   *
1894
   * @throws QueryException
1895
   */
1896 43 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...
1897
  {
1898
    // init
1899 43
    $table = \trim($table);
1900
1901 43
    if ($table === '') {
1902 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1903
1904 2
      return false;
1905
    }
1906
1907 43
    if (\is_string($where)) {
1908 16
      $WHERE = $this->escape($where, false);
1909 29
    } elseif (\is_array($where)) {
1910 29
      $WHERE = $this->_parseArrayPair($where, 'AND');
1911
    } else {
1912 2
      $WHERE = '';
1913
    }
1914
1915 43
    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...
1916
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1917
    }
1918
1919 43
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE)";
1920
1921 43
    return $this->query($sql);
1922
  }
1923
1924
  /**
1925
   * Selects a different database than the one specified on construction.
1926
   *
1927
   * @param string $database <p>Database name to switch to.</p>
1928
   *
1929
   * @return bool <p>Boolean true on success, false otherwise.</p>
1930
   */
1931
  public function select_db(string $database): bool
1932
  {
1933
    if (!$this->isReady()) {
1934
      return false;
1935
    }
1936
1937
    return mysqli_select_db($this->link, $database);
1938
  }
1939
1940
  /**
1941
   * @param array $extra_config           <p>
1942
   *                                      'session_to_db' => false|true<br>
1943
   *                                      'socket' => 'string (path)'<br>
1944
   *                                      'ssl' => 'bool'<br>
1945
   *                                      'clientkey' => 'string (path)'<br>
1946
   *                                      'clientcert' => 'string (path)'<br>
1947
   *                                      'cacert' => 'string (path)'<br>
1948
   *                                      </p>
1949
   */
1950 65
  public function setConfigExtra(array $extra_config)
1951
  {
1952 65
    if (isset($extra_config['session_to_db'])) {
1953
      $this->session_to_db = (boolean)$extra_config['session_to_db'];
1954
    }
1955
1956
    if (
1957 65
        isset($extra_config['doctrine'])
1958
        &&
1959 65
        $extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection
1960
    ) {
1961 54
      $this->_doctrine_connection = $extra_config['doctrine'];
1962
    }
1963
1964 65
    if (isset($extra_config['socket'])) {
1965
      $this->socket = $extra_config['socket'];
1966
    }
1967
1968 65
    if (isset($extra_config['ssl'])) {
1969
      $this->_ssl = $extra_config['ssl'];
1970
    }
1971
1972 65
    if (isset($extra_config['clientkey'])) {
1973
      $this->_clientkey = $extra_config['clientkey'];
1974
    }
1975
1976 65
    if (isset($extra_config['clientcert'])) {
1977
      $this->_clientcert = $extra_config['clientcert'];
1978
    }
1979
1980 65
    if (isset($extra_config['cacert'])) {
1981
      $this->_cacert = $extra_config['cacert'];
1982
    }
1983 65
  }
1984
1985
  /**
1986
   * Set the current charset.
1987
   *
1988
   * @param string $charset
1989
   *
1990
   * @return bool
1991
   */
1992 62
  public function set_charset(string $charset): bool
1993
  {
1994 62
    $charsetLower = strtolower($charset);
1995 62
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1996 60
      $charset = 'utf8';
1997
    }
1998 62
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1999 60
      $charset = 'utf8mb4';
2000
    }
2001
2002 62
    $this->charset = $charset;
2003
2004 62
    $return = mysqli_set_charset($this->link, $charset);
2005
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
2006 62
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
2007
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
2008 62
    @\mysqli_query($this->link, "SET NAMES '" . $charset . "'");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2009
2010 62
    return $return;
2011
  }
2012
2013
  /**
2014
   * Set the option to convert null to "''" (empty string).
2015
   *
2016
   * Used in secure() => select(), insert(), update(), delete()
2017
   *
2018
   * @deprecated It's not recommended to convert NULL into an empty string!
2019
   *
2020
   * @param bool $bool
2021
   *
2022
   * @return self
2023
   */
2024 2
  public function set_convert_null_to_empty_string(bool $bool): self
2025
  {
2026 2
    $this->_convert_null_to_empty_string = $bool;
2027
2028 2
    return $this;
2029
  }
2030
2031
  /**
2032
   * Enables or disables internal report functions
2033
   *
2034
   * @link http://php.net/manual/en/function.mysqli-report.php
2035
   *
2036
   * @param int $flags <p>
2037
   *                   <table>
2038
   *                   Supported flags
2039
   *                   <tr valign="top">
2040
   *                   <td>Name</td>
2041
   *                   <td>Description</td>
2042
   *                   </tr>
2043
   *                   <tr valign="top">
2044
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2045
   *                   <td>Turns reporting off</td>
2046
   *                   </tr>
2047
   *                   <tr valign="top">
2048
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2049
   *                   <td>Report errors from mysqli function calls</td>
2050
   *                   </tr>
2051
   *                   <tr valign="top">
2052
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2053
   *                   <td>
2054
   *                   Throw <b>mysqli_sql_exception</b> for errors
2055
   *                   instead of warnings
2056
   *                   </td>
2057
   *                   </tr>
2058
   *                   <tr valign="top">
2059
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2060
   *                   <td>Report if no index or bad index was used in a query</td>
2061
   *                   </tr>
2062
   *                   <tr valign="top">
2063
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2064
   *                   <td>Set all options (report all)</td>
2065
   *                   </tr>
2066
   *                   </table>
2067
   *                   </p>
2068
   *
2069
   * @return bool
2070
   */
2071
  public function set_mysqli_report(int $flags): bool
2072
  {
2073
    return \mysqli_report($flags);
2074
  }
2075
2076
  /**
2077
   * Show config errors by throw exceptions.
2078
   *
2079
   * @return bool
2080
   *
2081
   * @throws \InvalidArgumentException
2082
   */
2083 65
  public function showConfigError(): bool
2084
  {
2085
    // check if a doctrine connection is already open
2086
    if (
2087 65
        $this->_doctrine_connection
2088
        &&
2089 65
        $this->_doctrine_connection->isConnected()
2090
    ) {
2091 54
      return true;
2092
    }
2093
2094
    if (
2095 17
        !$this->hostname
2096
        ||
2097 15
        !$this->username
2098
        ||
2099 17
        !$this->database
2100
    ) {
2101
2102 6
      if (!$this->hostname) {
2103 2
        throw new \InvalidArgumentException('no-sql-hostname');
2104
      }
2105
2106 4
      if (!$this->username) {
2107 2
        throw new \InvalidArgumentException('no-sql-username');
2108
      }
2109
2110 2
      if (!$this->database) {
2111 2
        throw new \InvalidArgumentException('no-sql-database');
2112
      }
2113
2114
      return false;
2115
    }
2116
2117 11
    return true;
2118
  }
2119
2120
  /**
2121
   * alias: "beginTransaction()"
2122
   */
2123 2
  public function startTransaction(): bool
2124
  {
2125 2
    return $this->beginTransaction();
2126
  }
2127
2128
  /**
2129
   * Determine if database table exists
2130
   *
2131
   * @param string $table
2132
   *
2133
   * @return bool
2134
   */
2135 2
  public function table_exists(string $table): bool
2136
  {
2137 2
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2138
2139 2
    return $check !== false
2140
           &&
2141 2
           $check instanceof Result
2142
           &&
2143 2
           $check->num_rows > 0;
2144
  }
2145
2146
  /**
2147
   * Execute a callback inside a transaction.
2148
   *
2149
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2150
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2151
   *
2152
   * @return bool <p>Boolean true on success, false otherwise.</p>
2153
   */
2154 2
  public function transact($callback): bool
2155
  {
2156
    try {
2157
2158 2
      $beginTransaction = $this->beginTransaction();
2159 2
      if ($beginTransaction === false) {
2160 2
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
2161
2162 2
        return false;
2163
      }
2164
2165 2
      $result = $callback($this);
2166 2
      if ($result === false) {
2167
        /** @noinspection ThrowRawExceptionInspection */
2168 2
        throw new \Exception('call_user_func [' . $callback . '] === false');
2169
      }
2170
2171 2
      return $this->commit();
2172
2173 2
    } catch (\Exception $e) {
2174
2175 2
      $this->rollback();
2176
2177 2
      return false;
2178
    }
2179
  }
2180
2181
  /**
2182
   * Execute a "update"-query.
2183
   *
2184
   * @param string       $table
2185
   * @param array        $data
2186
   * @param array|string $where
2187
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2188
   *
2189
   * @return false|int <p>false on error</p>
2190
   *
2191
   * @throws QueryException
2192
   */
2193 14
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2194
  {
2195
    // init
2196 14
    $table = \trim($table);
2197
2198 14
    if ($table === '') {
2199 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2200
2201 2
      return false;
2202
    }
2203
2204 14
    if (\count($data) === 0) {
2205 4
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2206
2207 4
      return false;
2208
    }
2209
2210 14
    $SET = $this->_parseArrayPair($data);
2211
2212 14
    if (\is_string($where)) {
2213 4
      $WHERE = $this->escape($where, false);
2214 12
    } elseif (\is_array($where)) {
2215 10
      $WHERE = $this->_parseArrayPair($where, 'AND');
2216
    } else {
2217 2
      $WHERE = '';
2218
    }
2219
2220 14
    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...
2221
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2222
    }
2223
2224 14
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE)";
2225
2226 14
    return $this->query($sql);
2227
  }
2228
2229
}
2230