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

codeigniter4 / CodeIgniter4 / 21071236966

16 Jan 2026 03:13PM UTC coverage: 84.38% (+0.02%) from 84.361%
21071236966

Pull #9887

github

web-flow
Merge b627410a8 into 5ab52ae74
Pull Request #9887: refactor: Remove dead code from MySQLi Connection related to PHP 5

20895 of 24763 relevant lines covered (84.38%)

195.45 hits per line

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

69.76
/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 ($this->strictOn !== null) {
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
                if (! $this->mysqli->set_charset($this->charset)) {
27✔
211
                    log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
×
212

213
                    $this->mysqli->close();
×
214

215
                    if ($this->DBDebug) {
×
216
                        throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
×
217
                    }
218

219
                    return false;
×
220
                }
221

222
                return $this->mysqli;
27✔
223
            }
224
        } catch (Throwable $e) {
1✔
225
            // Clean sensitive information from errors.
226
            $msg = $e->getMessage();
1✔
227

228
            $msg = str_replace($this->username, '****', $msg);
1✔
229
            $msg = str_replace($this->password, '****', $msg);
1✔
230

231
            throw new DatabaseException($msg, $e->getCode(), $e);
1✔
232
        }
233

234
        return false;
×
235
    }
236

237
    /**
238
     * Keep or establish the connection if no queries have been sent for
239
     * a length of time exceeding the server's idle timeout.
240
     *
241
     * @return void
242
     */
243
    public function reconnect()
244
    {
245
        $this->close();
×
246
        $this->initialize();
×
247
    }
248

249
    /**
250
     * Close the database connection.
251
     *
252
     * @return void
253
     */
254
    protected function _close()
255
    {
256
        $this->connID->close();
1✔
257
    }
258

259
    /**
260
     * Select a specific database table to use.
261
     */
262
    public function setDatabase(string $databaseName): bool
263
    {
264
        if ($databaseName === '') {
×
265
            $databaseName = $this->database;
×
266
        }
267

268
        if (empty($this->connID)) {
×
269
            $this->initialize();
×
270
        }
271

272
        if ($this->connID->select_db($databaseName)) {
×
273
            $this->database = $databaseName;
×
274

275
            return true;
×
276
        }
277

278
        return false;
×
279
    }
280

281
    /**
282
     * Returns a string containing the version of the database being used.
283
     */
284
    public function getVersion(): string
285
    {
286
        if (isset($this->dataCache['version'])) {
4✔
287
            return $this->dataCache['version'];
3✔
288
        }
289

290
        if (empty($this->mysqli)) {
2✔
291
            $this->initialize();
1✔
292
        }
293

294
        return $this->dataCache['version'] = $this->mysqli->server_info;
2✔
295
    }
296

297
    /**
298
     * Executes the query against the database.
299
     *
300
     * @return false|mysqli_result
301
     */
302
    protected function execute(string $sql)
303
    {
304
        while ($this->connID->more_results()) {
702✔
305
            $this->connID->next_result();
×
306
            if ($res = $this->connID->store_result()) {
×
307
                $res->free();
×
308
            }
309
        }
310

311
        try {
312
            return $this->connID->query($this->prepQuery($sql), $this->resultMode);
702✔
313
        } catch (mysqli_sql_exception $e) {
30✔
314
            log_message('error', (string) $e);
30✔
315

316
            if ($this->DBDebug) {
30✔
317
                throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
13✔
318
            }
319
        }
320

321
        return false;
17✔
322
    }
323

324
    /**
325
     * Prep the query. If needed, each database adapter can prep the query string
326
     */
327
    protected function prepQuery(string $sql): string
328
    {
329
        // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
330
        // modifies the query so that it a proper number of affected rows is returned.
331
        if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
702✔
332
            return trim($sql) . ' WHERE 1=1';
3✔
333
        }
334

335
        return $sql;
702✔
336
    }
337

338
    /**
339
     * Returns the total number of rows affected by this query.
340
     */
341
    public function affectedRows(): int
342
    {
343
        return $this->connID->affected_rows ?? 0;
63✔
344
    }
345

346
    /**
347
     * Platform-dependant string escape
348
     */
349
    protected function _escapeString(string $str): string
350
    {
351
        if (! $this->connID) {
676✔
352
            $this->initialize();
1✔
353
        }
354

355
        return $this->connID->real_escape_string($str);
676✔
356
    }
357

358
    /**
359
     * Escape Like String Direct
360
     * There are a few instances where MySQLi queries cannot take the
361
     * additional "ESCAPE x" parameter for specifying the escape character
362
     * in "LIKE" strings, and this handles those directly with a backslash.
363
     *
364
     * @param list<string>|string $str Input string
365
     *
366
     * @return list<string>|string
367
     */
