• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.61
/system/Database/MySQLi/Connection.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database\MySQLi;
15

16
use CodeIgniter\Database\BaseConnection;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\TableName;
19
use CodeIgniter\Exceptions\LogicException;
20
use mysqli;
21
use mysqli_result;
22
use mysqli_sql_exception;
23
use stdClass;
24
use Throwable;
25

26
/**
27
 * Connection for MySQLi
28
 *
29
 * @extends BaseConnection<mysqli, mysqli_result>
30
 */
31
class Connection extends BaseConnection
32
{
33
    /**
34
     * Database driver
35
     *
36
     * @var string
37
     */
38
    public $DBDriver = 'MySQLi';
39

40
    /**
41
     * DELETE hack flag
42
     *
43
     * Whether to use the MySQL "delete hack" which allows the number
44
     * of affected rows to be shown. Uses a preg_replace when enabled,
45
     * adding a bit more processing to all queries.
46
     *
47
     * @var bool
48
     */
49
    public $deleteHack = true;
50

51
    /**
52
     * Identifier escape character
53
     *
54
     * @var string
55
     */
56
    public $escapeChar = '`';
57

58
    /**
59
     * MySQLi object
60
     *
61
     * Has to be preserved without being assigned to $connId.
62
     *
63
     * @var false|mysqli
64
     */
65
    public $mysqli;
66

67
    /**
68
     * MySQLi constant
69
     *
70
     * For unbuffered queries use `MYSQLI_USE_RESULT`.
71
     *
72
     * Default mode for buffered queries uses `MYSQLI_STORE_RESULT`.
73
     *
74
     * @var int
75
     */
76
    public $resultMode = MYSQLI_STORE_RESULT;
77

78
    /**
79
     * Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE
80
     *
81
     * @var bool
82
     */
83
    public $numberNative = false;
84

85
    /**
86
     * Use MYSQLI_CLIENT_FOUND_ROWS
87
     *
88
     * Whether affectedRows() should return number of rows found,
89
     * or number of rows changed, after an UPDATE query.
90
     *
91
     * @var bool
92
     */
93
    public $foundRows = false;
94

95
    /**
96
     * Connect to the database.
97
     *
98
     * @return false|mysqli
99
     *
100
     * @throws DatabaseException
101
     */
102
    public function connect(bool $persistent = false)
103
    {
104
        // Do we have a socket path?
105
        if ($this->hostname[0] === '/') {
27✔
106
            $hostname = null;
×
107
            $port     = null;
×
108
            $socket   = $this->hostname;
×
109
        } else {
110
            $hostname = $persistent ? 'p:' . $this->hostname : $this->hostname;
27✔
111
            $port     = empty($this->port) ? null : $this->port;
27✔
112
            $socket   = '';
27✔
113
        }
114

115
        $clientFlags  = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
27✔
116
        $this->mysqli = mysqli_init();
27✔
117

118
        mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
27✔
119

120
        $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
27✔
121

122
        if ($this->numberNative === true) {
27✔
123
            $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
1✔
124
        }
125

126
        if (isset($this->strictOn)) {
27✔
127
            if ($this->strictOn) {
27✔
128
                $this->mysqli->options(
1✔
129
                    MYSQLI_INIT_COMMAND,
1✔
130
                    "SET SESSION sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')"
1✔
131
                );
1✔
132
            } else {
133
                $this->mysqli->options(
26✔
134
                    MYSQLI_INIT_COMMAND,
26✔
135
                    "SET SESSION sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
26✔
136
                                        @@sql_mode,
137
                                        'STRICT_ALL_TABLES,', ''),
138
                                    ',STRICT_ALL_TABLES', ''),
139
                                'STRICT_ALL_TABLES', ''),
140
                            'STRICT_TRANS_TABLES,', ''),
141
                        ',STRICT_TRANS_TABLES', ''),
142
                    'STRICT_TRANS_TABLES', '')"
26✔
143
                );
26✔
144
            }
145
        }
146

