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

codeigniter4 / CodeIgniter4 / 25902734269

15 May 2026 05:51AM UTC coverage: 88.459% (+0.2%) from 88.299%
25902734269

Pull #10159

github

web-flow
Merge f0573f3e0 into 170b89a6e
Pull Request #10159: feat: Add support for callable TTLs in cache handlers

6 of 10 new or added lines in 3 files covered. (60.0%)

446 existing lines in 24 files now uncovered.

24114 of 27260 relevant lines covered (88.46%)

219.07 hits per line

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

91.01
/system/Database/BaseConnection.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;
15

16
use Closure;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\Exceptions\RetryableTransactionException;
19
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
20
use CodeIgniter\Events\Events;
21
use CodeIgniter\I18n\Time;
22
use Exception;
23
use ReflectionClass;
24
use ReflectionNamedType;
25
use ReflectionType;
26
use ReflectionUnionType;
27
use stdClass;
28
use Stringable;
29
use Throwable;
30

31
/**
32
 * @property-read array      $aliasedTables
33
 * @property-read string     $charset
34
 * @property-read bool       $compress
35
 * @property-read float      $connectDuration
36
 * @property-read float      $connectTime
37
 * @property-read string     $database
38
 * @property-read array      $dateFormat
39
 * @property-read string     $DBCollat
40
 * @property-read bool       $DBDebug
41
 * @property-read string     $DBDriver
42
 * @property-read string     $DBPrefix
43
 * @property-read string     $DSN
44
 * @property-read array|bool $encrypt
45
 * @property-read array      $failover
46
 * @property-read string     $hostname
47
 * @property-read Query      $lastQuery
48
 * @property-read string     $password
49
 * @property-read bool       $pConnect
50
 * @property-read int|string $port
51
 * @property-read bool       $pretend
52
 * @property-read string     $queryClass
53
 * @property-read array      $reservedIdentifiers
54
 * @property-read string     $subdriver
55
 * @property-read string     $swapPre
56
 * @property-read int        $transDepth
57
 * @property-read bool       $transFailure
58
 * @property-read bool       $transStatus
59
 * @property-read string     $username
60
 *
61
 * @template TConnection
62
 * @template TResult
63
 *
64
 * @implements ConnectionInterface<TConnection, TResult>
65
 * @see \CodeIgniter\Database\BaseConnectionTest
66
 */
67
abstract class BaseConnection implements ConnectionInterface
68
{
69
    /**
70
     * Cached builtin type names per class/property.
71
     *
72
     * @var array<class-string, array<string, list<string>>>
73
     */
74
    private static array $propertyBuiltinTypesCache = [];
75

76
    /**
77
     * Data Source Name / Connect string
78
     *
79
     * @var string
80
     */
81
    protected $DSN;
82

83
    /**
84
     * Database port
85
     *
86
     * @var int|string
87
     */
88
    protected $port = '';
89

90
    /**
91
     * Hostname
92
     *
93
     * @var string
94
     */
95
    protected $hostname;
96

97
    /**
98
     * Username
99
     *
100
     * @var string
101
     */
102
    protected $username;
103

104
    /**
105
     * Password
106
     *
107
     * @var string
108
     */
109
    protected $password;
110

111
    /**
112
     * Database name
113
     *
114
     * @var string
115
     */
116
    protected $database;
117

118
    /**
119
     * Database driver
120
     *
121
     * @var string
122
     */
123
    protected $DBDriver = 'MySQLi';
124

125
    /**
126
     * Sub-driver
127
     *
128
     * @used-by CI_DB_pdo_driver
129
     *
130
     * @var string
131
     */
132
    protected $subdriver;
133

134
    /**
135
     * Table prefix
136
     *
137
     * @var string
138
     */
139
    protected $DBPrefix = '';
140

141
    /**
142
     * Persistent connection flag
143
     *
144
     * @var bool
145
     */
146
    protected $pConnect = false;
147

148
    /**
149
     * Whether to throw Exception or not when an error occurs.
150
     *
151
     * @var bool
152
     */
153
    protected $DBDebug = true;
154

155
    /**
156
     * Character set
157
     *
158
     * This value must be updated by Config\Database if the driver use it.
159
     *
160
     * @var string
161
     */
162
    protected $charset = '';
163

164
    /**
165
     * Collation
166
     *
167
     * This value must be updated by Config\Database if the driver use it.
168
     *
169
     * @var string
170
     */
171
    protected $DBCollat = '';
172

173
    /**
174
     * Database session timezone
175
     *
176
     * false    = Don't set timezone (default, backward compatible)
177
     * true     = Automatically sync with app timezone
178
     * string   = Specific timezone (offset or named timezone)
179
     *
180
     * Named timezones (e.g., 'America/New_York') will be automatically
181
     * converted to offsets (e.g., '-05:00') for database compatibility.
182
     *
183
     * @var bool|string
184
     */
185
    protected $timezone = false;
186

187
    /**
188
     * Swap Prefix
189
     *
190
     * @var string
191
     */
192
    protected $swapPre = '';
193

194
    /**
195
     * Encryption flag/data
196
     *
197
     * @var array|bool
198
     */
199
    protected $encrypt = false;
200

201
    /**
202
     * Compression flag
203
     *
204
     * @var bool
205
     */
206
    protected $compress = false;
207

208
    /**
209
     * Settings for a failover connection.
210
     *
211
     * @var array
212
     */
213
    protected $failover = [];
214

215
    /**
216
     * The last query object that was executed
217
     * on this connection.
218
     *
219
     * @var Query
220
     */
221
    protected $lastQuery;
222

223
    /**
224
     * The exception that would have been thrown on the last failed query
225
     * if DBDebug were enabled. Null when the last query succeeded or when
226
     * DBDebug is true (in which case the exception is thrown directly and
227
     * this property is never set).
228
     */
229
    protected ?DatabaseException $lastException = null;
230

231
    /**
232
     * Connection ID
233
     *
234
     * @var false|TConnection
235
     */
236
    public $connID = false;
237

238
    /**
239
     * Result ID
240
     *
241
     * @var false|TResult
242
     */
243
    public $resultID = false;
244

245
    /**
246
     * Protect identifiers flag
247
     *
248
     * @var bool
249
     */
250
    public $protectIdentifiers = true;
251

252
    /**
253
     * List of reserved identifiers
254
     *
255
     * Identifiers that must NOT be escaped.
256
     *
257
     * @var array
258
     */
259
    protected $reservedIdentifiers = ['*'];
260

261
    /**
262
     * Identifier escape character
263
     *
264
     * @var array|string
265
     */
266
    public $escapeChar = '"';
267

268
    /**
269
     * ESCAPE statement string
270
     *
271
     * @var string
272
     */
273
    public $likeEscapeStr = " ESCAPE '%s' ";
274

275
    /**
276
     * ESCAPE character
277
     *
278
     * @var string
279
     */
280
    public $likeEscapeChar = '!';
281

282
    /**
283
     * RegExp used to escape identifiers
284
     *
285
     * @var array
286
     */
287
    protected $pregEscapeChar = [];
288

289
    /**
290
     * Holds previously looked up data
291
     * for performance reasons.
292
     *
293
     * @var array
294
     */
295
    public $dataCache = [];
296

297
    /**
298
     * Microtime when connection was made
299
     *
300
     * @var float
301
     */
302
    protected $connectTime = 0.0;
303

304
    /**
305
     * How long it took to establish connection.
306
     *
307
     * @var float
308
     */
309
    protected $connectDuration = 0.0;
310

311
    /**
312
     * If true, no queries will actually be
313
     * run against the database.
314
     *
315
     * @var bool
316
     */
317
    protected $pretend = false;
318

319
    /**
320
     * Transaction enabled flag
321
     *
322
     * @var bool
323
     */
324
    public $transEnabled = true;
325

326
    /**
327
     * Strict transaction mode flag
328
     *
329
     * @var bool
330
     */
331
    public $transStrict = true;
332

333
    /**
334
     * Transaction depth level
335
     *
336
     * @var int
337
     */
338
    protected $transDepth = 0;
339

340
    /**
341
     * Transaction status flag
342
     *
343
     * Used with transactions to determine if a rollback should occur.
344
     *
345
     * @var bool
346
     */
347
    protected $transStatus = true;
348

349
    /**
350
     * Transaction failure flag
351
     *
352
     * Used with transactions to determine if a transaction has failed.
353
     *
354
     * @var bool
355
     */
356
    protected $transFailure = false;
357

358
    /**
359
     * Whether to throw exceptions during transaction
360
     */
361
    protected bool $transException = false;
362

363
    /**
364
     * Callbacks to run after the outermost transaction commits.
365
     *
366
     * @var list<callable(): void>
367
     */
368
    protected array $transCommitCallbacks = [];
369

370
    /**
371
     * Callbacks to run after the outermost transaction rolls back.
372
     *
373
     * @var list<callable(): void>
374
     */
375
    protected array $transRollbackCallbacks = [];
376

377
    /**
378
     * Array of table aliases.
379
     *
380
     * @var list<string>
381
     */
382
    protected $aliasedTables = [];
383

384
    /**
385
     * Query Class
386
     *
387
     * @var string
388
     */
389
    protected $queryClass = Query::class;
390

391
    /**
392
     * Default Date/Time formats
393
     *
394
     * @var array<string, string>
395
     */
396
    protected array $dateFormat = [
397
        'date'        => 'Y-m-d',
398
        'datetime'    => 'Y-m-d H:i:s',
399
        'datetime-ms' => 'Y-m-d H:i:s.v',
400
        'datetime-us' => 'Y-m-d H:i:s.u',
401
        'time'        => 'H:i:s',
402
    ];
403

404
    /**
405
     * Saves our connection settings.
406
     */
407
    public function __construct(array $params)
408
    {
409
        if (isset($params['dateFormat'])) {
540✔
410
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
155✔
411
            unset($params['dateFormat']);
155✔
412
        }
413

414
        $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($params));
540✔
415

416
        foreach ($params as $key => $value) {
540✔
417
            if (property_exists($this, $key)) {
201✔
418
                $this->{$key} = $this->castScalarValueForTypedProperty(
201✔
419
                    $value,
201✔
420
                    $typedPropertyTypes[$key] ?? [],
201✔
421
                );
201✔
422
            }
423
        }
424

425
        $queryClass = str_replace('Connection', 'Query', static::class);
539✔
426

427
        if (class_exists($queryClass)) {
539✔
428
            $this->queryClass = $queryClass;
425✔
429
        }
430

431
        if ($this->failover !== []) {
539✔
432
            // If there is a failover database, connect now to do failover.
433
            // Otherwise, Query Builder creates SQL statement with the main database config
434
            // (DBPrefix) even when the main database is down.
435
            $this->initialize();
2✔
436
        }
437
    }
