Completed
Pull Request — master (#44)
by
unknown
03:52
created

DB::commit()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 28

Duplication

Lines 28
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 12.4072

Importance

Changes 0
Metric Value
dl 28
loc 28
ccs 7
cts 16
cp 0.4375
rs 8.8497
c 0
b 0
f 0
cc 6
nc 5
nop 0
crap 12.4072
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 110
        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
            // save into the cache
1176
            if (
1177 3
                $cacheKey !== null
1178
                &&
1179 3
                $useCache
1180
                &&
1181 3
                $cache instanceof Cache
1182
                &&
1183 3
                $cache->getCacheIsReady()
1184
            ) {
1185 1
                $return = $result->fetchAllArrayy();
1186
1187 1
                $cache->setItem($cacheKey, $return, $cacheTTL);
1188
            } else {
1189 3
                $return = $result->fetchAllArrayyYield();
1190
            }
1191
        } else {
1192 6
            $return = $result;
1193
        }
1194
1195 9
        return $return;
1196
    }
1197
1198
    /**
1199
     * Get all table-names via "SHOW TABLES".
1200
     *
1201
     * @return Arrayy
1202
     */
1203 3
    public function getAllTables(): Arrayy
1204
    {
1205 3
        $query = 'SHOW TABLES';
1206 3
        $result = $this->query($query);
1207
1208 3
        \assert($result instanceof Result);
1209
1210 3
        return $result->fetchAllArrayyYield();
1211
    }
1212
1213
    /**
1214
     * @return array
1215
     */
1216 49
    public function getConfig(): array
1217
    {
1218
        $config = [
1219 49
            'hostname'   => $this->hostname,
1220 49
            'username'   => $this->username,
1221 49
            'password'   => $this->password,
1222 49
            'port'       => $this->port,
1223 49
            'database'   => $this->database,
1224 49
            'socket'     => $this->socket,
1225 49
            'charset'    => $this->charset,
1226 49
            'cacert'     => $this->cacert,
1227 49
            'clientcert' => $this->clientcert,
1228 49
            'clientkey'  => $this->clientkey,
1229
        ];
1230
1231 49
        if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1232
            $config += $this->doctrine_connection->getParams();
1233
        }
1234
1235 49
        return $config;
1236
    }
1237
1238
    /**
1239
     * @return Debug
1240
     */
1241 10
    public function getDebugger(): Debug
1242
    {
1243 10
        return $this->debug;
1244
    }
1245
1246
    /**
1247
     * @return \Doctrine\DBAL\Connection|null|null
1248
     */
1249 2
    public function getDoctrineConnection()
1250
    {
1251 2
        return $this->doctrine_connection;
1252
    }
1253
1254
    /**
1255
     * @return \Doctrine\DBAL\Driver\Connection|false
1256
     */
1257 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...
1258
    {
1259
        if ($this->doctrine_connection) {
1260
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1261
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1262
                return $doctrineWrappedConnection;
1263
            }
1264
        }
1265
1266
        return false;
1267
    }
1268
1269
    /**
1270
     * Get errors from "$this->errors".
1271
     *
1272
     * @return array
1273
     */
1274 3
    public function getErrors(): array
1275
    {
1276 3
        return $this->debug->getErrors();
1277
    }
1278
1279
    /**
1280
     * @param string $hostname              <p>Hostname of the mysql server</p>
1281
     * @param string $username              <p>Username for the mysql connection</p>
1282
     * @param string $password              <p>Password for the mysql connection</p>
1283
     * @param string $database              <p>Database for the mysql connection</p>
1284
     * @param int    $port                  <p>default is (int)3306</p>
1285
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1286
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
1287
     *                                      'false'. Use false to disable it.</p>
1288
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
1289
     *                                      Use false to disable it.</p>
1290
     * @param string $logger_class_name
1291
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1292
     * @param array  $extra_config          <p>
1293
     *                                      're_connect'    => bool<br>
1294
     *                                      'session_to_db' => bool<br>
1295
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
1296
     *                                      'socket'        => string (path)<br>
1297
     *                                      'flags'         => null|int<br>
1298
     *                                      'ssl'           => bool<br>
1299
     *                                      'clientkey'     => string (path)<br>
1300
     *                                      'clientcert'    => string (path)<br>
1301
     *                                      'cacert'        => string (path)<br>
1302
     *                                      </p>
1303
     *
1304
     * @return self
1305
     */
1306 207
    public static function getInstance(
1307
        string $hostname = '',
1308
        string $username = '',
1309
        string $password = '',
1310
        string $database = '',
1311
        $port = 3306,
1312
        string $charset = 'utf8',
1313
        bool $exit_on_error = true,
1314
        bool $echo_on_error = true,
1315
        string $logger_class_name = '',
1316
        string $logger_level = '',
1317
        array $extra_config = []
1318
    ): self {
1319
        /**
1320
         * @var self[]
1321
         */
1322 207
        static $instance = [];
1323
1324
        /**
1325
         * @var self|null
1326
         */
1327 207
        static $firstInstance = null;
1328
1329
        // fallback
1330 207
        if (!$charset) {
1331 121
            $charset = 'utf8';
1332
        }
1333
1334
        if (
1335 207
            '' . $hostname . $username . $password . $database . $port . $charset === '' . $port . $charset
1336
            &&
1337 207
            $firstInstance instanceof self
1338
        ) {
1339 123
            if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1340
                $firstInstance->reconnect(true);
1341
            }
1342
1343 123
            return $firstInstance;
1344
        }
1345
1346 126
        $extra_config_string = '';