147
        if (is_array($this->encrypt)) {
27✔
148
            $ssl = [];
×
149

150
            if (! empty($this->encrypt['ssl_key'])) {
×
151
                $ssl['key'] = $this->encrypt['ssl_key'];
×
152
            }
153
            if (! empty($this->encrypt['ssl_cert'])) {
×
154
                $ssl['cert'] = $this->encrypt['ssl_cert'];
×
155
            }
156
            if (! empty($this->encrypt['ssl_ca'])) {
×
157
                $ssl['ca'] = $this->encrypt['ssl_ca'];
×
158
            }
159
            if (! empty($this->encrypt['ssl_capath'])) {
×
160
                $ssl['capath'] = $this->encrypt['ssl_capath'];
×
161
            }
162
            if (! empty($this->encrypt['ssl_cipher'])) {
×
163
                $ssl['cipher'] = $this->encrypt['ssl_cipher'];
×
164
            }
165

166
            if ($ssl !== []) {
×
167
                if (isset($this->encrypt['ssl_verify'])) {
×
168
                    if ($this->encrypt['ssl_verify']) {
×
169
                        if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
×
170
                            $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
×
171
                        }
172
                    }
173
                    // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
174
                    // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
175
                    // constant ...
176
                    //
177
                    // https://secure.php.net/ChangeLog-5.php#5.6.16
178
                    // https://bugs.php.net/bug.php?id=68344
179
                    elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
×
180
                        $clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
×
181
                    }
182
                }
183

184
                $this->mysqli->ssl_set(
×
185
                    $ssl['key'] ?? null,
×
186
                    $ssl['cert'] ?? null,
×
187
                    $ssl['ca'] ?? null,
×
188
                    $ssl['capath'] ?? null,
×
189
                    $ssl['cipher'] ?? null
×
190
                );
×
191
            }
192

193
            $clientFlags += MYSQLI_CLIENT_SSL;
×
194
        }
195

196
        if ($this->foundRows) {
27✔
197
            $clientFlags += MYSQLI_CLIENT_FOUND_ROWS;
1✔
198
        }
199

200
        try {
201
            if ($this->mysqli->real_connect(
27✔
202
                $hostname,
27✔
203
                $this->username,
27✔
204
                $this->password,
27✔
205
                $this->database,
27✔
206
                $port,
27✔
207
                $socket,
27✔
208
                $clientFlags
27✔
209
            )) {
27✔
210
                // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
211
                if (($clientFlags & MYSQLI_CLIENT_SSL) !== 0 && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=')
27✔
212
                    && empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value)
27✔
213
                ) {
214
                    $this->mysqli->close();
×
215
                    $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
×
216
                    log_message('error', $message);
×
217

218
                    if ($this->DBDebug) {
×
219
                        throw new DatabaseException($message);
×
220
                    }
221

222
                    return false;
×
223
                }
224

225
                if (! $this->mysqli->set_charset($this->charset)) {
27✔
226
                    log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
×
227

228
                    $this->mysqli->close();
×
229

230
                    if ($this->DBDebug) {
×
231
                        throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
×
232
                    }
233

234
                    return false;
×
235
                }
236

237
                return $this->mysqli;
27✔
238
            }
239
        } catch (Throwable $e) {
1✔
240
            // Clean sensitive information from errors.
241
            $msg = $e->getMessage();
1✔
242

243
            $msg = str_replace($this->username, '****', $msg);
1✔
244
            $msg = str_replace($this->password, '****', $msg);
1✔
245

246
            throw new DatabaseException($msg, $e->getCode(), $e);
1✔
247
        }
248

249
        return false;
×
250
    }
251

252
    /**
253
     * Keep or establish the connection if no queries have been sent for
254
     * a length of time exceeding the server's idle timeout.
255
     *
256
     * @return void
257
     */
258
    public function reconnect()
259
    {
260
        $this->close();
×
261
        $this->initialize();
×
262
    }
263

264
    /**
265
     * Close the database connection.
266
     *
267
     * @return void
268
     */
269
    protected function _close()
270
    {
271
        $this->connID->close();
1✔
272
    }