438

439
    /**
440
     * Some config values (especially env overrides without clear source type)
441
     * can still reach us as strings. Coerce them for typed properties to keep
442
     * strict typing compatible.
443
     *
444
     * @param list<string> $types
445
     */
446
    private function castScalarValueForTypedProperty(mixed $value, array $types): mixed
447
    {
448
        if (! is_string($value)) {
201✔
449
            return $value;
171✔
450
        }
451

452
        if ($types === [] || in_array('string', $types, true) || in_array('mixed', $types, true)) {
201✔
453
            return $value;
201✔
454
        }
455

456
        $trimmedValue = trim($value);
5✔
457

458
        if (in_array('null', $types, true) && strtolower($trimmedValue) === 'null') {
5✔
459
            return null;
1✔
460
        }
461

462
        if (in_array('int', $types, true) && preg_match('/^[+-]?\d+$/', $trimmedValue) === 1) {
5✔
463
            return (int) $trimmedValue;
2✔
464
        }
465

466
        if (in_array('float', $types, true) && is_numeric($trimmedValue)) {
4✔
UNCOV
467
            return (float) $trimmedValue;
×
468
        }
469

470
        if (in_array('bool', $types, true) || in_array('false', $types, true) || in_array('true', $types, true)) {
4✔
471
            $boolValue = filter_var($trimmedValue, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
3✔
472

473
            if ($boolValue !== null) {
3✔
474
                if (in_array('bool', $types, true)) {
3✔
475
                    return $boolValue;
2✔
476
                }
477

478
                if ($boolValue === false && in_array('false', $types, true)) {
1✔
479
                    return false;
1✔
480
                }
481

482
                if ($boolValue === true && in_array('true', $types, true)) {
1✔
483
                    return true;
1✔
484
                }
485
            }
486
        }
487

488
        return $value;
1✔
489
    }
490

491
    /**
492
     * @param list<string> $properties
493
     *
494
     * @return array<string, list<string>>
495
     */
496
    private function getBuiltinPropertyTypesMap(array $properties): array
497
    {
498
        $className = static::class;
540✔
499
        $requested = array_fill_keys($properties, true);
540✔
500

501
        if (! isset(self::$propertyBuiltinTypesCache[$className])) {
540✔
502
            self::$propertyBuiltinTypesCache[$className] = [];
20✔
503
        }
504

505
        // Fill only the properties requested by this call that are not cached yet.
506
        $missing = array_diff_key($requested, self::$propertyBuiltinTypesCache[$className]);
540✔
507

508
        if ($missing !== []) {
540✔
509
            $reflection = new ReflectionClass($className);
31✔
510

511
            foreach ($reflection->getProperties() as $property) {
31✔
512
                $propertyName = $property->getName();
31✔
513

514
                if (! isset($missing[$propertyName])) {
31✔
515
                    continue;
31✔
516
                }
517

518
                $type = $property->getType();
31✔
519

520
                if (! $type instanceof ReflectionType) {
31✔
521
                    self::$propertyBuiltinTypesCache[$className][$propertyName] = [];
30✔
522

523
                    continue;
30✔
524
                }
525

526
                $namedTypes   = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
18✔
527
                $builtinTypes = [];
18✔
528

529
                foreach ($namedTypes as $namedType) {
18✔
530
                    if (! $namedType instanceof ReflectionNamedType || ! $namedType->isBuiltin()) {
18✔
UNCOV
531
                        continue;
×
532
                    }
533

534
                    $builtinTypes[] = $namedType->getName();
18✔
535
                }
536

537
                if ($type->allowsNull() && ! in_array('null', $builtinTypes, true)) {
18✔
538
                    $builtinTypes[] = 'null';
13✔
539
                }
540

541
                self::$propertyBuiltinTypesCache[$className][$propertyName] = $builtinTypes;
18✔
542
            }
543

544
            // Untyped or unresolved properties are cached as empty to avoid re-reflecting them.
545
            foreach (array_keys($missing) as $propertyName) {
31✔
546
                self::$propertyBuiltinTypesCache[$className][$propertyName] ??= [];
31✔
547
            }
548
        }
549

550
        $typedProperties = [];
540✔
551

552
        foreach ($properties as $property) {
540✔
553
            $typedProperties[$property] = self::$propertyBuiltinTypesCache[$className][$property] ?? [];
201✔
554
        }
555

556
        return $typedProperties;
540✔
557
    }
558

559
    /**
560
     * Initializes the database connection/settings.
561
     *
562
     * @return void
563
     *
564
     * @throws DatabaseException
565
     */
566
    public function initialize()
567
    {
568
        /* If an established connection is available, then there's
569
         * no need to connect and select the database.
570
         *
571
         * Depending on the database driver, connID can be either
572
         * boolean TRUE, a resource or an object.
573
         */
574
        if ($this->connID) {
904✔
575
            return;
834✔
576
        }
577

578
        $this->connectTime = microtime(true);
86✔
579
        $connectionErrors  = [];
86✔
580

581
        try {
582
            // Connect to the database and set the connection ID
583
            $this->connID = $this->connect($this->pConnect);
86✔
584
        } catch (Throwable $e) {
2✔
585
            $this->connID       = false;
2✔
586
            $connectionErrors[] = sprintf(
2✔
587
                'Main connection [%s]: %s',
2✔
588
                $this->DBDriver,
2✔
589
                $e->getMessage(),
2✔
590
            );
2✔
591
            log_message('error', 'Error connecting to the database: ' . $e);
2✔
592
        }
593

594
        // No connection resource? Check if there is a failover else throw an error
595
        if (! $this->connID) {
86✔
596
            // Check if there is a failover set
597
            if (! empty($this->failover) && is_array($this->failover)) {
4✔
598
                // Go over all the failovers
599
                foreach ($this->failover as $index => $failover) {
2✔
600
                    $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($failover));
2✔
601

602
                    // Replace the current settings with those of the failover
603
                    foreach ($failover as $key => $val) {
2✔
604
                        if (property_exists($this, $key)) {
2✔
605
                            $this->{$key} = $this->castScalarValueForTypedProperty(
2✔
606
                                $val,
2✔
607
                                $typedPropertyTypes[$key] ?? [],
2✔
608
                            );
2✔
609
                        }
610
                    }
611

612
                    try {
613
                        // Try to connect
614
                        $this->connID = $this->connect($this->pConnect);
2✔
615
                    } catch (Throwable $e) {
1✔
616
                        $connectionErrors[] = sprintf(
1✔
617
                            'Failover #%d [%s]: %s',
1✔
618
                            ++$index,
1✔
619
                            $this->DBDriver,
1✔
620
                            $e->getMessage(),
1✔
621
                        );
1✔
622
                        log_message('error', 'Error connecting to the database: ' . $e);
1✔
623
                    }
624

625
                    // If a connection is made break the foreach loop
626
                    if ($this->connID) {
2✔
627
                        break;
2✔
628
                    }
629
                }
630
            }
631

632
            // We still don't have a connection?
633
            if (! $this->connID) {
4✔
634
                throw new DatabaseException(sprintf(
2✔
635
                    'Unable to connect to the database.%s%s',
2✔
636
                    PHP_EOL,
2✔
637
                    implode(PHP_EOL, $connectionErrors),
2✔
638
                ));
2✔
639
            }
640
        }
641

642
        $this->connectDuration = microtime(true) - $this->connectTime;
84✔
643
    }
