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

codeigniter4 / CodeIgniter4 / 12739860967

13 Jan 2025 03:03AM UTC coverage: 84.454%. Remained the same
12739860967

push

github

web-flow
chore: add more trailing commas in more places (#9395)

* Apply to parameters

* Apply to array destructuring

* Apply to match

* Apply for arguments

337 of 397 new or added lines in 117 files covered. (84.89%)

1 existing line in 1 file now uncovered.

20464 of 24231 relevant lines covered (84.45%)

189.67 hits per line

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

66.34
/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 LogicException;
19
use mysqli;
20
use mysqli_result;
21
use mysqli_sql_exception;
22
use stdClass;
23
use Throwable;
24

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

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

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

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

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

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

84
    /**
85
     * Connect to the database.
86
     *
87
     * @return false|mysqli
88
     *
89
     * @throws DatabaseException
90
     */
91
    public function connect(bool $persistent = false)
92
    {
93
        // Do we have a socket path?
94
        if ($this->hostname[0] === '/') {
23✔
95
            $hostname = null;
×
96
            $port     = null;
×
97
            $socket   = $this->hostname;
×
98
        } else {
99
            $hostname = $persistent ? 'p:' . $this->hostname : $this->hostname;
23✔
100
            $port     = empty($this->port) ? null : $this->port;
23✔
101
            $socket   = '';
23✔
102
        }
103

104
        $clientFlags  = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
23✔
105
        $this->mysqli = mysqli_init();
23✔
106

107
        mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
23✔
108

109
        $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
23✔
110

111
        if ($this->numberNative === true) {
23✔
112
            $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
1✔
113
        }
114

115
        if (isset($this->strictOn)) {
23✔
116
            if ($this->strictOn) {
23✔
117
                $this->mysqli->options(
1✔
118
                    MYSQLI_INIT_COMMAND,
1✔
119
                    "SET SESSION sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')",
1✔
120
                );
1✔
121
            } else {
122
                $this->mysqli->options(
22✔
123
                    MYSQLI_INIT_COMMAND,
22✔
124
                    "SET SESSION sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
22✔
125
                                        @@sql_mode,
126
                                        'STRICT_ALL_TABLES,', ''),
127
                                    ',STRICT_ALL_TABLES', ''),
128
                                'STRICT_ALL_TABLES', ''),
129
                            'STRICT_TRANS_TABLES,', ''),
130
                        ',STRICT_TRANS_TABLES', ''),
131
                    'STRICT_TRANS_TABLES', '')",
22✔
132
                );
22✔
133
            }
134
        }
135

136
        if (is_array($this->encrypt)) {
23✔
137
            $ssl = [];
×
138

139
            if (! empty($this->encrypt['ssl_key'])) {
×
140
                $ssl['key'] = $this->encrypt['ssl_key'];
×
141
            }
142
            if (! empty($this->encrypt['ssl_cert'])) {
×
143
                $ssl['cert'] = $this->encrypt['ssl_cert'];
×
144
            }
145
            if (! empty($this->encrypt['ssl_ca'])) {
×
146
                $ssl['ca'] = $this->encrypt['ssl_ca'];
×
147
            }
148
            if (! empty($this->encrypt['ssl_capath'])) {
×
149
                $ssl['capath'] = $this->encrypt['ssl_capath'];
×
150
            }
151
            if (! empty($this->encrypt['ssl_cipher'])) {
×
152
                $ssl['cipher'] = $this->encrypt['ssl_cipher'];
×
153
            }
154

155
            if ($ssl !== []) {
×
156
                if (isset($this->encrypt['ssl_verify'])) {
×
157
                    if ($this->encrypt['ssl_verify']) {
×
158
                        if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
×
159
                            $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
×
160
                        }
161
                    }
162
                    // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
163
                    // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
164
                    // constant ...
165
                    //
166
                    // https://secure.php.net/ChangeLog-5.php#5.6.16
167
                    // https://bugs.php.net/bug.php?id=68344
168
                    elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
×
169
                        $clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
×
170
                    }
171
                }
172