273

274
    /**
275
     * Select a specific database table to use.
276
     */
277
    public function setDatabase(string $databaseName): bool
278
    {
279
        if ($databaseName === '') {
×
280
            $databaseName = $this->database;
×
281
        }
282

283
        if (empty($this->connID)) {
×
284
            $this->initialize();
×
285
        }
286

287
        if ($this->connID->select_db($databaseName)) {
×
288
            $this->database = $databaseName;
×
289

290
            return true;
×
291
        }
292

293
        return false;
×
294
    }
295

296
    /**
297
     * Returns a string containing the version of the database being used.
298
     */
299
    public function getVersion(): string
300
    {
301
        if (isset($this->dataCache['version'])) {
4✔
302
            return $this->dataCache['version'];
2✔
303
        }
304

305
        if (empty($this->mysqli)) {
3✔
306
            $this->initialize();
×
307
        }
308

309
        return $this->dataCache['version'] = $this->mysqli->server_info;
3✔
310
    }
311

312
    /**
313
     * Executes the query against the database.
314
     *
315
     * @return false|mysqli_result
316
     */
317
    protected function execute(string $sql)
318
    {
319
        while ($this->connID->more_results()) {
696✔
320
            $this->connID->next_result();
×
321
            if ($res = $this->connID->store_result()) {
×
322
                $res->free();
×
323
            }
324
        }
325

326
        try {
327
            return $this->connID->query($this->prepQuery($sql), $this->resultMode);
696✔
328
        } catch (mysqli_sql_exception $e) {
30✔
329
            log_message('error', (string) $e);
30✔
330

331
            if ($this->DBDebug) {
30✔
332
                throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
13✔
333
            }
334
        }
335

336
        return false;
17✔
337
    }
338

339
    /**
340
     * Prep the query. If needed, each database adapter can prep the query string
341
     */
342
    protected function prepQuery(string $sql): string
343
    {
344
        // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
345
        // modifies the query so that it a proper number of affected rows is returned.
346
        if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
696✔
347
            return trim($sql) . ' WHERE 1=1';
3✔
348
        }
349

350
        return $sql;
696✔
351
    }
352

353
    /**
354
     * Returns the total number of rows affected by this query.
355
     */
356
    public function affectedRows(): int
357
    {
358
        return $this->connID->affected_rows ?? 0;
60✔
359
    }
360

361
    /**
362
     * Platform-dependant string escape
363
     */
364
    protected function _escapeString(string $str): string
365
    {
366
        if (! $this->connID) {
670✔
367
            $this->initialize();
1✔
368
        }
369

370
        return $this->connID->real_escape_string($str);
670✔
371
    }
372

373
    /**
374
     * Escape Like String Direct
375
     * There are a few instances where MySQLi queries cannot take the
376
     * additional "ESCAPE x" parameter for specifying the escape character
377
     * in "LIKE" strings, and this handles those directly with a backslash.
378
     *
379
     * @param list<string>|string $str Input string
380
     *
381
     * @return list<string>|string
382
     */
383
    public function escapeLikeStringDirect($str)
384
    {
385
        if (is_array($str)) {
1✔
386
            foreach ($str as $key => $val) {
×
387
                $str[$key] = $this->escapeLikeStringDirect($val);
×
388
            }
389

390
            return $str;
×
391
        }
392

393
        $str = $this->_escapeString($str);
1✔
394

395
        // Escape LIKE condition wildcards
396
        return str_replace(
1✔
397
            [$this->likeEscapeChar, '%', '_'],
1✔
398
            ['\\' . $this->likeEscapeChar, '\\%', '\\_'],
1✔
399
            $str
1✔
400
        );
1✔
401
    }
402

403
    /**
404
     * Generates the SQL for listing tables in a platform-dependent manner.
405
     * Uses escapeLikeStringDirect().
406
     *
407
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
408
     */