644

645
    /**
646
     * Close the database connection.
647
     *
648
     * @return void
649
     */
650
    public function close()
651
    {
652
        if ($this->connID) {
3✔
653
            $this->_close();
3✔
654
            $this->connID = false;
3✔
655
        }
656
    }
657

658
    /**
659
     * Keep or establish the connection if no queries have been sent for
660
     * a length of time exceeding the server's idle timeout.
661
     *
662
     * @return void
663
     */
664
    public function reconnect()
665
    {
666
        if ($this->ping() === false) {
2✔
667
            $this->close();
1✔
668
            $this->initialize();
1✔
669
        }
670
    }
671

672
    /**
673
     * Platform dependent way method for closing the connection.
674
     *
675
     * @return void
676
     */
677
    abstract protected function _close();
678

679
    /**
680
     * Check if the connection is still alive.
681
     */
682
    public function ping(): bool
683
    {
684
        if ($this->connID === false) {
5✔
685
            return false;
2✔
686
        }
687

688
        return $this->_ping();
4✔
689
    }
690

691
    /**
692
     * Driver-specific ping implementation.
693
     */
694
    protected function _ping(): bool
695
    {
696
        try {
697
            $result = $this->simpleQuery('SELECT 1');
4✔
698

699
            return $result !== false;
4✔
UNCOV
700
        } catch (DatabaseException) {
×
UNCOV
701
            return false;
×
702
        }
703
    }
704

705
    /**
706
     * Create a persistent database connection.
707
     *
708
     * @return false|TConnection
709
     */
710
    public function persistentConnect()
711
    {
UNCOV
712
        return $this->connect(true);
×
713
    }
714

715
    /**
716
     * Returns the actual connection object. If both a 'read' and 'write'
717
     * connection has been specified, you can pass either term in to
718
     * get that connection. If you pass either alias in and only a single
719
     * connection is present, it must return the sole connection.
720
     *
721
     * @return false|TConnection
722
     */
723
    public function getConnection(?string $alias = null)
724
    {
725
        // @todo work with read/write connections
726
        return $this->connID;
2✔
727
    }
728

729
    /**
730
     * Returns the name of the current database being used.
731
     */
732
    public function getDatabase(): string
733
    {
734
        return empty($this->database) ? '' : $this->database;
848✔
735
    }
736

737
    /**
738
     * Set DB Prefix
739
     *
740
     * Set's the DB Prefix to something new without needing to reconnect
741
     *
742
     * @param string $prefix The prefix
743
     */
744
    public function setPrefix(string $prefix = ''): string
745
    {
746
        return $this->DBPrefix = $prefix;
14✔
747
    }
748

749
    /**
750
     * Returns the database prefix.
751
     */
752
    public function getPrefix(): string
753
    {
754
        return $this->DBPrefix;
13✔
755
    }
756

757
    /**
758
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
759
     */
760
    public function getPlatform(): string
761
    {
762
        return $this->DBDriver;
23✔
763
    }
764

765
    /**
766
     * Sets the Table Aliases to use. These are typically
767
     * collected during use of the Builder, and set here
768
     * so queries are built correctly.
769
     *
770
     * @return $this
771
     */
772
    public function setAliasedTables(array $aliases)
773
    {
774
        $this->aliasedTables = $aliases;
1,095✔
775

776
        return $this;
1,095✔
777
    }
778

779
    /**
780
     * Add a table alias to our list.
781
     *
782
     * @return $this
783
     */
784
    public function addTableAlias(string $alias)
785
    {
786
        if ($alias === '') {
35✔
787
            return $this;
6✔
788
        }
789

790
        if (! in_array($alias, $this->aliasedTables, true)) {
29✔
791
            $this->aliasedTables[] = $alias;
29✔
792
        }
793

794
        return $this;
29✔
795
    }
796

797
    /**
798
     * Executes the query against the database.
799
     *
800
     * @return false|TResult
801
     */
802
    abstract protected function execute(string $sql);
803

804
    /**
805
     * Orchestrates a query against the database. Queries must use
806
     * Database\Statement objects to store the query and build it.
807
     * This method works with the cache.
808
     *
809
     * Should automatically handle different connections for read/write
810
     * queries if needed.
811
     *
812
     * @param array|string|null $binds
813
     *
814
     * @return BaseResult<TConnection, TResult>|bool|Query
815
     *
816
     * @todo BC set $queryClass default as null in 4.1
817
     */
818
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
819
    {
820
        $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass;
897✔
821

822
        if (empty($this->connID)) {
897✔
823
            $this->initialize();
54✔
824
        }
825

826
        /** @var Query $query */
827
        $query = new $queryClass($this);
897✔
828

829
        $query->setQuery($sql, $binds, $setEscapeFlags);
897✔
830

831
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
897✔
UNCOV
832
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
833
        }
834

835
        $startTime = microtime(true);
897✔
836

837
        // Always save the last query so we can use
838
        // the getLastQuery() method.
839
        $this->lastQuery = $query;
897✔
840

841
        // If $pretend is true, then we just want to return
842
        // the actual query object here. There won't be
843
        // any results to return.
844
        if ($this->pretend) {
897✔
845
            $query->setDuration($startTime);
11✔
846

847
            return $query;
11✔
848
        }
849

850
        // Run the query for real
851
        try {
852
            $exception           = null;
897✔
853
            $this->lastException = null;
897✔
854
            $this->resultID      = $this->simpleQuery($query->getQuery());
897✔
855
        } catch (DatabaseException $exception) {
19✔
856
            $this->resultID = false;
19✔
857
        }
858

859
        if ($this->resultID === false) {
897✔
860
            $query->setDuration($startTime, $startTime);
42✔
861

862
            // This will trigger a rollback if transactions are being used
863
            $this->handleTransStatus();
42✔
864

865
            if (
866
                $this->DBDebug
42✔
867
                && (
868
                    // Not in transactions
869
                    $this->transDepth === 0
42✔
870
                    // In transactions, do not throw exception by default.
42✔
871
                    || $this->transException
42✔
872
                )
873
            ) {
874
                // We call this function in order to roll-back queries
875
                // if transactions are enabled. If we don't call this here
876
                // the error message will trigger an exit, causing the
877
                // transactions to remain in limbo.
878
                while ($this->transDepth !== 0) {
15✔
879
                    $transDepth = $this->transDepth;
3✔
880
                    $this->transComplete();
3✔
881

882
                    if ($transDepth === $this->transDepth) {
3✔
UNCOV
883
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
UNCOV
884
                        break;
×
885
                    }
886
                }
887

888
                // Let others do something with this query.
889
                Events::trigger('DBQuery', $query);
15✔
890

891
                if ($exception instanceof DatabaseException) {
15✔
892
                    throw $exception;
13✔
893
                }
894

895
                return false;
2✔
896
            }
897

898
            // Let others do something with this query.
899
            Events::trigger('DBQuery', $query);
27✔
900

901
            return false;
27✔
902
        }
903

904
        $query->setDuration($startTime);
896✔
905

906
        // Let others do something with this query
907
        Events::trigger('DBQuery', $query);
896✔
908

909
        // resultID is not false, so it must be successful
910
        if ($this->isWriteType($sql)) {
896✔
911
            return true;
857✔
912
        }
913

914
        // query is not write-type, so it must be read-type query; return QueryResult
915
        $resultClass = str_replace('Connection', 'Result', static::class);
895✔
916

917
        return new $resultClass($this->connID, $this->resultID);
895✔
918
    }
919

920
    /**
921
     * Performs a basic query against the database. No binding or caching
922
     * is performed, nor are transactions handled. Simply takes a raw
923
     * query string and returns the database-specific result id.
924
     *
925
     * @return false|TResult
926
     */
927
    public function simpleQuery(string $sql)
928
    {
929
        if (empty($this->connID)) {
904✔
930
            $this->initialize();
6✔
931
        }
932

933
        return $this->execute($sql);
904✔
934
    }
935

936
    /**
937
     * Disable Transactions
938
     *
939
     * This permits transactions to be disabled at run-time.
940
     *
941
     * @return void
942
     */
943
    public function transOff()
944
    {
945
        $this->transEnabled = false;
2✔
946
    }
947

