Completed
Push — master ( 834586...b472fa )
by Lars
02:24
created

DB::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 60
ccs 17
cts 17
cp 1
rs 8.8727
c 0
b 0
f 0
cc 1
nc 1
nop 11
crap 1

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