Completed
Push — master ( 3d203f...f18ebe )
by Lars
02:19
created

DB::getDebugger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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