1347 126
        foreach ($extra_config as $extra_config_key => $extra_config_value) {
1348 56
            if (\is_object($extra_config_value)) {
1349
                $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1350
            } else {
1351 56
                $extra_config_value_tmp = (string) $extra_config_value;
1352
            }
1353 56
            $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1354
        }
1355
1356 126
        $connection = \md5(
1357 126
            $hostname . $username . $password . $database . $port . $charset . (int) $exit_on_error . (int) $echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1358
        );
1359
1360 126
        if (!isset($instance[$connection])) {
1361 24
            $instance[$connection] = new self(
1362 24
                $hostname,
1363 24
                $username,
1364 24
                $password,
1365 24
                $database,
1366 24
                $port,
1367 24
                $charset,
1368 24
                $exit_on_error,
1369 24
                $echo_on_error,
1370 24
                $logger_class_name,
1371 24
                $logger_level,
1372 24
                $extra_config
1373
            );
1374
1375 6
            if ($firstInstance === null) {
1376 1
                $firstInstance = $instance[$connection];
1377
            }
1378
        }
1379
1380 114
        if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1381
            $instance[$connection]->reconnect(true);
1382
        }
1383
1384 114
        return $instance[$connection];
1385
    }
1386
1387
    /**
1388
     * @param \Doctrine\DBAL\Connection $doctrine
1389
     * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1390
     * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1391
     *                                                 return 'false'. Use false to disable it.</p>
1392
     * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1393
     *                                                 Use false to disable it.</p>
1394
     * @param string                    $logger_class_name
1395
     * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1396
     * @param array                     $extra_config  <p>
1397
     *                                                 're_connect'    => bool<br>
1398
     *                                                 'session_to_db' => bool<br>
1399
     *                                                 'socket'        => string (path)<br>
1400
     *                                                 'flags'         => null|int<br>
1401
     *                                                 'ssl'           => bool<br>
1402
     *                                                 'clientkey'     => string (path)<br>
1403
     *                                                 'clientcert'    => string (path)<br>
1404
     *                                                 'cacert'        => string (path)<br>
1405
     *                                                 </p>
1406
     *
1407
     * @return self
1408
     */
1409 55
    public static function getInstanceDoctrineHelper(
1410
        \Doctrine\DBAL\Connection $doctrine,
1411
        string $charset = 'utf8',
1412
        bool $exit_on_error = true,
1413
        bool $echo_on_error = true,
1414
        string $logger_class_name = '',
1415
        string $logger_level = '',
1416
        array $extra_config = []
1417
    ): self {
1418 55
        $extra_config['doctrine'] = $doctrine;
1419
1420 55
        return self::getInstance(
1421 55
            '',
1422 55
            '',
1423 55
            '',
1424 55
            '',
1425 55
            3306,
1426 55
            $charset,
1427 55
            $exit_on_error,
1428 55
            $echo_on_error,
1429 55
            $logger_class_name,
1430 55
            $logger_level,
1431 55
            $extra_config
1432
        );
1433
    }
1434
1435
    /**
1436
     * Get the mysqli-link (link identifier returned by mysqli-connect).
1437
     *
1438
     * @return \mysqli|null
1439
     */
1440 15
    public function getLink()
1441
    {
1442 15
        return $this->mysqli_link;
1443
    }
1444
1445
    /**
1446
     * Get the current charset.
1447
     *
1448
     * @return string
1449
     */
1450 3
    public function get_charset(): string
1451
    {
1452 3
        return $this->charset;
1453
    }
1454
1455
    /**
1456
     * Check if we are in a transaction.
1457
     *
1458
     * @return bool
1459
     */
1460
    public function inTransaction(): bool
1461
    {
1462
        return $this->in_transaction;
1463
    }
1464
1465
    /**
1466
     * Execute a "insert"-query.
1467
     *
1468
     * @param string      $table
1469
     * @param array       $data
1470
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1471
     *
1472
     * @throws QueryException
1473
     *
1474
     * @return false|int|string
1475
     *                   <p>false on error</p>
1476
     */
1477 74
    public function insert(
1478
        string $table,
1479
        array $data = [],
1480
        string $databaseName = null
1481
    ) {
1482
        // init
1483 74
        $table = \trim($table);
1484
1485 74
        if ($table === '') {
1486 6
            $this->debug->displayError('Invalid table name, table name in empty.', false);
1487
1488 6
            return false;
1489
        }
1490
1491 71
        if (\count($data) === 0) {
1492 9
            $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1493
1494 9
            return false;
1495
        }
1496
1497 65
        $SET = $this->_parseArrayPair($data);
1498
1499 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...
1500
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1501
        }
1502
1503 65
        $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET ${SET}";
1504
1505 65
        $return = $this->query($sql);
1506 65
        if ($return === false) {
1507 3
            return false;
1508
        }
1509
1510 65
        \assert(\is_int($return) || \is_string($return));
1511
1512 65
        return $return;
1513
    }
1514
1515
    /**
1516
     * Returns the auto generated id used in the last query.
1517
     *
1518
     * @return false|int|string
1519
     */
1520 100
    public function insert_id()
1521
    {
1522 100
        if ($this->mysqli_link) {
1523 100
            return \mysqli_insert_id($this->mysqli_link);
1524
        }
1525
1526
        $doctrinePDOConnection = $this->getDoctrinePDOConnection();
1527
        if ($doctrinePDOConnection) {
1528
            return $doctrinePDOConnection->lastInsertId();
1529
        }
1530
1531
        return false;
1532
    }
1533
1534
    /**
1535
     * @return bool
1536
     */
1537
    public function isDoctrineMySQLiConnection(): bool