173
                $this->mysqli->ssl_set(
×
174
                    $ssl['key'] ?? null,
×
175
                    $ssl['cert'] ?? null,
×
176
                    $ssl['ca'] ?? null,
×
177
                    $ssl['capath'] ?? null,
×
NEW
178
                    $ssl['cipher'] ?? null,
×
179
                );
×
180
            }
181

182
            $clientFlags += MYSQLI_CLIENT_SSL;
×
183
        }
184

185
        try {
186
            if ($this->mysqli->real_connect(
23✔
187
                $hostname,
23✔
188
                $this->username,
23✔
189
                $this->password,
23✔
190
                $this->database,
23✔
191
                $port,
23✔
192
                $socket,
23✔
193
                $clientFlags,
23✔
194
            )) {
23✔
195
                // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
196
                if (($clientFlags & MYSQLI_CLIENT_SSL) !== 0 && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=')
23✔
197
                    && empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value)
23✔
198
                ) {
199
                    $this->mysqli->close();
×
200
                    $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
×
201
                    log_message('error', $message);
×
202

203
                    if ($this->DBDebug) {
×
204
                        throw new DatabaseException($message);
×
205
                    }
206

207
                    return false;
×
208
                }
209

210
                if (! $this->mysqli->set_charset($this->charset)) {
23✔
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;
23✔
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'];
2✔
288
        }
289

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

294
        return $this->dataCache['version'] = $this->mysqli->server_info;
3✔
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()) {
678✔
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);
678✔
313
        } catch (mysqli_sql_exception $e) {
28✔
314
            log_message('error', (string) $e);
28✔
315

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

321
        return false;
16✔
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)) {
678✔
332
            return trim($sql) . ' WHERE 1=1';
3✔
333
        }
334

335
        return $sql;
678✔
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;
54✔
344
    }
345

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

355
        return $this->connID->real_escape_string($str);
652✔
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);
599✔
397

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

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

406
        return $sql;
29✔
407
    }
408

409
    /**
410
     * Generates a platform-specific query string so that the column names can be fetched.
411
     */
412
    protected function _listColumns(string $table = ''): string
413
    {
414
        return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false);
8✔
415
    }
416

417
    /**
418
     * Returns an array of objects with field data
419
     *
420
     * @return list<stdClass>
421
     *
422
     * @throws DatabaseException
423
     */
424
    protected function _fieldData(string $table): array
425
    {
426
        $table = $this->protectIdentifiers($table, true, null, false);
13✔
427

428
        if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
13✔
429
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
430
        }
431
        $query = $query->getResultObject();
13✔
432

433
        $retVal = [];
13✔
434

435
        for ($i = 0, $c = count($query); $i < $c; $i++) {
13✔
436
            $retVal[$i]       = new stdClass();
13✔
437
            $retVal[$i]->name = $query[$i]->Field;
13✔
438

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

441
            $retVal[$i]->nullable    = $query[$i]->Null === 'YES';
13✔
442
            $retVal[$i]->default     = $query[$i]->Default;
13✔
443
            $retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
13✔
444
        }
445

446
        return $retVal;
13✔
447
    }
448

449
    /**
450
     * Returns an array of objects with index data
451
     *
452
     * @return array<string, stdClass>
453
     *
454
     * @throws DatabaseException
455
     * @throws LogicException
456
     */
457
    protected function _indexData(string $table): array
458
    {
459
        $table = $this->protectIdentifiers($table, true, null, false);
8✔
460

461
        if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
8✔
462
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
463
        }
464

465
        $indexes = $query->getResultArray();
8✔
466

467
        if ($indexes === []) {
8✔
468
            return [];
3✔
469
        }
470

471
        $keys = [];
7✔
472

473
        foreach ($indexes as $index) {
7✔
474
            if (empty($keys[$index['Key_name']])) {
7✔
475
                $keys[$index['Key_name']]       = new stdClass();
7✔
476
                $keys[$index['Key_name']]->name = $index['Key_name'];
7✔
477

478
                if ($index['Key_name'] === 'PRIMARY') {
7✔
479
                    $type = 'PRIMARY';
5✔
480
                } elseif ($index['Index_type'] === 'FULLTEXT') {
5✔
481
                    $type = 'FULLTEXT';
×
482
                } elseif ($index['Non_unique']) {
5✔
483
                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
5✔
484
                } else {
485
                    $type = 'UNIQUE';
3✔
486
                }
487

488
                $keys[$index['Key_name']]->type = $type;
7✔
489
            }
490

491
            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
7✔
492
        }
493

494
        return $keys;
7✔
495
    }
496

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

528
        if (($query = $this->query($sql)) === false) {
6✔
529
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
530
        }
531

532
        $query   = $query->getResultObject();
6✔
533
        $indexes = [];
6✔
534

535
        foreach ($query as $row) {
6✔
536
            $indexes[$row->CONSTRAINT_NAME]['constraint_name']       = $row->CONSTRAINT_NAME;
5✔
537
            $indexes[$row->CONSTRAINT_NAME]['table_name']            = $row->TABLE_NAME;
5✔
538
            $indexes[$row->CONSTRAINT_NAME]['column_name'][]         = $row->COLUMN_NAME;
5✔
539
            $indexes[$row->CONSTRAINT_NAME]['foreign_table_name']    = $row->REFERENCED_TABLE_NAME;
5✔
540
            $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
5✔
541
            $indexes[$row->CONSTRAINT_NAME]['on_delete']             = $row->DELETE_RULE;
5✔
542
            $indexes[$row->CONSTRAINT_NAME]['on_update']             = $row->UPDATE_RULE;
5✔
543
            $indexes[$row->CONSTRAINT_NAME]['match']                 = $row->MATCH_OPTION;
5✔
544
        }
545

546
        return $this->foreignKeyDataToObjects($indexes);
6✔
547
    }
548

549
    /**
550
     * Returns platform-specific SQL to disable foreign key checks.
551
     *
552
     * @return string
553
     */
554
    protected function _disableForeignKeyChecks()
555
    {
556
        return 'SET FOREIGN_KEY_CHECKS=0';
604✔
557
    }
558

559
    /**
560
     * Returns platform-specific SQL to enable foreign key checks.
561
     *
562
     * @return string
563
     */
564
    protected function _enableForeignKeyChecks()
565
    {
566
        return 'SET FOREIGN_KEY_CHECKS=1';
604✔
567
    }
568

569
    /**
570
     * Returns the last error code and message.
571
     * Must return this format: ['code' => string|int, 'message' => string]
572
     * intval(code) === 0 means "no error".
573
     *
574
     * @return array<string, int|string>
575
     */
576
    public function error(): array
577
    {
578
        if (! empty($this->mysqli->connect_errno)) {
2✔
579
            return [
×
580
                'code'    => $this->mysqli->connect_errno,
×
581
                'message' => $this->mysqli->connect_error,
×
582
            ];
×
583
        }
584

585
        return [
2✔
586
            'code'    => $this->connID->errno,
2✔
587
            'message' => $this->connID->error,
2✔
588
        ];
2✔
589
    }
590

591
    /**
592
     * Insert ID
593
     */
594
    public function insertID(): int
595
    {
596
        return $this->connID->insert_id;
79✔
597
    }
598

599
    /**
600
     * Begin Transaction
601
     */
602
    protected function _transBegin(): bool
603
    {
604
        $this->connID->autocommit(false);
15✔
605

606
        return $this->connID->begin_transaction();
15✔
607
    }
608

609
    /**
610
     * Commit Transaction
611
     */
612
    protected function _transCommit(): bool
613
    {
614
        if ($this->connID->commit()) {
3✔
615
            $this->connID->autocommit(true);
3✔
616

617
            return true;
3✔
618
        }
619

620
        return false;
×
621
    }
622

623
    /**
624
     * Rollback Transaction
625
     */
626
    protected function _transRollback(): bool
627
    {
628
        if ($this->connID->rollback()) {
15✔
629
            $this->connID->autocommit(true);
15✔
630

631
            return true;
15✔
632
        }
633

634
        return false;
×
635
    }
636
}
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