Completed
Push — master ( 54db38...e6245f )
by Lars
01:29
created

DB::_loadConfig()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.0046

Importance

Changes 0
Metric Value
dl 0
loc 55
ccs 21
cts 22
cp 0.9545
rs 8.0484
c 0
b 0
f 0
cc 7
nc 16
nop 11
crap 7.0046

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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