409
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
410
    {
411
        $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
617✔
412

413
        if ((string) $tableName !== '') {
617✔
414
            return $sql . ' LIKE ' . $this->escape($tableName);
617✔
415
        }
416

417
        if ($prefixLimit && $this->DBPrefix !== '') {
31✔
418
            return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
×
419
        }
420

421
        return $sql;
31✔
422
    }
423

424
    /**
425
     * Generates a platform-specific query string so that the column names can be fetched.
426
     *
427
     * @param string|TableName $table
428
     */
429
    protected function _listColumns($table = ''): string
430
    {
431
        $tableName = $this->protectIdentifiers(
8✔
432
            $table,
8✔
433
            true,
8✔
434
            null,
8✔
435
            false
8✔
436
        );
8✔
437

438
        return 'SHOW COLUMNS FROM ' . $tableName;
8✔
439
    }
440

441
    /**
442
     * Returns an array of objects with field data
443
     *
444
     * @return list<stdClass>
445
     *
446
     * @throws DatabaseException
447
     */
448
    protected function _fieldData(string $table): array
449
    {
450
        $table = $this->protectIdentifiers($table, true, null, false);
13✔
451

452
        if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
13✔
453
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
454
        }
455
        $query = $query->getResultObject();
13✔
456

457
        $retVal = [];
13✔
458

459
        for ($i = 0, $c = count($query); $i < $c; $i++) {
13✔
460
            $retVal[$i]       = new stdClass();
13✔
461
            $retVal[$i]->name = $query[$i]->Field;
13✔
462

463
            sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
13✔
464

465
            $retVal[$i]->nullable    = $query[$i]->Null === 'YES';
13✔
466
            $retVal[$i]->default     = $query[$i]->Default;
13✔
467
            $retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
13✔
468
        }
469

470
        return $retVal;
13✔
471
    }
472

473
    /**
474
     * Returns an array of objects with index data
475
     *
476
     * @return array<string, stdClass>
477
     *
478
     * @throws DatabaseException
479
     * @throws LogicException
480
     */
481
    protected function _indexData(string $table): array
482
    {
483
        $table = $this->protectIdentifiers($table, true, null, false);
8✔
484

485
        if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
8✔
486
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
487
        }
488

489
        $indexes = $query->getResultArray();
8✔
490

491
        if ($indexes === []) {
8✔
492
            return [];
3✔
493
        }
494

495
        $keys = [];
7✔
496

497
        foreach ($indexes as $index) {
7✔
498
            if (empty($keys[$index['Key_name']])) {
7✔
499
                $keys[$index['Key_name']]       = new stdClass();
7✔
500
                $keys[$index['Key_name']]->name = $index['Key_name'];
7✔
501

502
                if ($index['Key_name'] === 'PRIMARY') {
7✔
503
                    $type = 'PRIMARY';
5✔
504
                } elseif ($index['Index_type'] === 'FULLTEXT') {
5✔
505
                    $type = 'FULLTEXT';
×
506
                } elseif ($index['Non_unique']) {
5✔
507
                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
5✔
508
                } else {
509
                    $type = 'UNIQUE';
3✔
510
                }
511

512
                $keys[$index['Key_name']]->type = $type;
7✔
513
            }
514

515
            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
7✔
516
        }
517

518
        return $keys;
7✔
519
    }
520

521
    /**
522
     * Returns an array of objects with Foreign key data
523
     *
524
     * @return array<string, stdClass>
525
     *
526
     * @throws DatabaseException
527
     */
528
    protected function _foreignKeyData(string $table): array