1538
    {
1539
        if ($this->doctrine_connection) {
1540
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1541
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1542
                return true;
1543
            }
1544
        }
1545
1546
        return false;
1547
    }
1548
1549
    /**
1550
     * @return bool
1551
     */
1552 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...
1553
    {
1554
        if ($this->doctrine_connection) {
1555
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1556
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1557
                return true;
1558
            }
1559
        }
1560
1561
        return false;
1562
    }
1563
1564
    /**
1565
     * Check if db-connection is ready.
1566
     *
1567
     * @return bool
1568
     */
1569 162
    public function isReady(): bool
1570
    {
1571 162
        return $this->connected ? true : false;
1572
    }
1573
1574
    /**
1575
     * Get the last sql-error.
1576
     *
1577
     * @return false|string
1578
     *                      <p>false === there was no error</p>
1579
     */
1580 3
    public function lastError()
1581
    {
1582 3
        $errors = $this->debug->getErrors();
1583
1584 3
        return \count($errors) > 0 ? \end($errors) : false;
1585
    }
1586
1587
    /**
1588
     * Execute a sql-multi-query.
1589
     *
1590
     * @param string $sql
1591
     *
1592
     * @throws QueryException
1593
     *
1594
     * @return bool|Result[]
1595
     *                        <ul>
1596
     *                        <li>"Result"-Array by "<b>SELECT</b>"-queries</li>
1597
     *                        <li>"bool" by only "<b>INSERT</b>"-queries</li>
1598
     *                        <li>"bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1599
     *                        <li>"bool" by only by e.g. "DROP"-queries</li>
1600
     *                        </ul>
1601
     */
1602 3
    public function multi_query(string $sql)
1603
    {
1604 3
        if (!$this->isReady()) {
1605
            return false;
1606
        }
1607
1608 3
        if (!$sql || $sql === '') {
1609 3
            $this->debug->displayError('Can not execute an empty query.', false);
1610
1611 3
            return false;
1612
        }
1613
1614 3
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1615
            $query_start_time = \microtime(true);
1616
            $queryException = null;
1617
            $query_result_doctrine = false;
1618
1619
            try {
1620
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1621
                $resultTmp = $query_result_doctrine->execute();
1622
                $mysqli_field_count = $query_result_doctrine->columnCount();
1623
            } catch (\Exception $e) {
1624
                $resultTmp = false;
1625
                $mysqli_field_count = null;
1626
1627
                $queryException = $e;
1628
            }
1629
1630
            $query_duration = \microtime(true) - $query_start_time;
1631
1632
            $this->debug->logQuery($sql, $query_duration, 0);
1633
1634
            $returnTheResult = false;
1635
            $result = [];
1636
1637
            if ($resultTmp) {
1638
                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...
1639
                    if (
1640
                        $query_result_doctrine
1641
                        &&
1642
                        $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1643
                    ) {
1644
                        $result = $query_result_doctrine;
1645
                    }
1646
                } else {
1647
                    $result = $resultTmp;
1648
                }
1649
1650
                if (
1651
                    $result instanceof \Doctrine\DBAL\Statement
1652
                    &&
1653
                    $result->columnCount() > 0
1654
                ) {
1655
                    $returnTheResult = true;
1656
1657
                    // return query result object
1658
                    $result = [new Result($sql, $result)];
1659
                } else {
1660
                    $result = [$result];
1661
                }
1662
            } else {
1663
1664
                // log the error query
1665
                $this->debug->logQuery($sql, $query_duration, 0, true);
1666
1667
                if (
1668
                    isset($queryException)
1669
                    &&
1670
                    $queryException instanceof \Doctrine\DBAL\Query\QueryException
1671
                ) {
1672
                    return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1673
                }
1674
            }
1675 3
        } elseif ($this->mysqli_link) {
1676 3
            $query_start_time = \microtime(true);
1677 3
            $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1678 3
            $query_duration = \microtime(true) - $query_start_time;
1679
1680 3
            $this->debug->logQuery($sql, $query_duration, 0);
1681
1682 3
            $returnTheResult = false;
1683 3
            $result = [];
1684
1685 3
            if ($resultTmp) {
1686
                do {
1687 3
                    $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1688
1689 3
                    if ($resultTmpInner instanceof \mysqli_result) {
1690 3
                        $returnTheResult = true;
1691 3
                        $result[] = new Result($sql, $resultTmpInner);
1692 3
                    } elseif (\mysqli_errno($this->mysqli_link)) {
1693
                        $result[] = false;
1694
                    } else {
1695 3
                        $result[] = true;
1696
                    }
1697 3
                } while (\mysqli_more_results($this->mysqli_link) ? \mysqli_next_result($this->mysqli_link) : false);
1698
            } else {
1699
1700
                // log the error query
1701 3
                $this->debug->logQuery($sql, $query_duration, 0, true);
1702
1703 3
                return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1704
            }
1705
        } else {
1706
1707
            // log the error query
1708
            $this->debug->logQuery($sql, 0, 0, true);
1709
1710
            return $this->queryErrorHandling('no database connection', 1, $sql, false, true);
1711
        }
1712
1713
        // return the result only if there was a "SELECT"-query
1714 3
        if ($returnTheResult) {
1715 3
            return $result;
1716
        }
1717
1718
        if (
1719 3
            \count($result) > 0
1720
            &&
1721 3
            !\in_array(false, $result, true)
1722
        ) {
1723 3
            return true;
1724
        }
1725
1726
        return false;
1727
    }
1728
1729
    /**
1730
     * Count number of rows found matching a specific query.
1731
     *
1732
     * @param string $query
1733
     *
1734
     * @return int
1735
     */
1736 3
    public function num_rows(string $query): int
1737
    {
1738 3
        $check = $this->query($query);
1739
1740
        if (
1741 3
            $check === false
1742
            ||
1743 3
            !$check instanceof Result
1744
        ) {
1745
            return 0;
1746
        }
1747
1748 3
        return $check->num_rows;
1749
    }
1750
1751
    /**
1752
     * Pings a server connection, or tries to reconnect
1753
     * if the connection has gone down.
1754
     *
1755
     * @return bool
1756
     */
1757 9
    public function ping(): bool
1758
    {
1759 9
        if (!$this->connected) {
1760 3
            return false;
1761
        }
1762
1763 6
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1764
            return $this->doctrine_connection->ping();
1765
        }
1766
1767 6
        if (!$this->mysqli_link) {
1768
            return false;
1769
        }
1770
1771
        if (
1772 6
            $this->mysqli_link
1773
            &&
1774 6
            $this->mysqli_link->connect_errno
1775
        ) {
1776
            return false;
1777
        }
1778
1779
        if (
1780 6
            $this->mysqli_link
1781
            &&
1782 6
            $this->mysqli_link instanceof \mysqli
1783
        ) {
1784 6
            return \mysqli_ping($this->mysqli_link);
1785
        }
1786
1787
        return false;
1788
    }
1789
1790
    /**
1791
     * Get a new "Prepare"-Object for your sql-query.
1792
     *
1793
     * @param string $query
1794
     *
1795
     * @return Prepare
1796
     */
1797 2
    public function prepare(string $query): Prepare
1798
    {
1799 2
        return new Prepare($this, $query);
1800
    }
1801
1802
    /**
1803
     * Execute a sql-query and return the result-array for select-statements.
1804
     *
1805
     * @param string $query
1806
     *
1807
     * @throws \Exception
1808
     *
1809
     * @return mixed
1810
     *
1811
     * @deprecated
1812
     */
1813 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...
1814
    {
1815 3
        $db = self::getInstance();
1816
1817 3
        $args = \func_get_args();
1818
        /** @noinspection SuspiciousAssignmentsInspection */
1819 3
        $query = \array_shift($args);
1820 3
        $query = \str_replace('?', '%s', $query);
1821 3
        $args = \array_map(
1822
            [
1823 3
                $db,
1824 3
                'escape',
1825
            ],
1826 3
            $args
1827
        );
1828 3
        \array_unshift($args, $query);
1829 3
        $query = \sprintf(...$args);
1830 3
        $result = $db->query($query);
1831
1832 3
        if ($result instanceof Result) {
1833 3
            return $result->fetchAllArrayyYield();
1834
        }
1835
1836 3
        return $result;
1837
    }
1838
1839
    /**
1840
     * Execute a sql-query.
1841
     *
1842
     * example:
1843
     * <code>
1844
     * $sql = "INSERT INTO TABLE_NAME_HERE
1845
     *   SET
1846
     *     foo = :foo,
1847
     *     bar = :bar
1848
     * ";
1849
     * $insert_id = $db->query(
1850
     *   $sql,
1851
     *   [
1852
     *     'foo' => 1.1,
1853
     *     'bar' => 1,
1854
     *   ]
1855
     * );
1856
     * </code>
1857
     *
1858
     * @param string     $sql               <p>The sql query-string.</p>
1859
     * @param array|bool $params            <p>
1860
     *                                      "array" of sql-query-parameters<br/>
1861
     *                                      "false" if you don't need any parameter (default)<br/>
1862
     *                                      </p>
1863
     *
1864
     * @throws QueryException
1865
     *
1866
     * @return bool|int|Result|string
1867
     *                                      <p>
1868
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
1869
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1870
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1871
     *                                      "true" by e.g. "DROP"-queries<br />
1872
     *                                      "false" on error
1873
     *                                      </p>
1874
     */
1875 140
    public function query(string $sql = '', $params = false)
1876
    {
1877 140
        if (!$this->isReady()) {
1878
            return false;
1879
        }
1880
1881 140
        if ($sql === '') {
1882 12
            $this->debug->displayError('Can not execute an empty query.', false);
1883
1884 12
            return false;
1885
        }
1886
1887
        if (
1888 134
            $params !== false
1889
            &&
1890 134
            \is_array($params)
1891
            &&
1892 134
            \count($params) > 0
1893
        ) {
1894 7
            $parseQueryParams = $this->_parseQueryParams($sql, $params);
1895 7
            $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1896 7
            $sql = $parseQueryParamsByName['sql'];
1897
        }
1898
1899
        // DEBUG
1900
        // var_dump($params);
1901
        // echo $sql . "\n";
1902
1903 134
        $query_start_time = \microtime(true);
1904 134
        $queryException = null;
1905 134
        $query_result_doctrine = false;
1906
1907 134
        if ($this->doctrine_connection) {
1908
            try {
1909
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1910
                $query_result = $query_result_doctrine->execute();
1911
                $mysqli_field_count = $query_result_doctrine->columnCount();
1912
            } catch (\Exception $e) {
1913
                $query_result = false;
1914
                $mysqli_field_count = null;
1915
1916
                $queryException = $e;
1917
            }
1918 134
        } elseif ($this->mysqli_link) {
1919 134
            $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1920 134
            $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1921
        } else {
1922
            $query_result = false;
1923
            $mysqli_field_count = null;
1924
1925
            $queryException = new DBConnectException('no mysql connection');
1926
        }
1927
1928 134
        $query_duration = \microtime(true) - $query_start_time;
1929
1930 134
        $this->query_count++;
1931
1932 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...
1933 99
            if ($this->doctrine_connection) {
1934
                $result = false;
1935
                if (
1936
                    $query_result_doctrine
1937
                    &&
1938
                    $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1939
                ) {
1940
                    $result = $query_result_doctrine;
1941
                }
1942 99
            } elseif ($this->mysqli_link) {
1943 99
                $result = \mysqli_store_result($this->mysqli_link);
1944
            } else {
1945 99
                $result = false;
1946
            }
1947
        } else {
1948 106
            $result = $query_result;
1949
        }
