Completed
Push — master ( 34de65...fd537c )
by Lars
02:35
created

DB::insert_id()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.679

Importance

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