368
    public function escapeLikeStringDirect($str)
369
    {
370
        if (is_array($str)) {
1✔
371
            foreach ($str as $key => $val) {
×
372
                $str[$key] = $this->escapeLikeStringDirect($val);
×
373
            }
374

375
            return $str;
×
376
        }
377

378
        $str = $this->_escapeString($str);
1✔
379

380
        // Escape LIKE condition wildcards
381
        return str_replace(
1✔
382
            [$this->likeEscapeChar, '%', '_'],
1✔
383
            ['\\' . $this->likeEscapeChar, '\\%', '\\_'],
1✔
384
            $str,
1✔
385
        );
1✔
386
    }
387

388
    /**
389
     * Generates the SQL for listing tables in a platform-dependent manner.
390
     * Uses escapeLikeStringDirect().
391
     *
392
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
393
     */
394
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
395
    {
396
        $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
623✔
397

398
        if ((string) $tableName !== '') {
623✔
399
            return $sql . ' LIKE ' . $this->escape($tableName);
623✔
400
        }
401

402
        if ($prefixLimit && $this->DBPrefix !== '') {
31✔
403
            return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
×
404
        }
405

406
        return $sql;
31✔
407
    }
408

409
    /**
410
     * Generates a platform-specific query string so that the column names can be fetched.
411
     *
412
     * @param string|TableName $table
413
     */
414
    protected function _listColumns($table = ''): string
415
    {
416
        $tableName = $this->protectIdentifiers(
8✔
417
            $table,
8✔
418
            true,
8✔
419
            null,
8✔
420
            false,
8✔
421
        );
8✔
422

423
        return 'SHOW COLUMNS FROM ' . $tableName;
8✔
424
    }
425

426
    /**
427
     * Returns an array of objects with field data
428
     *
429
     * @return list<stdClass>
430
     *
431
     * @throws DatabaseException
432
     */
433
    protected function _fieldData(string $table): array
434
    {
435
        $table = $this->protectIdentifiers($table, true, null, false);
14✔
436

437
        if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
14✔
438
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
439
        }
440
        $query = $query->getResultObject();
14✔
441

442
        $retVal = [];
14✔
443

444
        for ($i = 0, $c = count($query); $i < $c; $i++) {
14✔
445
            $retVal[$i]       = new stdClass();
14✔
446
            $retVal[$i]->name = $query[$i]->Field;
14✔
447

448
            sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
14✔
449

450
            $retVal[$i]->nullable    = $query[$i]->Null === 'YES';
14✔
451
            $retVal[$i]->default     = $query[$i]->Default;
14✔
452
            $retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
14✔
453
        }
454

455
        return $retVal;
14✔
456
    }
457

458
    /**
459
     * Returns an array of objects with index data
460
     *
461
     * @return array<string, stdClass>
462
     *
463
     * @throws DatabaseException
464
     * @throws LogicException
465
     */
466
    protected function _indexData(string $table): array
467
    {
468
        $table = $this->protectIdentifiers($table, true, null, false);
8✔
469

470
        if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
8✔
471
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
472
        }
473

474
        $indexes = $query->getResultArray();
8✔
475

476
        if ($indexes === []) {
8✔
477
            return [];
3✔
478
        }
479

480
        $keys = [];
7✔
481

482
        foreach ($indexes as $index) {
7✔
483
            if (empty($keys[$index['Key_name']])) {
7✔
484
                $keys[$index['Key_name']]       = new stdClass();
7✔
485
                $keys[$index['Key_name']]->name = $index['Key_name'];
7✔
486

487
                if ($index['Key_name'] === 'PRIMARY') {
7✔
488
                    $type = 'PRIMARY';
5✔
489
                } elseif ($index['Index_type'] === 'FULLTEXT') {
5✔
490
                    $type = 'FULLTEXT';
×
491
                } elseif ($index['Non_unique']) {
5✔
492
                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
5✔
493
                } else {
494
                    $type = 'UNIQUE';
3✔
495
                }
496

497
                $keys[$index['Key_name']]->type = $type;
7✔
498
            }
499

500
            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
7✔
501
        }
502

503
        return $keys;
7✔
504
    }
505

506
    /**
507
     * Returns an array of objects with Foreign key data
508
     *
509
     * @return array<string, stdClass>
510
     *
511
     * @throws DatabaseException
512
     */
513
    protected function _foreignKeyData(string $table): array