948
    /**
949
     * Enable/disable Transaction Strict Mode
950
     *
951
     * When strict mode is enabled, if you are running multiple groups of
952
     * transactions, if one group fails all subsequent groups will be
953
     * rolled back.
954
     *
955
     * If strict mode is disabled, each group is treated autonomously,
956
     * meaning a failure of one group will not affect any others
957
     *
958
     * @param bool $mode = true
959
     *
960
     * @return $this
961
     */
962
    public function transStrict(bool $mode = true)
963
    {
964
        $this->transStrict = $mode;
6✔
965

966
        return $this;
6✔
967
    }
968

969
    /**
970
     * Start Transaction
971
     */
972
    public function transStart(bool $testMode = false): bool
973
    {
974
        if (! $this->transEnabled) {
63✔
UNCOV
975
            return false;
×
976
        }
977

978
        return $this->transBegin($testMode);
63✔
979
    }
980

981
    /**
982
     * If set to true, exceptions are thrown during transactions.
983
     *
984
     * @return $this
985
     */
986
    public function transException(bool $transException)
987
    {
988
        $this->transException = $transException;
4✔
989

990
        return $this;
4✔
991
    }
992

993
    /**
994
     * Complete Transaction
995
     */
996
    public function transComplete(): bool
997
    {
998
        if (! $this->transEnabled) {
68✔
UNCOV
999
            return false;
×
1000
        }
1001

1002
        // The query() function will set this flag to FALSE in the event that a query failed
1003
        if ($this->transStatus === false || $this->transFailure === true) {
68✔
1004
            try {
1005
                $this->transRollback();
23✔
1006
            } finally {
1007
                // If we are NOT running in strict mode, we will reset
1008
                // the _trans_status flag so that subsequent groups of
1009
                // transactions will be permitted.
1010
                if ($this->transStrict === false) {
23✔
1011
                    $this->transStatus = true;
23✔
1012
                }
1013
            }
1014

1015
            return false;
21✔
1016
        }
1017

1018
        return $this->transCommit();
50✔
1019
    }
1020

1021
    /**
1022
     * Lets you retrieve the transaction flag to determine if it has failed
1023
     */
1024
    public function transStatus(): bool
1025
    {
1026
        return $this->transStatus;
18✔
1027
    }
1028

1029
    /**
1030
     * Checks whether this connection is inside an active transaction.
1031
     */
1032
    public function inTransaction(): bool
1033
    {
1034
        return $this->transDepth > 0;
6✔
1035
    }
1036

1037
    /**
1038
     * Register a callback to run after the outermost transaction commits.
1039
     *
1040
     * If no transaction is active, the callback runs immediately.
1041
     *
1042
     * @param callable(): void $callback
1043
     *
1044
     * @return $this
1045
     */
1046
    public function afterCommit(callable $callback): static
1047
    {
1048
        if ($this->transDepth === 0) {
12✔
1049
            $callback();
2✔
1050

1051
            return $this;
2✔
1052
        }
1053

1054
        $this->transCommitCallbacks[] = $callback;
10✔
1055

1056
        return $this;
10✔
1057
    }
1058

1059
    /**
1060
     * Register a callback to run after the outermost transaction rolls back.
1061
     *
1062
     * If no transaction is active, the callback is not run.
1063
     *
1064
     * @param callable(): void $callback
1065
     *
1066
     * @return $this
1067
     */
1068
    public function afterRollback(callable $callback): static
1069
    {
1070
        if ($this->transDepth === 0) {
13✔
1071
            return $this;
1✔
1072
        }
1073

1074
        $this->transRollbackCallbacks[] = $callback;
12✔
1075

1076
        return $this;
12✔
1077
    }
1078

1079
    /**
1080
     * Run the callback inside a transaction.
1081
     *
1082
     * @template TReturn
1083
     *
1084
     * @param callable(self): TReturn $callback
1085
     *
1086
     * @return false|TReturn
1087
     */
1088
    public function transaction(callable $callback): mixed
1089
    {
1090
        if (! $this->transEnabled) {
15✔
1091
            return $callback($this);
1✔
1092
        }
1093

1094
        if (! $this->transBegin()) {
14✔
1095
            return false;
1✔
1096
        }
1097

1098
        try {
1099
            $result = $callback($this);
13✔
1100
        } catch (Throwable $e) {
5✔
1101
            try {
1102
                $this->transRollback();
5✔
1103
            } catch (Throwable $rollbackException) {
1✔
1104
                log_message('error', 'Database: Transaction callback threw an exception before rollback failed: ' . $e);
1✔
1105

1106
                throw $rollbackException;
1✔
1107
            } finally {
1108
                if ($this->transDepth > 0) {
5✔
1109
                    $this->transStatus = false;
1✔
1110
                } elseif ($this->transStrict === false) {
4✔
1111
                    $this->transStatus = true;
5✔
1112
                }
1113
            }
1114

1115
            throw $e;
4✔
1116
        }
1117

1118
        if (! $this->transComplete()) {
8✔
1119
            return false;
1✔
1120
        }
1121

1122
        return $result;
6✔
1123
    }
1124

1125
    /**
1126
     * Begin Transaction
1127
     */
1128
    public function transBegin(bool $testMode = false): bool
1129
    {
1130
        if (! $this->transEnabled) {
84✔
1131
            return false;
1✔
1132
        }
1133

1134
        // When transactions are nested we only begin/commit/rollback the outermost ones
1135
        if ($this->transDepth > 0) {
83✔
1136
            $this->transDepth++;
6✔
1137

1138
            return true;
6✔
1139
        }
1140

1141
        if (empty($this->connID)) {
83✔
1142
            $this->initialize();
6✔
1143
        }
1144

1145
        // Reset the transaction failure flag.
1146
        // If the $testMode flag is set to TRUE transactions will be rolled back
1147
        // even if the queries produce a successful result.
1148
        $this->transFailure = $testMode;
83✔
1149

1150
        if ($this->_transBegin()) {
83✔
1151
            $this->transDepth++;
82✔
1152

1153
            return true;
82✔
1154
        }
1155

1156
        return false;
1✔
1157
    }
1158

1159
    /**
1160
     * Commit Transaction
1161
     */
1162
    public function transCommit(): bool
1163
    {
1164
        if (! $this->transEnabled || $this->transDepth === 0) {
54✔
UNCOV
1165
            return false;
×
1166
        }
1167

1168
        // When transactions are nested we only begin/commit/rollback the outermost ones
1169
        if ($this->transDepth > 1 || $this->_transCommit()) {
54✔
1170
            $this->transDepth--;
54✔
1171

1172
            if ($this->transDepth === 0) {
54✔
1173
                $this->transRollbackCallbacks = [];
53✔
1174
                $this->runTransCommitCallbacks();
53✔
1175
            }
1176

1177
            return true;
52✔
1178
        }
1179

1180
        return false;
1✔
1181
    }
1182

1183
    /**
1184
     * Rollback Transaction
1185
     */
1186
    public function transRollback(): bool
1187
    {
1188
        if (! $this->transEnabled || $this->transDepth === 0) {
35✔
UNCOV
1189
            return false;
×
1190
        }
1191

1192
        // When transactions are nested we only begin/commit/rollback the outermost ones
1193
        if ($this->transDepth > 1 || $this->_transRollback()) {
35✔
1194
            $this->transDepth--;
35✔
1195

1196
            if ($this->transDepth === 0) {
35✔
1197
                $this->transCommitCallbacks = [];
35✔
1198
                $this->runTransRollbackCallbacks();
35✔
1199
            }
1200

1201
            return true;
31✔
1202
        }
1203

1204
        return false;
1✔
1205
    }
1206

1207
    /**
1208
     * Reset transaction status - to restart transactions after strict mode failure
1209
     */
1210
    public function resetTransStatus(): static
1211
    {
1212
        $this->transStatus = true;
5✔
1213

1214
        return $this;
5✔
1215
    }
1216

1217
    /**
1218
     * Handle transaction status when a query fails
1219
     *
1220
     * @internal This method is for internal database component use only
1221
     */
1222
    public function handleTransStatus(): void
1223
    {
1224
        if ($this->transDepth !== 0) {
50✔
1225
            $this->transStatus = false;
21✔
1226
        }
1227
    }
1228

1229
    /**
1230
     * Run and clear callbacks registered for a successful transaction commit.
1231
     */
1232
    protected function runTransCommitCallbacks(): void
1233
    {
1234
        $callbacks                  = $this->transCommitCallbacks;
53✔
1235
        $this->transCommitCallbacks = [];
53✔
1236

1237
        foreach ($callbacks as $callback) {
53✔
1238
            $callback();
9✔
1239
        }
1240
    }
1241

1242
    /**
1243
     * Run and clear callbacks registered for a transaction rollback.
1244
     */
1245
    protected function runTransRollbackCallbacks(): void
1246
    {
1247
        $callbacks                    = $this->transRollbackCallbacks;
35✔
1248
        $this->transRollbackCallbacks = [];
35✔
1249

1250
        foreach ($callbacks as $callback) {
35✔
1251
            $callback();
11✔
1252
        }
1253
    }
1254

1255
    /**
1256
     * Begin Transaction
1257
     */
1258
    abstract protected function _transBegin(): bool;
1259

1260
    /**
1261
     * Commit Transaction
1262
     */
1263
    abstract protected function _transCommit(): bool;
1264

1265
    /**
1266
     * Rollback Transaction
1267
     */
1268
    abstract protected function _transRollback(): bool;
1269

1270
    /**
1271
     * Returns a non-shared new instance of the query builder for this connection.
1272
     *
1273
     * @param array|string|TableName $tableName
1274
     *
1275
     * @return BaseBuilder
1276
     *
1277
     * @throws DatabaseException
1278
     */
1279
    public function table($tableName)
1280
    {
1281
        if (empty($tableName)) {
1,035✔
UNCOV
1282
            throw new DatabaseException('You must set the database table to be used with your query.');
×
1283
        }
1284

1285
        $className = str_replace('Connection', 'Builder', static::class);
1,035✔
1286

1287
        return new $className($tableName, $this);
1,035✔
1288
    }
1289

1290
    /**
1291
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
1292
     */
1293
    public function newQuery(): BaseBuilder
1294
    {
1295
        // save table aliases
1296
        $tempAliases         = $this->aliasedTables;
21✔
1297
        $builder             = $this->table(',')->from([], true);
21✔
1298
        $this->aliasedTables = $tempAliases;
21✔
1299

1300
        return $builder;
21✔
1301
    }
1302

1303
    /**
1304
     * Creates a prepared statement with the database that can then
1305
     * be used to execute multiple statements against. Within the
1306
     * closure, you would build the query in any normal way, though
1307
     * the Query Builder is the expected manner.
1308
     *
1309
     * Example:
1310
     *    $stmt = $db->prepare(function($db)
1311
     *           {
1312
     *             return $db->table('users')
1313
     *                   ->where('id', 1)
1314
     *                     ->get();
1315
     *           })
1316
     *
1317
     * @param Closure(BaseConnection): mixed $func
1318
     *
1319
     * @return BasePreparedQuery|null
1320
     */
1321
    public function prepare(Closure $func, array $options = [])
1322
    {
1323
        if (empty($this->connID)) {
17✔
UNCOV
1324
            $this->initialize();
×
1325
        }
1326

1327
        $this->pretend();
17✔
1328

1329
        $sql = $func($this);
17✔
1330

1331
        $this->pretend(false);
17✔
1332

1333
        if ($sql instanceof QueryInterface) {
17✔
1334
            $sql = $sql->getOriginalQuery();
17✔
1335
        }
1336

1337
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
17✔
1338
        /** @var BasePreparedQuery $class */
1339
        $class = new $class($this);
17✔
1340

1341
        return $class->prepare($sql, $options);
17✔
1342
    }
1343

1344
    /**
1345
     * Returns the last query's statement object.
1346
     *
1347
     * @return Query
1348
     */
1349
    public function getLastQuery()
1350
    {
1351
        return $this->lastQuery;
11✔
1352
    }
1353

1354
    /**
1355
     * Returns a string representation of the last query's statement object.
1356
     */
1357
    public function showLastQuery(): string
1358
    {
UNCOV
1359
        return (string) $this->lastQuery;
×
1360
    }
1361

1362
    /**
1363
     * Returns the time we started to connect to this database in
1364
     * seconds with microseconds.
1365
     *
1366
     * Used by the Debug Toolbar's timeline.
1367
     */
1368
    public function getConnectStart(): ?float
1369
    {
1370
        return $this->connectTime;
1✔
1371
    }
1372

1373
    /**
1374
     * Returns the number of seconds with microseconds that it took
1375
     * to connect to the database.
1376
     *
1377
     * Used by the Debug Toolbar's timeline.
1378
     */
1379
    public function getConnectDuration(int $decimals = 6): string
1380
    {
1381
        return number_format($this->connectDuration, $decimals);
2✔
1382
    }
1383

1384
    /**
1385
     * Protect Identifiers
1386
     *
1387
     * This function is used extensively by the Query Builder class, and by
1388
     * a couple functions in this class.
1389
     * It takes a column or table name (optionally with an alias) and inserts
1390
     * the table prefix onto it. Some logic is necessary in order to deal with
1391
     * column names that include the path. Consider a query like this:
1392
     *
1393
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1394
     *
1395
     * Or a query with aliasing:
1396
     *
1397
     * SELECT m.member_id, m.member_name FROM members AS m
1398
     *
1399
     * Since the column name can include up to four segments (host, DB, table, column)
1400
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1401
     * insert the table prefix (if it exists) in the proper position, and escape only
1402
     * the correct identifiers.
1403
     *
1404
     * @param array|int|string|TableName $item
1405
     * @param bool                       $prefixSingle       Prefix a table name with no segments?
1406
     * @param bool                       $protectIdentifiers Protect table or column names?
1407
     * @param bool                       $fieldExists        Supplied $item contains a column name?
1408
     *
1409
     * @return ($item is array ? array : string)
1410
     */
1411
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1412
    {
1413
        if (! is_bool($protectIdentifiers)) {
1,224✔
1414
            $protectIdentifiers = $this->protectIdentifiers;
1,191✔
1415
        }
1416

1417
        if (is_array($item)) {
1,224✔
1418
            $escapedArray = [];
1✔
1419

1420
            foreach ($item as $k => $v) {
1✔
1421
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1422
            }
1423

1424
            return $escapedArray;
1✔
1425
        }
1426

1427
        if ($item instanceof TableName) {
1,224✔
1428
            /** @psalm-suppress NoValue I don't know why ERROR. */
1429
            return $this->escapeTableName($item);
2✔
1430
        }
1431

1432
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1433
        $item = (string) $item;
1,224✔
1434

1435
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1436
        // If a parenthesis is found we know that we do not need to
1437
        // escape the data or add a prefix. There's probably a more graceful
1438
        // way to deal with this, but I'm not thinking of it
1439
        //
1440
        // Added exception for single quotes as well, we don't want to alter
1441
        // literal strings.
1442
        if (strcspn($item, "()'") !== strlen($item)) {
1,224✔
1443
            /** @psalm-suppress NoValue I don't know why ERROR. */
1444
            return $item;
859✔
1445
        }
1446

1447
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1448
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,213✔
1449
            /** @psalm-suppress NoValue I don't know why ERROR. */
1450
            return $item;
113✔
1451
        }
1452

1453
        // Convert tabs or multiple spaces into single spaces
1454
        /** @psalm-suppress NoValue I don't know why ERROR. */
1455
        $item = preg_replace('/\s+/', ' ', trim($item));
1,212✔
1456

1457
        // If the item has an alias declaration we remove it and set it aside.
1458
        // Note: strripos() is used in order to support spaces in table names
1459
        if ($offset = strripos($item, ' AS ')) {
1,212✔
1460
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1461
            $item  = substr($item, 0, $offset);
11✔
1462
        } elseif ($offset = strrpos($item, ' ')) {
1,207✔
1463
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
15✔
1464
            $item  = substr($item, 0, $offset);
15✔
1465
        } else {
1466
            $alias = '';
1,200✔
1467
        }
1468

1469
        // Break the string apart if it contains periods, then insert the table prefix
1470
        // in the correct location, assuming the period doesn't indicate that we're dealing
1471
        // with an alias. While we're at it, we will escape the components
1472
        if (str_contains($item, '.')) {
1,212✔
1473
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
157✔
1474
        }
1475

1476
        // In some cases, especially 'from', we end up running through
1477
        // protect_identifiers twice. This algorithm won't work when
1478
        // it contains the escapeChar so strip it out.
1479
        $item = trim($item, $this->escapeChar);
1,204✔
1480

1481
        // Is there a table prefix? If not, no need to insert it
1482
        if ($this->DBPrefix !== '') {
1,204✔
1483
            // Verify table prefix and replace if necessary
1484
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
898✔
UNCOV
1485
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1486
            }
1487
            // Do we prefix an item with no segments?
1488
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
898✔
1489
                $item = $this->DBPrefix . $item;
891✔
1490
            }
1491
        }
1492

1493
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,204✔
1494
            $item = $this->escapeIdentifiers($item);
1,202✔
1495
        }
1496

1497
        return $item . $alias;
1,204✔
1498
    }
1499

1500
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1501
    {
1502
        $parts = explode('.', $item);
157✔
1503

1504
        // Does the first segment of the exploded item match
1505
        // one of the aliases previously identified? If so,
1506
        // we have nothing more to do other than escape the item
1507
        //
1508
        // NOTE: The ! empty() condition prevents this method
1509
        // from breaking when QB isn't enabled.
1510
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
157✔
1511
            if ($protectIdentifiers) {
12✔
1512
                foreach ($parts as $key => $val) {
12✔
1513
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
12✔
1514
                        $parts[$key] = $this->escapeIdentifiers($val);
12✔
1515
                    }
1516
                }
1517

1518
                $item = implode('.', $parts);
12✔
1519
            }
1520

1521
            return $item . $alias;
12✔
1522
        }
1523

1524
        // Is there a table prefix defined in the config file? If not, no need to do anything
1525
        if ($this->DBPrefix !== '') {
150✔
1526
            // We now add the table prefix based on some logic.
1527
            // Do we have 4 segments (hostname.database.table.column)?
1528
            // If so, we add the table prefix to the column name in the 3rd segment.
1529
            if (isset($parts[3])) {
134✔
UNCOV
1530
                $i = 2;
×
1531
            }
1532
            // Do we have 3 segments (database.table.column)?
1533
            // If so, we add the table prefix to the column name in 2nd position
1534
            elseif (isset($parts[2])) {
134✔
UNCOV
1535
                $i = 1;
×
1536
            }
1537
            // Do we have 2 segments (table.column)?
1538
            // If so, we add the table prefix to the column name in 1st segment
1539
            else {
1540
                $i = 0;
134✔
1541
            }
1542

1543
            // This flag is set when the supplied $item does not contain a field name.
1544
            // This can happen when this function is being called from a JOIN.
1545
            if ($fieldExists === false) {
134✔
UNCOV
1546
                $i++;
×
1547
            }
1548

1549
            // Verify table prefix and replace if necessary
1550
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
134✔
UNCOV
1551
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1552
            }
1553
            // We only add the table prefix if it does not already exist
1554
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
134✔
1555
                $parts[$i] = $this->DBPrefix . $parts[$i];
134✔
1556
            }
1557

1558
            // Put the parts back together
1559
            $item = implode('.', $parts);
134✔
1560
        }
1561

1562
        if ($protectIdentifiers) {
150✔
1563
            $item = $this->escapeIdentifiers($item);
150✔
1564
        }
1565

1566
        return $item . $alias;
150✔
1567
    }
1568

1569
    /**
1570
     * Escape the SQL Identifier
1571
     *
1572
     * This function escapes single identifier.
1573
     *
1574
     * @param non-empty-string|TableName $item
1575
     */
1576
    public function escapeIdentifier($item): string
1577
    {
1578
        if ($item === '') {
787✔
UNCOV
1579
            return '';
×
1580
        }
1581

1582
        if ($item instanceof TableName) {
787✔
1583
            return $this->escapeTableName($item);
7✔
1584
        }
1585

1586
        return $this->escapeChar
787✔
1587
            . str_replace(
787✔
1588
                $this->escapeChar,
787✔
1589
                $this->escapeChar . $this->escapeChar,
787✔
1590
                $item,
787✔
1591
            )
787✔
1592
            . $this->escapeChar;
787✔
1593
    }
1594

1595
    /**
1596
     * Returns escaped table name with alias.
1597
     */
1598
    private function escapeTableName(TableName $tableName): string
1599
    {
1600
        $alias = $tableName->getAlias();
7✔
1601

1602
        return $this->escapeIdentifier($tableName->getActualTableName())
7✔
1603
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7✔
1604
    }
1605

1606
    /**
1607
     * Escape the SQL Identifiers
1608
     *
1609
     * This function escapes column and table names
1610
     *
1611
     * @param array|string $item
1612
     *
1613
     * @return ($item is array ? array : string)
1614
     */
1615
    public function escapeIdentifiers($item)
1616
    {
1617
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
1,230✔
1618
            return $item;
5✔
1619
        }
1620

1621
        if (is_array($item)) {
1,229✔
1622
            foreach ($item as $key => $value) {
802✔
1623
                $item[$key] = $this->escapeIdentifiers($value);
802✔
1624
            }
1625

1626
            return $item;
802✔
1627
        }
1628

1629
        // Avoid breaking functions and literal values inside queries
1630
        if (ctype_digit($item)
1,229✔
1631
            || $item[0] === "'"
1,228✔
1632
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,228✔
1633
            || str_contains($item, '(')) {
1,229✔
1634
            return $item;
48✔
1635
        }
1636

1637
        if ($this->pregEscapeChar === []) {
1,228✔
1638
            if (is_array($this->escapeChar)) {
383✔
1639
                $this->pregEscapeChar = [
×
1640
                    preg_quote($this->escapeChar[0], '/'),
×
1641
                    preg_quote($this->escapeChar[1], '/'),
×
1642
                    $this->escapeChar[0],
×
UNCOV
1643
                    $this->escapeChar[1],
×
UNCOV
1644
                ];
×
1645
            } else {
1646
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
383✔
1647
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
383✔
1648
            }
1649
        }
1650

1651
        foreach ($this->reservedIdentifiers as $id) {
1,228✔
1652
            /** @psalm-suppress NoValue I don't know why ERROR. */
1653
            if (str_contains($item, '.' . $id)) {
1,228✔
1654
                return preg_replace(
3✔
1655
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1656
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1657
                    $item,
3✔
1658
                );
3✔
1659
            }
1660
        }
1661

1662
        /** @psalm-suppress NoValue I don't know why ERROR. */
1663
        return preg_replace(
1,226✔
1664
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,226✔
1665
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,226✔
1666
            $item,
1,226✔
1667
        );
1,226✔
1668
    }
1669

1670
    /**
1671
     * Prepends a database prefix if one exists in configuration
1672
     *
1673
     * @throws DatabaseException
1674
     */
1675
    public function prefixTable(string $table = ''): string
1676
    {
1677
        if ($table === '') {
3✔
UNCOV
1678
            throw new DatabaseException('A table name is required for that operation.');
×
1679
        }
1680

1681
        return $this->DBPrefix . $table;
3✔
1682
    }
1683

1684
    /**
1685
     * Returns the total number of rows affected by this query.
1686
     */
1687
    abstract public function affectedRows(): int;
1688

1689
    /**
1690
     * "Smart" Escape String
1691
     *
1692
     * Escapes data based on type.
1693
     * Sets boolean and null types
1694
     *
1695
     * @param array|bool|float|int|object|string|null $str
1696
     *
1697
     * @return ($str is array ? array : float|int|string)
1698
     */
1699
    public function escape($str)
1700
    {
1701
        if (is_array($str)) {
1,003✔
1702
            return array_map($this->escape(...), $str);
816✔
1703
        }
1704

1705
        if ($str instanceof Stringable) {
1,003✔
1706
            if ($str instanceof RawSql) {
13✔
1707
                return $str->__toString();
12✔
1708
            }
1709

1710
            $str = (string) $str;
1✔
1711
        }
1712

1713
        if (is_string($str)) {
1,000✔
1714
            return "'" . $this->escapeString($str) . "'";
938✔
1715
        }
1716

1717
        if (is_bool($str)) {
921✔
1718
            return ($str === false) ? 0 : 1;
8✔
1719
        }
1720

1721
        return $str ?? 'NULL';
919✔
1722
    }
1723

1724
    /**
1725
     * Escape String
1726
     *
1727
     * @param list<string|Stringable>|string|Stringable $str  Input string
1728
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1729
     *
1730
     * @return list<string>|string
1731
     */
1732
    public function escapeString($str, bool $like = false)
1733
    {
1734
        if (is_array($str)) {
938✔
UNCOV
1735
            foreach ($str as $key => $val) {
×
UNCOV
1736
                $str[$key] = $this->escapeString($val, $like);
×
1737
            }
1738

UNCOV
1739
            return $str;
×
1740
        }
1741

1742
        if ($str instanceof Stringable) {
938✔
1743
            if ($str instanceof RawSql) {
2✔
UNCOV
1744
                return $str->__toString();
×
1745
            }
1746

1747
            $str = (string) $str;
2✔
1748
        }
1749

1750
        $str = $this->_escapeString($str);
938✔
1751

1752
        // escape LIKE condition wildcards
1753
        if ($like) {
938✔
1754
            return str_replace(
2✔
1755
                [
2✔
1756
                    $this->likeEscapeChar,
2✔
1757
                    '%',
2✔
1758
                    '_',
2✔
1759
                ],
2✔
1760
                [
2✔
1761
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1762
                    $this->likeEscapeChar . '%',
2✔
1763
                    $this->likeEscapeChar . '_',
2✔
1764
                ],
2✔
1765
                $str,
2✔
1766
            );
2✔
1767
        }
1768

1769
        return $str;
938✔
1770
    }
1771

1772
    /**
1773
     * Escape LIKE String
1774
     *
1775
     * Calls the individual driver for platform
1776
     * specific escaping for LIKE conditions
1777
     *
1778
     * @param list<string|Stringable>|string|Stringable $str
1779
     *
1780
     * @return list<string>|string
1781
     */
1782
    public function escapeLikeString($str)
1783
    {
1784
        return $this->escapeString($str, true);
2✔
1785
    }
1786

1787
    /**
1788
     * Platform independent string escape.
1789
     *
1790
     * Will likely be overridden in child classes.
1791
     */
1792
    protected function _escapeString(string $str): string
1793
    {
1794
        return str_replace("'", "''", remove_invisible_characters($str, false));
892✔
1795
    }
1796

1797
    /**
1798
     * This function enables you to call PHP database functions that are not natively included
1799
     * in CodeIgniter, in a platform independent manner.
1800
     *
1801
     * @param array ...$params
1802
     *
1803
     * @throws DatabaseException
1804
     */
1805
    public function callFunction(string $functionName, ...$params): bool
1806
    {
1807
        $driver = $this->getDriverFunctionPrefix();
2✔
1808

1809
        if (! str_starts_with($functionName, $driver)) {
2✔
1810
            $functionName = $driver . $functionName;
1✔
1811
        }
1812

1813
        if (! function_exists($functionName)) {
2✔
UNCOV
1814
            if ($this->DBDebug) {
×
UNCOV
1815
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1816
            }
1817

UNCOV
1818
            return false;
×
1819
        }
1820

1821
        return $functionName(...$params);
2✔
1822
    }
1823

1824
    /**
1825
     * Get the prefix of the function to access the DB.
1826
     */
1827
    protected function getDriverFunctionPrefix(): string
1828
    {
UNCOV
1829
        return strtolower($this->DBDriver) . '_';
×
1830
    }
1831

1832
    // --------------------------------------------------------------------
1833
    // META Methods
1834
    // --------------------------------------------------------------------
1835

1836
    /**
1837
     * Returns an array of table names
1838
     *
1839
     * @return false|list<string>
1840
     *
1841
     * @throws DatabaseException
1842
     */
1843
    public function listTables(bool $constrainByPrefix = false)
1844
    {
1845
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
850✔
1846
            $tables = $constrainByPrefix
844✔
1847
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1848
                : $this->dataCache['table_names'];
844✔
1849

1850
            return array_values($tables);
844✔
1851
        }
1852

1853
        $sql = $this->_listTables($constrainByPrefix);
84✔
1854

1855
        if ($sql === false) {
84✔
1856
            if ($this->DBDebug) {
×
UNCOV
1857
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1858
            }
1859

UNCOV
1860
            return false;
×
1861
        }
1862

1863
        $this->dataCache['table_names'] = [];
84✔
1864

1865
        $query = $this->query($sql);
84✔
1866

1867
        foreach ($query->getResultArray() as $row) {
84✔
1868
            /** @var string $table */
1869
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
81✔
1870

1871
            $this->dataCache['table_names'][] = $table;
81✔
1872
        }
1873

1874
        return $this->dataCache['table_names'];
84✔
1875
    }
1876

1877
    /**
1878
     * Determine if a particular table exists
1879
     *
1880
     * @param bool $cached Whether to use data cache
1881
     */
1882
    public function tableExists(string $tableName, bool $cached = true): bool
1883
    {
1884
        if ($cached) {
844✔
1885
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
843✔
1886
        }
1887

1888
        if (false === ($sql = $this->_listTables(false, $tableName))) {
797✔
1889
            if ($this->DBDebug) {
×
UNCOV
1890
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1891
            }
1892

UNCOV
1893
            return false;
×
1894
        }
1895

1896
        $tableExists = $this->query($sql)->getResultArray() !== [];
797✔
1897

1898
        // if cache has been built already
1899
        if (! empty($this->dataCache['table_names'])) {
797✔
1900
            $key = array_search(
793✔
1901
                strtolower($tableName),
793✔
1902
                array_map(strtolower(...), $this->dataCache['table_names']),
793✔
1903
                true,
793✔
1904
            );
793✔
1905

1906
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1907
            // OR if table does exist but is not found in cache
1908
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
793✔
1909
                $this->resetDataCache();
1✔
1910
            }
1911
        }
1912

1913
        return $tableExists;
797✔
1914
    }
1915

1916
    /**
1917
     * Fetch Field Names
1918
     *
1919
     * @param string|TableName $tableName
1920
     *
1921
     * @return false|list<string>
1922
     *
1923
     * @throws DatabaseException
1924
     */
1925
    public function getFieldNames($tableName)
1926
    {
1927
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12✔
1928

1929
        // Is there a cached result?
1930
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1931
            return $this->dataCache['field_names'][$table];
7✔
1932
        }
1933

1934
        if (empty($this->connID)) {
8✔
1935
            $this->initialize();
×
1936
        }
1937

1938
        if (false === ($sql = $this->_listColumns($tableName))) {
8✔
1939
            if ($this->DBDebug) {
×
UNCOV
1940
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1941
            }
1942

UNCOV
1943
            return false;
×
1944
        }
1945

1946
        $query = $this->query($sql);
8✔
1947

1948
        $this->dataCache['field_names'][$table] = [];
8✔
1949

1950
        foreach ($query->getResultArray() as $row) {
8✔
1951
            // Do we know from where to get the column's name?
1952
            if (! isset($key)) {
8✔
1953
                if (isset($row['column_name'])) {
8✔
1954
                    $key = 'column_name';
8✔
1955
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1956
                    $key = 'COLUMN_NAME';
8✔
1957
                } else {
1958
                    // We have no other choice but to just get the first element's key.
1959
                    $key = key($row);
8✔
1960
                }
1961
            }
1962

1963
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1964
        }
1965

1966
        return $this->dataCache['field_names'][$table];
8✔
1967
    }
1968

1969
    /**
1970
     * Determine if a particular field exists
1971
     */
1972
    public function fieldExists(string $fieldName, string $tableName): bool
1973
    {
1974
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1975
    }
1976

1977
    /**
1978
     * Returns an object with field data
1979
     *
1980
     * @return list<stdClass>
1981
     */
1982
    public function getFieldData(string $table)
1983
    {
1984
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
150✔
1985
    }
1986

1987
    /**
1988
     * Returns an object with key data
1989
     *
1990
     * @return array<string, stdClass>
1991
     */
1992
    public function getIndexData(string $table)
1993
    {
1994
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
165✔
1995
    }
1996

1997
    /**
1998
     * Returns an object with foreign key data
1999
     *
2000
     * @return array<string, stdClass>
2001
     */
2002
    public function getForeignKeyData(string $table)
2003
    {
2004
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
37✔
2005
    }
2006

2007
    /**
2008
     * Converts array of arrays generated by _foreignKeyData() to array of objects
2009
     *
2010
     * @return array<string, stdClass>
2011
     *
2012
     * array[
2013
     *    {constraint_name} =>
2014
     *        stdClass[
2015
     *            'constraint_name'     => string,
2016
     *            'table_name'          => string,
2017
     *            'column_name'         => string[],
2018
     *            'foreign_table_name'  => string,
2019
     *            'foreign_column_name' => string[],
2020
     *            'on_delete'           => string,
2021
     *            'on_update'           => string,
2022
     *            'match'               => string
2023
     *        ]
2024
     * ]
2025
     */
2026
    protected function foreignKeyDataToObjects(array $data)
2027
    {
2028
        $retVal = [];
37✔
2029

2030
        foreach ($data as $row) {
37✔
2031
            $name = $row['constraint_name'];
12✔
2032

2033
            // for sqlite generate name
2034
            if ($name === null) {
12✔
2035
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
2036
            }
2037

2038
            $obj                      = new stdClass();
12✔
2039
            $obj->constraint_name     = $name;
12✔
2040
            $obj->table_name          = $row['table_name'];
12✔
2041
            $obj->column_name         = $row['column_name'];
12✔
2042
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
2043
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
2044
            $obj->on_delete           = $row['on_delete'];
12✔
2045
            $obj->on_update           = $row['on_update'];
12✔
2046
            $obj->match               = $row['match'];
12✔
2047

2048
            $retVal[$name] = $obj;
12✔
2049
        }
2050

2051
        return $retVal;
37✔
2052
    }
2053

2054
    /**
2055
     * Disables foreign key checks temporarily.
2056
     *
2057
     * @return bool
2058
     */
2059
    public function disableForeignKeyChecks()
2060
    {
2061
        $sql = $this->_disableForeignKeyChecks();
811✔
2062

2063
        if ($sql === '') {
811✔
2064
            // The feature is not supported.
UNCOV
2065
            return false;
×
2066
        }
2067

2068
        return $this->query($sql);
811✔
2069
    }
2070

2071
    /**
2072
     * Enables foreign key checks temporarily.
2073
     *
2074
     * @return bool
2075
     */
2076
    public function enableForeignKeyChecks()
2077
    {
2078
        $sql = $this->_enableForeignKeyChecks();
894✔
2079

2080
        if ($sql === '') {
894✔
2081
            // The feature is not supported.
UNCOV
2082
            return false;
×
2083
        }
2084

2085
        return $this->query($sql);
894✔
2086
    }
2087

2088
    /**
2089
     * Allows the engine to be set into a mode where queries are not
2090
     * actually executed, but they are still generated, timed, etc.
2091
     *
2092
     * This is primarily used by the prepared query functionality.
2093
     *
2094
     * @return $this
2095
     */
2096
    public function pretend(bool $pretend = true)
2097
    {
2098
        $this->pretend = $pretend;
18✔
2099

2100
        return $this;
18✔
2101
    }
2102

2103
    /**
2104
     * Empties our data cache. Especially helpful during testing.
2105
     *
2106
     * @return $this
2107
     */
2108
    public function resetDataCache()
2109
    {
2110
        $this->dataCache = [];
36✔
2111

2112
        return $this;
36✔
2113
    }
2114

2115
    /**
2116
     * Determines if the statement is a write-type query or not.
2117
     *
2118
     * @param string $sql
2119
     */
2120
    public function isWriteType($sql): bool
2121
    {
2122
        return (bool) preg_match('/^\s*(WITH\s.+(\s|[)]))?"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s(?!.*\sRETURNING\s)/is', $sql);
920✔
2123
    }
2124

2125
    /**
2126
     * Returns the last error code and message.
2127
     *
2128
     * Must return an array with keys 'code' and 'message':
2129
     *
2130
     * @return array{code: int|string|null, message: string|null}
2131
     */
2132
    abstract public function error(): array;
2133

2134
    /**
2135
     * Returns the exception that would have been thrown on the last failed
2136
     * query if DBDebug were enabled. Returns null if the last query succeeded
2137
     * or if DBDebug is true (in which case the exception is always thrown
2138
     * directly and this method will always return null).
2139
     */
2140
    public function getLastException(): ?DatabaseException
2141
    {
2142
        return $this->lastException;
6✔
2143
    }
2144

2145
    /**
2146
     * Sets the exception for the last failed database operation.
2147
     *
2148
     * @internal This method is for internal database component use only.
2149
     */
2150
    public function setLastException(?DatabaseException $exception): void
2151
    {
2152
        $this->lastException = $exception;
17✔
2153
    }
2154

2155
    /**
2156
     * Checks whether the native database error represents a unique constraint violation.
2157
     */
2158
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
2159
    {
2160
        return false;
1✔
2161
    }
2162

2163
    /**
2164
     * Checks whether the native database code represents a retryable transaction failure.
2165
     */
2166
    protected function isRetryableTransactionErrorCode(int|string $code): bool
2167
    {
2168
        return false;
1✔
2169
    }
2170

2171
    /**
2172
     * Creates the appropriate database exception for a native database error.
2173
     *
2174
     * @internal This method is for internal database component use only.
2175
     */
2176
    public function createDatabaseException(
2177
        string $message,
2178
        int|string $code = 0,
2179
        ?Throwable $previous = null,
2180
    ): DatabaseException {
2181
        if ($this->isUniqueConstraintViolation($code, $message)) {
75✔
2182
            return new UniqueConstraintViolationException($message, $code, $previous);
38✔
2183
        }
2184

2185
        if ($this->isRetryableTransactionErrorCode($code)) {
57✔
2186
            return new RetryableTransactionException($message, $code, $previous);
11✔
2187
        }
2188

2189
        return new DatabaseException($message, $code, $previous);
46✔
2190
    }
2191

2192
    /**
2193
     * Insert ID
2194
     *
2195
     * @return int|string
2196
     */
2197
    abstract public function insertID();
2198

2199
    /**
2200
     * Generates the SQL for listing tables in a platform-dependent manner.
2201
     *
2202
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
2203
     *
2204
     * @return false|string
2205
     */
2206
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
2207

2208
    /**
2209
     * Generates a platform-specific query string so that the column names can be fetched.
2210
     *
2211
     * @param string|TableName $table
2212
     *
2213
     * @return false|string
2214
     */
2215
    abstract protected function _listColumns($table = '');
2216

2217
    /**
2218
     * Platform-specific field data information.
2219
     *
2220
     * @see getFieldData()
2221
     *
2222
     * @return list<stdClass>
2223
     */
2224
    abstract protected function _fieldData(string $table): array;
2225

2226
    /**
2227
     * Platform-specific index data.
2228
     *
2229
     * @see    getIndexData()
2230
     *
2231
     * @return array<string, stdClass>
2232
     */
2233
    abstract protected function _indexData(string $table): array;
2234

2235
    /**
2236
     * Platform-specific foreign keys data.
2237
     *
2238
     * @see    getForeignKeyData()
2239
     *
2240
     * @return array<string, stdClass>
2241
     */
2242
    abstract protected function _foreignKeyData(string $table): array;
2243

2244
    /**
2245
     * Platform-specific SQL statement to disable foreign key checks.
2246
     *
2247
     * If this feature is not supported, return empty string.
2248
     *
2249
     * @TODO This method should be moved to an interface that represents foreign key support.
2250
     *
2251
     * @return string
2252
     *
2253
     * @see disableForeignKeyChecks()
2254
     */
2255
    protected function _disableForeignKeyChecks()
2256
    {
UNCOV
2257
        return '';
×
2258
    }
2259

2260
    /**
2261
     * Platform-specific SQL statement to enable foreign key checks.
2262
     *
2263
     * If this feature is not supported, return empty string.
2264
     *
2265
     * @TODO This method should be moved to an interface that represents foreign key support.
2266
     *
2267
     * @return string
2268
     *
2269
     * @see enableForeignKeyChecks()
2270
     */
2271
    protected function _enableForeignKeyChecks()
2272
    {
UNCOV
2273
        return '';
×
2274
    }
2275

2276
    /**
2277
     * Converts a named timezone to an offset string.
2278
     *
2279
     * Converts timezone identifiers (e.g., 'America/New_York') to offset strings
2280
     * (e.g., '-05:00' or '-04:00' depending on DST). This is useful because not all
2281
     * databases have timezone tables loaded, but all support offset notation.
2282
     *
2283
     * @param string $timezone Named timezone (e.g., 'America/New_York', 'UTC', 'Europe/Paris')
2284
     *
2285
     * @return string Offset string (e.g., '+00:00', '-05:00', '+01:00')
2286
     */
2287
    protected function convertTimezoneToOffset(string $timezone): string
2288
    {
2289
        // If it's already an offset, return as-is
2290
        if (preg_match('/^[+-]\d{2}:\d{2}$/', $timezone)) {
9✔
2291
            return $timezone;
3✔
2292
        }
2293

2294
        try {
2295
            $offset = Time::now($timezone)->getOffset();
6✔
2296

2297
            // Convert offset seconds to +-HH:MM format
2298
            $hours   = (int) ($offset / 3600);
5✔
2299
            $minutes = abs((int) (($offset % 3600) / 60));
5✔
2300

2301
            return sprintf('%+03d:%02d', $hours, $minutes);
5✔
2302
        } catch (Exception $e) {
1✔
2303
            // If timezone conversion fails, log and return UTC
2304
            log_message('error', "Invalid timezone '{$timezone}'. Falling back to UTC. {$e->getMessage()}.");
1✔
2305

2306
            return '+00:00';
1✔
2307
        }
2308
    }
2309

2310
    /**
2311
     * Gets the timezone string to use for database session.
2312
     *
2313
     * Handles the timezone configuration logic:
2314
     * - false: Don't set timezone (returns null)
2315
     * - true: Auto-sync with app timezone from config
2316
     * - string: Use specific timezone (converts named timezones to offsets)
2317
     *
2318
     * @return string|null The timezone offset string, or null if timezone should not be set
2319
     */
2320
    protected function getSessionTimezone(): ?string
2321
    {
2322
        if ($this->timezone === false) {
68✔
2323
            return null;
62✔
2324
        }
2325

2326
        // Auto-sync with app timezone
2327
        if ($this->timezone === true) {
6✔
2328
            $appConfig = config('App');
2✔
2329
            $timezone  = $appConfig->appTimezone;
2✔
2330
        } else {
2331
            // Use specific timezone from config
2332
            $timezone = $this->timezone;
4✔
2333
        }
2334

2335
        return $this->convertTimezoneToOffset($timezone);
6✔
2336
    }
2337

2338
    /**
2339
     * Accessor for properties if they exist.
2340
     *
2341
     * @return array|bool|float|int|object|resource|string|null
2342
     */
2343
    public function __get(string $key)
2344
    {
2345
        if (property_exists($this, $key)) {
1,188✔
2346
            return $this->{$key};
1,187✔
2347
        }
2348

2349
        return null;
1✔
2350
    }
2351

2352
    /**
2353
     * Checker for properties existence.
2354
     */
2355
    public function __isset(string $key): bool
2356
    {
2357
        return property_exists($this, $key);
291✔
2358
    }
2359
}
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