Completed
Push — master ( 195702...f3dab2 )
by Lars
02:13 queued 15s
created

DB::connect_helper()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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