514
    {
515
        $sql = '
6✔
516
                SELECT
517
                    tc.CONSTRAINT_NAME,
518
                    tc.TABLE_NAME,
519
                    kcu.COLUMN_NAME,
520
                    rc.REFERENCED_TABLE_NAME,
521
                    kcu.REFERENCED_COLUMN_NAME,
522
                    rc.DELETE_RULE,
523
                    rc.UPDATE_RULE,
524
                    rc.MATCH_OPTION
525
                FROM information_schema.table_constraints AS tc
526
                INNER JOIN information_schema.referential_constraints AS rc
527
                    ON tc.constraint_name = rc.constraint_name
528
                    AND tc.constraint_schema = rc.constraint_schema
529
                INNER JOIN information_schema.key_column_usage AS kcu
530
                    ON tc.constraint_name = kcu.constraint_name
531
                    AND tc.constraint_schema = kcu.constraint_schema
532
                WHERE
533
                    tc.constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
6✔
534
                    tc.table_schema = ' . $this->escape($this->database) . ' AND
6✔
535
                    tc.table_name = ' . $this->escape($table);
6✔
536

537
        if (($query = $this->query($sql)) === false) {
6✔
538
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
539
        }
540

541
        $query   = $query->getResultObject();
6✔
542
        $indexes = [];
6✔
543

544
        foreach ($query as $row) {
6✔
545
            $indexes[$row->CONSTRAINT_NAME]['constraint_name']       = $row->CONSTRAINT_NAME;
5✔
546
            $indexes[$row->CONSTRAINT_NAME]['table_name']            = $row->TABLE_NAME;
5✔
547
            $indexes[$row->CONSTRAINT_NAME]['column_name'][]         = $row->COLUMN_NAME;
5✔
548
            $indexes[$row->CONSTRAINT_NAME]['foreign_table_name']    = $row->REFERENCED_TABLE_NAME;
5✔
549
            $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
5✔
550
            $indexes[$row->CONSTRAINT_NAME]['on_delete']             = $row->DELETE_RULE;
5✔
551
            $indexes[$row->CONSTRAINT_NAME]['on_update']             = $row->UPDATE_RULE;
5✔
552
            $indexes[$row->CONSTRAINT_NAME]['match']                 = $row->MATCH_OPTION;
5✔
553
        }
554

555
        return $this->foreignKeyDataToObjects($indexes);
6✔
556
    }
557

558
    /**
559
     * Returns platform-specific SQL to disable foreign key checks.
560
     *
561
     * @return string
562
     */
563
    protected function _disableForeignKeyChecks()
564
    {
565
        return 'SET FOREIGN_KEY_CHECKS=0';
628✔
566
    }
567

568
    /**
569
     * Returns platform-specific SQL to enable foreign key checks.
570
     *
571
     * @return string
572
     */
573
    protected function _enableForeignKeyChecks()
574
    {
575
        return 'SET FOREIGN_KEY_CHECKS=1';
628✔
576
    }
577

578
    /**
579
     * Returns the last error code and message.
580
     * Must return this format: ['code' => string|int, 'message' => string]
581
     * intval(code) === 0 means "no error".
582
     *
583
     * @return array<string, int|string>
584
     */
585
    public function error(): array
586
    {
587
        if (! empty($this->mysqli->connect_errno)) {
2✔
588
            return [
×
589
                'code'    => $this->mysqli->connect_errno,
×
590
                'message' => $this->mysqli->connect_error,
×
591
            ];
×
592
        }
593

594
        return [
2✔
595
            'code'    => $this->connID->errno,
2✔
596
            'message' => $this->connID->error,
2✔
597
        ];
2✔
598
    }
599

600
    /**
601
     * Insert ID
602
     */
603
    public function insertID(): int
604
    {
605
        return $this->connID->insert_id;
79✔
606
    }
607

608
    /**
609
     * Begin Transaction
610
     */
611
    protected function _transBegin(): bool
612
    {
613
        $this->connID->autocommit(false);
18✔
614

615
        return $this->connID->begin_transaction();
18✔
616
    }
617

618
    /**
619
     * Commit Transaction
620
     */
621
    protected function _transCommit(): bool
622
    {
623
        if ($this->connID->commit()) {
5✔
624
            $this->connID->autocommit(true);
5✔
625

626
            return true;
5✔
627
        }
628

629
        return false;
×
630
    }
631

632
    /**
633
     * Rollback Transaction
634
     */
635
    protected function _transRollback(): bool
636
    {
637
        if ($this->connID->rollback()) {
18✔
638
            $this->connID->autocommit(true);
18✔
639

640
            return true;
18✔
641
        }
642

643
        return false;
×
644
    }
645
}
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

© 2026 Coveralls, Inc