1950
1951
        if (
1952 134
            $result instanceof \Doctrine\DBAL\Statement
1953
            &&
1954 134
            $result->columnCount() > 0
1955
        ) {
1956
1957
            // log the select query
1958
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1959
1960
            // return query result object
1961
            return new Result($sql, $result);
1962
        }
1963
1964 134
        if ($result instanceof \mysqli_result) {
1965
1966
            // log the select query
1967 96
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1968
1969
            // return query result object
1970 96
            return new Result($sql, $result);
1971
        }
1972
1973 112
        if ($query_result) {
1974
1975
            // "INSERT" || "REPLACE"
1976 103
            if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1977 100
                $insert_id = $this->insert_id();
1978
1979 100
                $this->debug->logQuery($sql, $query_duration, $insert_id);
1980
1981 100
                return $insert_id;
1982
            }
1983
1984
            // "UPDATE" || "DELETE"
1985 50
            if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1986 25
                if ($this->mysqli_link) {
1987 25
                    $this->affected_rows = $this->affected_rows();
1988
                } elseif ($query_result_doctrine) {
1989
                    $this->affected_rows = $query_result_doctrine->rowCount();
1990
                }
1991
1992 25
                $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1993
1994 25
                return $this->affected_rows;
1995
            }
1996
1997
            // log the ? query
1998 25
            $this->debug->logQuery($sql, $query_duration, 0);
1999
2000 25
            return true;
2001
        }
2002
2003
        // log the error query
2004 33
        $this->debug->logQuery($sql, $query_duration, 0, true);
2005
2006 33
        if ($queryException) {
2007
            return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
2008
        }
2009
2010 33
        if ($this->mysqli_link) {
2011 33
            return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
2012
        }
2013
2014
        return false;
2015
    }
2016
2017
    /**
2018
     * Error-handling for the sql-query.
2019
     *
2020
     * @param string     $errorMessage
2021
     * @param int        $errorNumber
2022
     * @param string     $sql
2023
     * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
2024
     * @param bool       $sqlMultiQuery
2025
     *
2026
     * @throws QueryException
2027
     * @throws DBGoneAwayException
2028
     *
2029
     * @return false|mixed
2030
     */
2031 39
    private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
2032
    {
2033
        if (
2034 39
            $errorMessage === 'DB server has gone away'
2035
            ||
2036 36
            $errorMessage === 'MySQL server has gone away'
2037
            ||
2038 39
            $errorNumber === 2006
2039
        ) {
2040 3
            static $RECONNECT_COUNTER;
2041
2042
            // exit if we have more then 3 "DB server has gone away"-errors
2043 3
            if ($RECONNECT_COUNTER > 3) {
2044
                $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
2045
2046
                throw new DBGoneAwayException($errorMessage);
2047
            }
2048
2049 3
            $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2050
2051
            // reconnect
2052 3
            $RECONNECT_COUNTER++;
2053 3
            $this->reconnect(true);
2054
2055
            // re-run the current (non multi) query
2056 3
            if (!$sqlMultiQuery) {
2057 3
                return $this->query($sql, $sqlParams);
2058
            }
2059
2060
            return false;
2061
        }
2062
2063 36
        $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2064
2065 36
        $force_exception_after_error = null; // auto
2066 36
        if ($this->in_transaction) {
2067 12
            $force_exception_after_error = false;
2068
        }
2069
        // this query returned an error, we must display it (only for dev) !!!
2070
2071 36
        $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2072
2073 36
        return false;
2074
    }
2075
2076
    /**
2077
     * Quote && Escape e.g. a table name string.
2078
     *
2079
     * @param mixed $str
2080
     *
2081
     * @return string
2082
     */
2083 86
    public function quote_string($str): string
2084
    {
2085 86
        $str = \str_replace(
2086 86
            '`',
2087 86
            '``',
2088 86
            \trim(
2089 86
                (string) $this->escape($str, false),
2090 86
                '`'
2091
            )
2092
        );
2093
2094 86
        return '`' . $str . '`';
2095
    }
2096
2097
    /**
2098
     * Reconnect to the MySQL-Server.
2099
     *
2100
     * @param bool $checkViaPing
2101
     *
2102
     * @return bool
2103
     */
2104 7
    public function reconnect(bool $checkViaPing = false): bool
2105
    {
2106 7
        $ping = false;
2107 7
        if ($checkViaPing) {
2108 6
            $ping = $this->ping();
2109
        }
2110
2111 7
        if (!$ping) {
2112 7
            $this->connected = false;
2113 7
            $this->connect();
2114
        }
2115
2116 7
        return $this->isReady();
2117
    }
2118
2119
    /**
2120
     * Execute a "replace"-query.
2121
     *
2122
     * @param string      $table
2123
     * @param array       $data
2124
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2125
     *
2126
     * @throws QueryException
2127
     *
2128
     * @return false|int
2129
     *                   <p>false on error</p>
2130
     */
2131 3
    public function replace(string $table, array $data = [], string $databaseName = null)
2132
    {
2133
        // init
2134 3
        $table = \trim($table);
2135
2136 3
        if ($table === '') {
2137 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2138
2139 3
            return false;
2140
        }
2141
2142 3
        if (\count($data) === 0) {
2143 3
            $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2144
2145 3
            return false;
2146
        }
2147
2148
        // extracting column names
2149 3
        $columns = \array_keys($data);
2150 3
        foreach ($columns as $k => $_key) {
2151 3
            $columns[$k] = $this->quote_string($_key);
2152
        }
2153
2154 3
        $columns = \implode(',', $columns);
2155
2156
        // extracting values
2157 3
        foreach ($data as $k => $_value) {
2158 3
            $data[$k] = $this->secure($_value);
2159
        }
2160 3
        $values = \implode(',', $data);
2161
2162 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...
2163
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2164
        }
2165
2166 3
        $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " (${columns}) VALUES (${values})";
2167
2168 3
        $return = $this->query($sql);
2169 3
        \assert(\is_int($return) || $return === false);
2170
2171 3
        return $return;
2172
    }
2173
2174
    /**
2175
     * Rollback in a transaction and end the transaction.
2176
     *
2177
     * @return bool
2178
     *              <p>bool true on success, false otherwise.</p>
2179
     */
2180 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...
2181
    {
2182 12
        if (!$this->in_transaction) {
2183
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
2184
2185
            return false;
2186
        }
2187
2188
        // init
2189 12
        $return = false;
2190
2191 12
        if ($this->mysqli_link) {
2192 12
            $return = \mysqli_rollback($this->mysqli_link);
2193 12
            \mysqli_autocommit($this->mysqli_link, true);
2194
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2195
            $this->doctrine_connection->rollBack();
2196
            $this->doctrine_connection->setAutoCommit(true);
2197
2198
            if ($this->doctrine_connection->isAutoCommit()) {
2199
                $return = true;
2200
            } else {
2201
                $return = false;
2202
            }
2203
        }
2204
2205 12
        $this->in_transaction = false;
2206
2207 12
        return $return;
2208
    }
2209
2210
    /**
2211
     * Try to secure a variable, so can you use it in sql-queries.
2212
     *
2213
     * <p>
2214
     * <strong>int:</strong> (also strings that contains only an int-value)<br />
2215
     * 1. parse into "int"
2216
     * </p><br />
2217
     *
2218
     * <p>
2219
     * <strong>float:</strong><br />
2220
     * 1. return "float"
2221
     * </p><br />
2222
     *
2223
     * <p>
2224
     * <strong>string:</strong><br />
2225
     * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2226
     * 2. trim '<br />
2227
     * 3. escape the string (and remove non utf-8 chars)<br />
2228
     * 4. trim ' again (because we maybe removed some chars)<br />
2229
     * 5. add ' around the new string<br />
2230
     * </p><br />
2231
     *
2232
     * <p>
2233
     * <strong>array:</strong><br />
2234
     * 1. return null
2235
     * </p><br />
2236
     *
2237
     * <p>
2238
     * <strong>object:</strong><br />
2239
     * 1. return false
2240
     * </p><br />
2241
     *
2242
     * <p>
2243
     * <strong>null:</strong><br />
2244
     * 1. return null
2245
     * </p>
2246
     *
2247
     * @param mixed     $var
2248
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2249
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2250
     *                                 <strong>null</strong> => Convert the array into null, every time.
2251
     *
2252
     * @return mixed
2253
     */
2254 87
    public function secure($var, $convert_array = true)
2255
    {
2256 87
        if (\is_array($var)) {
2257 6
            if ($convert_array === null) {
2258
                if ($this->convert_null_to_empty_string) {
2259
                    $var = "''";
2260
                } else {
2261
                    $var = 'NULL';
2262
                }
2263
            } else {
2264 6
                $varCleaned = [];
2265 6
                foreach ((array) $var as $key => $value) {
2266 6
                    $key = $this->escape($key, false, false, $convert_array);
2267 6
                    $value = $this->secure($value);
2268
2269
                    /** @noinspection OffsetOperationsInspection */
2270 6
                    $varCleaned[$key] = $value;
2271
                }
2272
2273 6 View Code Duplication
                if ($convert_array === true) {
2274 6
                    $varCleaned = \implode(',', $varCleaned);
2275
2276 6
                    $var = $varCleaned;
2277
                } else {
2278
                    $var = $varCleaned;
2279
                }
2280
            }
2281
2282 6
            return $var;
2283
        }
2284
2285 87
        if ($var === '') {
2286 6
            return "''";
2287
        }
2288
2289 87
        if ($var === "''") {
2290 3
            return "''";
2291
        }
2292
2293 87
        if ($var === null) {
2294 3
            if ($this->convert_null_to_empty_string) {
2295 3
                return "''";
2296
            }
2297
2298 3
            return 'NULL';
2299
        }
2300
2301 87
        if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2302 3
            return $var;
2303
        }
2304
2305 87
        if (\is_string($var)) {
2306 77
            $var = \trim($var, "'");
2307
        }
2308
2309 87
        $var = $this->escape($var, false, false, null);
2310
2311 84
        if (\is_string($var)) {
2312 77
            $var = "'" . \trim($var, "'") . "'";
2313
        }
2314
2315 84
        return $var;
2316
    }
2317
2318
    /**
2319
     * Execute a "select"-query.
2320
     *
2321
     * @param string       $table
2322
     * @param array|string $where
2323
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2324
     *
2325
     * @throws QueryException
2326
     *
2327
     * @return false|Result
2328
     *                      <p>false on error</p>
2329
     */
2330 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...
2331
        string $table,
2332
        $where = '1=1',
2333
        string $databaseName = null
2334
    ) {
2335
        // init
2336 62
        $table = \trim($table);
2337
2338 62
        if ($table === '') {
2339 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2340
2341 3
            return false;
2342
        }
2343
2344 62
        if (\is_string($where)) {
2345 25
            $WHERE = $this->escape($where, false);
2346 41
        } elseif (\is_array($where)) {
2347 41
            $WHERE = $this->_parseArrayPair($where, 'AND');
2348
        } else {
2349 3
            $WHERE = '';
2350
        }
2351
2352 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...
2353
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2354
        }
2355
2356 62
        $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
2357
2358 62
        $return = $this->query($sql);
2359 62
        \assert($return instanceof Result || $return === false);
2360
2361 62
        return $return;
2362
    }
2363
2364
    /**
2365
     * Selects a different database than the one specified on construction.
2366
     *
2367
     * @param string $database <p>Database name to switch to.</p>
2368
     *
2369
     * @return bool
2370
     *              <p>bool true on success, false otherwise.</p>
2371
     */
2372
    public function select_db(string $database): bool
2373
    {
2374
        if (!$this->isReady()) {
2375
            return false;
2376
        }
2377
2378
        if ($this->mysqli_link) {
2379
            return \mysqli_select_db($this->mysqli_link, $database);
2380
        }
2381
2382
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2383
            $return = $this->query('use :database', ['database' => $database]);
2384
            \assert(\is_bool($return));
2385
2386
            return $return;
2387
        }
2388
2389
        return false;
2390
    }
2391
2392
    /**
2393
     * @param array  $extra_config          <p>
2394
     *                                      'session_to_db' => bool<br>
2395
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
2396
     *                                      'socket'        => string (path)<br>
2397
     *                                      'flags'         => null|int<br>
2398
     *                                      'ssl'           => bool<br>
2399
     *                                      'clientkey'     => string (path)<br>
2400
     *                                      'clientcert'    => string (path)<br>
2401
     *                                      'cacert'        => string (path)<br>
2402
     *                                      </p>
2403
     */
2404 24
    public function setConfigExtra(array $extra_config)
2405
    {
2406 24
        if (isset($extra_config['session_to_db'])) {
2407
            $this->session_to_db = (bool) $extra_config['session_to_db'];
2408
        }
2409
2410 24
        if (isset($extra_config['doctrine'])) {
2411
            if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2412
                $this->doctrine_connection = $extra_config['doctrine'];
2413
            } else {
2414
                throw new DBConnectException('Error "doctrine"-connection is not valid');
2415
            }
2416
        }
2417
2418 24
        if (isset($extra_config['socket'])) {
2419
            $this->socket = $extra_config['socket'];
2420
        }
2421
2422 24
        if (isset($extra_config['flags'])) {
2423 1
            $this->flags = $extra_config['flags'];
2424
        }
2425
2426 24
        if (isset($extra_config['ssl'])) {
2427
            $this->ssl = $extra_config['ssl'];
2428
        }
2429
2430 24
        if (isset($extra_config['clientkey'])) {
2431
            $this->clientkey = $extra_config['clientkey'];
2432
        }
2433
2434 24
        if (isset($extra_config['clientcert'])) {
2435
            $this->clientcert = $extra_config['clientcert'];
2436
        }
2437
2438 24
        if (isset($extra_config['cacert'])) {
2439
            $this->cacert = $extra_config['cacert'];
2440
        }
2441 24
    }
2442
2443
    /**
2444
     * Set the current charset.
2445
     *
2446
     * @param string $charset
2447
     *
2448
     * @return bool
2449
     */
2450 15
    public function set_charset(string $charset): bool
2451
    {
2452 15
        $charsetLower = \strtolower($charset);
2453 15
        if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2454 9
            $charset = 'utf8';
2455
        }
2456 15
        if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this)) {
2457 9
            $charset = 'utf8mb4';
2458
        }
2459
2460 15
        $this->charset = $charset;
2461
2462
        if (
2463 15
            $this->mysqli_link
2464
            &&
2465 15
            $this->mysqli_link instanceof \mysqli
2466
        ) {
2467 15
            $return = \mysqli_set_charset($this->mysqli_link, $charset);
2468
2469
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2470 15
            @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2471
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2472 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...
2473
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2474
            $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2475
            if (!$doctrineWrappedConnection instanceof Connection) {
2476
                return false;
2477
            }
2478
2479
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2480
            @$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...
2481
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2482
            @$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...
2483
2484
            $return = true;
2485
        } else {
2486
            $return = false;
2487
        }
2488
2489 15
        return $return;
2490
    }
2491
2492
    /**
2493
     * Set the option to convert null to "''" (empty string).
2494
     *
2495
     * Used in secure() => select(), insert(), update(), delete()
2496
     *
2497
     * @deprecated It's not recommended to convert NULL into an empty string!
2498
     *
2499
     * @param bool $bool
2500
     *
2501
     * @return self
2502
     */
2503 3
    public function set_convert_null_to_empty_string(bool $bool): self
2504
    {
2505 3
        $this->convert_null_to_empty_string = $bool;
2506
2507 3
        return $this;
2508
    }
2509
2510
    /**
2511
     * Enables or disables internal report functions
2512
     *
2513
     * @see http://php.net/manual/en/function.mysqli-report.php
2514
     *
2515
     * @param int $flags <p>
2516
     *                   <table>
2517
     *                   Supported flags
2518
     *                   <tr valign="top">
2519
     *                   <td>Name</td>
2520
     *                   <td>Description</td>
2521
     *                   </tr>
2522
     *                   <tr valign="top">
2523
     *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2524
     *                   <td>Turns reporting off</td>
2525
     *                   </tr>
2526
     *                   <tr valign="top">
2527
     *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2528
     *                   <td>Report errors from mysqli function calls</td>
2529
     *                   </tr>
2530
     *                   <tr valign="top">
2531
     *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2532
     *                   <td>
2533
     *                   Throw <b>mysqli_sql_exception</b> for errors
2534
     *                   instead of warnings
2535
     *                   </td>
2536
     *                   </tr>
2537
     *                   <tr valign="top">
2538
     *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2539
     *                   <td>Report if no index or bad index was used in a query</td>
2540
     *                   </tr>
2541
     *                   <tr valign="top">
2542
     *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2543
     *                   <td>Set all options (report all)</td>
2544
     *                   </tr>
2545
     *                   </table>
2546
     *                   </p>
2547
     *
2548
     * @return bool
2549
     */
2550
    public function set_mysqli_report(int $flags): bool
2551
    {
2552
        if (
2553
            $this->mysqli_link
2554
            &&
2555
            $this->mysqli_link instanceof \mysqli
2556
        ) {
2557
            return \mysqli_report($flags);
2558
        }
2559
2560
        return false;
2561
    }
2562
2563
    /**
2564
     * Show config errors by throw exceptions.
2565
     *
2566
     * @throws \InvalidArgumentException
2567
     *
2568
     * @return bool
2569
     */
2570 24
    public function showConfigError(): bool
2571
    {
2572
        // check if a doctrine connection is already open, first
2573
        if (
2574 24
            $this->doctrine_connection
2575
            &&
2576 24
            $this->doctrine_connection->isConnected()
2577
        ) {
2578
            return true;
2579
        }
2580
2581
        if (
2582 24
            !$this->hostname
2583
            ||
2584 21
            !$this->username
2585
            ||
2586 24
            !$this->database
2587
        ) {
2588 9
            if (!$this->hostname) {
2589 3
                throw new \InvalidArgumentException('no-sql-hostname');
2590
            }
2591
2592 6
            if (!$this->username) {
2593 3
                throw new \InvalidArgumentException('no-sql-username');
2594
            }
2595
2596 3
            if (!$this->database) {
2597 3
                throw new \InvalidArgumentException('no-sql-database');
2598
            }
2599
2600
            return false;
2601
        }
2602
2603 15
        return true;
2604
    }
2605
2606
    /**
2607
     * alias: "beginTransaction()"
2608
     */
2609 3
    public function startTransaction(): bool
2610
    {
2611 3
        return $this->beginTransaction();
2612
    }
2613
2614
    /**
2615
     * Determine if database table exists
2616
     *
2617
     * @param string $table
2618
     *
2619
     * @return bool
2620
     */
2621 3
    public function table_exists(string $table): bool
2622
    {
2623 3
        $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2624
2625 3
        return $check !== false
2626
               &&
2627 3
               $check instanceof Result
2628
               &&
2629 3
               $check->num_rows > 0;
2630
    }
2631
2632
    /**
2633
     * Execute a callback inside a transaction.
2634
     *
2635
     * @param \Closure $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2636
     *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2637
     *
2638
     * @return bool
2639
     *              <p>bool true on success, false otherwise.</p>
2640
     */
2641 3
    public function transact($callback): bool
2642
    {
2643
        try {
2644 3
            $beginTransaction = $this->beginTransaction();
2645 3
            if (!$beginTransaction) {
2646 3
                $this->debug->displayError('Error: transact -> can not start transaction!', false);
2647
2648 3
                return false;
2649
            }
2650
2651 3
            $result = $callback($this);
2652 3
            if ($result === false) {
2653
                /** @noinspection ThrowRawExceptionInspection */
2654 3
                throw new \Exception('call_user_func [' . \print_r($callback, true) . '] === false');
2655
            }
2656
2657 3
            return $this->commit();
2658 3
        } catch (\Exception $e) {
2659 3
            $this->rollback();
2660
2661 3
            return false;
2662
        }
2663
    }
2664
2665
    /**
2666
     * Execute a "update"-query.
2667
     *
2668
     * @param string       $table
2669
     * @param array        $data
2670
     * @param array|string $where
2671
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2672
     *
2673
     * @throws QueryException
2674
     *
2675
     * @return false|int
2676
     *                   <p>false on error</p>
2677
     */
2678 21
    public function update(
2679
        string $table,
2680
        array $data = [],
2681
        $where = '1=1',
2682
        string $databaseName = null
2683
    ) {
2684
        // init
2685 21
        $table = \trim($table);
2686
2687 21
        if ($table === '') {
2688 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2689
2690 3
            return false;
2691
        }
2692
2693 21
        if (\count($data) === 0) {
2694 6
            $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2695
2696 6
            return false;
2697
        }
2698
2699
        // DEBUG
2700
        //var_dump($data);
2701
2702 21
        $SET = $this->_parseArrayPair($data);
2703
2704
        // DEBUG
2705
        //var_dump($SET);
2706
2707 21
        if (\is_string($where)) {
2708 6
            $WHERE = $this->escape($where, false);
2709 18
        } elseif (\is_array($where)) {
2710 15
            $WHERE = $this->_parseArrayPair($where, 'AND');
2711
        } else {
2712 3
            $WHERE = '';
2713
        }
2714
2715 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...
2716
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2717
        }
2718
2719 21
        $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET ${SET} WHERE (${WHERE})";
2720
2721 21
        $return = $this->query($sql);
2722 21
        \assert(\is_int($return) || $return === false);
2723
2724 21
        return $return;
2725
    }
2726
}
2727