529
    {
530
        $sql = '
6✔
531
                SELECT
532
                    tc.CONSTRAINT_NAME,
533
                    tc.TABLE_NAME,
534
                    kcu.COLUMN_NAME,
535
                    rc.REFERENCED_TABLE_NAME,
536
                    kcu.REFERENCED_COLUMN_NAME,
537
                    rc.DELETE_RULE,
538
                    rc.UPDATE_RULE,
539
                    rc.MATCH_OPTION
540
                FROM information_schema.table_constraints AS tc
541
                INNER JOIN information_schema.referential_constraints AS rc
542
                    ON tc.constraint_name = rc.constraint_name
543
                    AND tc.constraint_schema = rc.constraint_schema
544
                INNER JOIN information_schema.key_column_usage AS kcu
545
                    ON tc.constraint_name = kcu.constraint_name
546
                    AND tc.constraint_schema = kcu.constraint_schema
547
                WHERE
548
                    tc.constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
6✔
549
                    tc.table_schema = ' . $this->escape($this->database) . ' AND
6✔
550
                    tc.table_name = ' . $this->escape($table);
6✔
551

552
        if (($query = $this->query($sql)) === false) {
6✔
553
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
554
        }
555

556
        $query   = $query->getResultObject();
6✔
557
        $indexes = [];
6✔
558

559
        foreach ($query as $row) {
6✔
560
            $indexes[$row->CONSTRAINT_NAME]['constraint_name']       = $row->CONSTRAINT_NAME;
5✔
561
            $indexes[$row->CONSTRAINT_NAME]['table_name']            = $row->TABLE_NAME;
5✔
562
            $indexes[$row->CONSTRAINT_NAME]['column_name'][]         = $row->COLUMN_NAME;
5✔
563
            $indexes[$row->CONSTRAINT_NAME]['foreign_table_name']    = $row->REFERENCED_TABLE_NAME;
5✔
564
            $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
5✔
565
            $indexes[$row->CONSTRAINT_NAME]['on_delete']             = $row->DELETE_RULE;
5✔
566
            $indexes[$row->CONSTRAINT_NAME]['on_update']             = $row->UPDATE_RULE;
5✔
567
            $indexes[$row->CONSTRAINT_NAME]['match']                 = $row->MATCH_OPTION;
5✔
568
        }
569

570
        return $this->foreignKeyDataToObjects($indexes);
6✔
571
    }
572

573
    /**
574
     * Returns platform-specific SQL to disable foreign key checks.
575
     *
576
     * @return string
577
     */
578
    protected function _disableForeignKeyChecks()
579
    {
580
        return 'SET FOREIGN_KEY_CHECKS=0';
622✔
581
    }
582

583
    /**
584
     * Returns platform-specific SQL to enable foreign key checks.
585
     *
586
     * @return string
587
     */
588
    protected function _enableForeignKeyChecks()
589
    {
590
        return 'SET FOREIGN_KEY_CHECKS=1';
622✔
591
    }
592

593
    /**
594
     * Returns the last error code and message.
595
     * Must return this format: ['code' => string|int, 'message' => string]
596
     * intval(code) === 0 means "no error".
597
     *
598
     * @return array<string, int|string>
599
     */
600
    public function error(): array
601
    {
602
        if (! empty($this->mysqli->connect_errno)) {
2✔
603
            return [
×
604
                'code'    => $this->mysqli->connect_errno,
×
605
                'message' => $this->mysqli->connect_error,
×
606
            ];
×
607
        }
608

609
        return [
2✔
610
            'code'    => $this->connID->errno,
2✔
611
            'message' => $this->connID->error,
2✔
612
        ];
2✔
613
    }
614

615
    /**
616
     * Insert ID
617
     */
618
    public function insertID(): int
619
    {
620
        return $this->connID->insert_id;
79✔
621
    }
622

623
    /**
624
     * Begin Transaction
625
     */
626
    protected function _transBegin(): bool
627
    {
628
        $this->connID->autocommit(false);
17✔
629

630
        return $this->connID->begin_transaction();
17✔
631
    }
632

633
    /**
634
     * Commit Transaction
635
     */
636
    protected function _transCommit(): bool
637
    {
638
        if ($this->connID->commit()) {
5✔
639
            $this->connID->autocommit(true);
5✔
640

641
            return true;
5✔
642
        }
643

644
        return false;
×
645
    }
646

647
    /**
648
     * Rollback Transaction
649
     */
650
    protected function _transRollback(): bool
651
    {
652
        if ($this->connID->rollback()) {
17✔
653
            $this->connID->autocommit(true);
17✔
654

655
            return true;
17✔
656
        }
657

658
        return false;
×
659
    }
660
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc