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

rotexsoft / leanorm / 25626720283

10 May 2026 10:46AM UTC coverage: 96.372% (-0.04%) from 96.41%
25626720283

push

github

rotexdegba
Pre 7.x release updates

61 of 62 new or added lines in 2 files covered. (98.39%)

5 existing lines in 3 files now uncovered.

1700 of 1764 relevant lines covered (96.37%)

192.52 hits per line

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

99.34
/src/LeanOrm/DBConnector.php
1
<?php
2
declare(strict_types=1);
3

4
namespace LeanOrm;
5

6
use \Psr\Log\LoggerInterface;
7

8
/**
9
 * 
10
 * This class creates and manages one or more pdo connection(s) to one or more 
11
 * database servers. IT IS A STRIPPED DOWN VERSION OF IDIORM.
12
 * 
13
 * It also provides convenience methods that aggregate common db operations.
14
 * E.g. preparing statements, binding parameters to a statement and then 
15
 *      executing the statement in one single method. 
16
 *
17
 * @author Rotimi Adegbamigbe
18
 * @copyright (c) 2026, Rotexsoft
19
 * 
20
 * BSD Licensed.
21
 *
22
 * Copyright (c) 2010, Jamie Matthews
23
 * All rights reserved.
24
 *
25
 * Redistribution and use in source and binary forms, with or without
26
 * modification, are permitted provided that the following conditions are met:
27
 *
28
 * * Redistributions of source code must retain the above copyright notice, this
29
 *   list of conditions and the following disclaimer.
30
 *
31
 * * Redistributions in binary form must reproduce the above copyright notice,
32
 *   this list of conditions and the following disclaimer in the documentation
33
 *   and/or other materials provided with the distribution.
34
 *
35
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
37
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
39
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
40
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
41
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
42
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
43
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
44
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45
 * 
46
 * @psalm-suppress ClassMustBeFinal
47
 */
48
class DBConnector {
49

50
    // ----------------------- //
51
    // --- CLASS CONSTANTS --- //
52
    // ----------------------- //
53
    /**
54
     * @var string
55
     */
56
    final public const DEFAULT_CONNECTION = 'default';
57

58
    /**
59
     * @var int
60
     */
61
    final public const NANO_SECOND_TO_SECOND_DIVISOR = 1_000_000_000;
62

63
    final public const CONFIG_KEY_USERNAME = 'username';
64

65
    final public const CONFIG_KEY_PASSWORD = 'password';
66

67
    final public const CONFIG_KEY_ERR_MODE = 'error_mode';
68

69
    final public const CONFIG_KEY_DRIVER_OPTS = 'driver_options';
70

71
    final public const CONFIG_KEY_CONNECTION_STR = 'connection_string';
72

73
    final public const LOG_ENTRY_SQL_KEY = 'sql';
74

75
    final public const LOG_ENTRY_BIND_PARAMS_KEY = 'bind_params';
76

77
    final public const LOG_ENTRY_DATE_EXECUTED_KEY = 'date_executed';
78

79
    final public const LOG_ENTRY_CALL_STACK_KEY = 'call_stack';
80

81
    final public const LOG_ENTRY_CALLING_OBJECT_HASH = 'calling_object';
82

83
    final public const LOG_ENTRY_EXEC_TIME_KEY = 'query_execution_time_in_seconds';
84

85
////////////////////////////////////////////////////////////////////////////////        
86
//////////// -------------------------------- //////////////////////////////////
87
//////////// --- CLASS PROPERTIES TO KEEP --- //////////////////////////////////
88
////////////////////////////////////////////////////////////////////////////////
89

90
    // Class configuration
91
    protected static array $default_config = [
92
        self::CONFIG_KEY_CONNECTION_STR => 'sqlite::memory:',
93
        self::CONFIG_KEY_ERR_MODE => \PDO::ERRMODE_EXCEPTION,
94
        self::CONFIG_KEY_USERNAME => null,
95
        self::CONFIG_KEY_PASSWORD => null,
96
        self::CONFIG_KEY_DRIVER_OPTS => null,
97
    ];
98

99
    // Map of configuration settings
100
    protected static array $config = [];
101

102
    /**
103
     * Map of instances of this class keyed on connection name
104
     * 
105
     * @var array<string, \LeanOrm\DBConnector>
106
     */
107
    protected static array $db_connector_instances = [];
108

109
    /**
110
     * Map of database connections, instances of the PDO class
111
     * 
112
     * @var array<string, \PDO>
113
     */
114
    protected static array $pdo_connections = [];
115

116
    /**
117
     * An array containing a log of all queries executed by all instances of this class
118
     * 
119
     * @var array<string, array>
120
     */
121
    protected static array $query_log = [];
122

123
    protected bool $can_log_queries = false;
124

125
    protected null|LoggerInterface $logger = null;
126

127
    //////////// ------------------------------------ //////////////////////////////
128
    //////////// --- END CLASS PROPERTIES TO KEEP --- //////////////////////////////
129
    ////////////////////////////////////////////////////////////////////////////////
130

131
    // ---------------------- //
132
    // --- STATIC METHODS --- //
133
    // ---------------------- //
134

135
    /**
136
     * @param string $connection_name The name of a connection
137
     *                                (registered via \LeanOrm\DBConnector::configure($key, $value, $connection_name)
138
     *                                 or \LeanOrm\DBConnector::create($connection_name))
139
     *                                whose log entries are to be cleared.
140
     *                                Null means clear log for all connections.
141
     *
142
     * @param null|object $object_to_match an object that triggered calling of methods of this class that executed queries in this class.
143
     *                                     Only queries associated with $object_to_match will be cleared from the queries logged for
144
     *                                     $connection_name. If $object_to_match is null then all logged queries for $connection_name
145
     *                                     will be cleared
146
     * @psalm-suppress MixedAssignment
147
     * @psalm-suppress MixedArrayAccess
148
     * @psalm-suppress MixedArrayOffset
149
     * @psalm-suppress RedundantConditionGivenDocblockType
150
     */
151
    public static function clearQueryLog(
152
        null|string $connection_name = null,
153
        null|object $object_to_match = null
154
    ): void {
155

156
        if($connection_name === null) {
80✔
157

158
            // clear all log entires across all connections
159
            static::$query_log = [];
8✔
160

161
        } else {
162

163
            if($object_to_match === null) {
76✔
164

165
                // clear all log entires across for specified connection
166
                static::$query_log[$connection_name] = [];
76✔
167

168
            } else { // $object_to_match !== null
169

170
                $object_class_name = $object_to_match::class;
52✔
171

172
                if(
173
                    isset(static::$query_log[$connection_name])
52✔
174
                    && \is_array(static::$query_log[$connection_name])
52✔
175
                    && \array_key_exists($object_class_name, static::$query_log[$connection_name])
52✔
176
                ) {
177

178
                    $object_hash = \spl_object_hash($object_to_match);
48✔
179

180
                    foreach (static::$query_log[$connection_name][$object_class_name] as $curr_key => $curr_entry) {
48✔
181

182
                        if($object_hash === $curr_entry[static::LOG_ENTRY_CALLING_OBJECT_HASH]) {
48✔
183

184
                            // clear all log entires across for specified connection and only for the specified object
185
                            unset(static::$query_log[$connection_name][$object_class_name][$curr_key]);
48✔
186

187
                        } // if($object_to_match === $curr_entry[static::LOG_ENTRY_CALLING_OBJECT_KEY])
188
                    } // foreach (static::$query_log[$connection_name][$object_class_name] as $curr_key => $curr_entry)
189
                } // if(isset(static::$query_log[$connection_name]) &&  \is_array(static::$query_log[$connection_name]) ...
190
            } // if($object_to_match === null) {} else {}
191
        } // if($connection_name === null){ ... } else { ... }
192
    }
193
    
194
    /**
195
     * The array returned if log is not empty based on supplied arguments to
196
     * this method looks like this (NOTE: an empty array is returned if log
197
     * is empty based on supplied arguments to this method):
198
     *
199
     *   array{
200
     *           $a_connection_name => array {
201
     *
202
     *                   $calling_object::class => array {
203
     *
204
     *                           0 => array {
205
     *                                   DBConnector::LOG_ENTRY_SQL_KEY => $sql_query1_string,
206
     *                                   DBConnector::LOG_ENTRY_BIND_PARAMS_KEY => $bind_parameters_array_for_sql_query1,
207
     *                                   DBConnector::LOG_ENTRY_DATE_EXECUTED_KEY => \date('Y-m-d H:i:s'),
208
     *                                   DBConnector::LOG_ENTRY_EXEC_TIME_KEY => $total_execution_time_in_seconds_for_sql_query1,
209
     *                                   DBConnector::LOG_ENTRY_CALL_STACK_KEY => \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT),
210
     *                                   DBConnector::LOG_ENTRY_CALLING_OBJECT_HASH => \spl_object_hash($an_instance_of_calling_object),
211
     *                           },
212
     *                           ...,
213
     *                           ...,
214
     *                           (N-1) => array {
215
     *                                   DBConnector::LOG_ENTRY_SQL_KEY => $sql_queryN_string,
216
     *                                   DBConnector::LOG_ENTRY_BIND_PARAMS_KEY => $bind_parameters_array_for_sql_queryN,
217
     *                                   DBConnector::LOG_ENTRY_DATE_EXECUTED_KEY => \date('Y-m-d H:i:s'),
218
     *                                   DBConnector::LOG_ENTRY_EXEC_TIME_KEY => $total_execution_time_in_seconds_for_sql_queryN,
219
     *                                   DBConnector::LOG_ENTRY_CALL_STACK_KEY => \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT),
220
     *                                   DBConnector::LOG_ENTRY_CALLING_OBJECT_HASH => \spl_object_hash($an_instance_of_calling_object),
221
     *                           },
222
     *                   },
223
     *                   ...,
224
     *                   ...,
225
     *                   $last_calling_object::class => array {
226
     *
227
     *                           0 => array {
228
     *                                   DBConnector::LOG_ENTRY_SQL_KEY => $sql_query1_string,
229
     *                                   DBConnector::LOG_ENTRY_BIND_PARAMS_KEY => $bind_parameters_array_for_sql_query1,
230
     *                                   DBConnector::LOG_ENTRY_DATE_EXECUTED_KEY => \date('Y-m-d H:i:s'),
231
     *                                   DBConnector::LOG_ENTRY_EXEC_TIME_KEY => $total_execution_time_in_seconds_for_sql_query1,
232
     *                                   DBConnector::LOG_ENTRY_CALL_STACK_KEY => \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT),
233
     *                                   DBConnector::LOG_ENTRY_CALLING_OBJECT_HASH => \spl_object_hash($an_instance_of_last_calling_object),
234
     *                           },
235
     *                           ...,
236
     *                           ...,
237
     *                           (N-1) => array {
238
     *                                   DBConnector::LOG_ENTRY_SQL_KEY => $sql_queryN_string,
239
     *                                   DBConnector::LOG_ENTRY_BIND_PARAMS_KEY => $total_execution_time_in_seconds_for_sql_queryN,
240
     *                                   DBConnector::LOG_ENTRY_DATE_EXECUTED_KEY => \date('Y-m-d H:i:s'),
241
     *                                   DBConnector::LOG_ENTRY_EXEC_TIME_KEY => $total_execution_time_in_seconds_for_sql_queryN,
242
     *                                   DBConnector::LOG_ENTRY_CALL_STACK_KEY => \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT),
243
     *                                   DBConnector::LOG_ENTRY_CALLING_OBJECT_HASH => \spl_object_hash($instance_of_last_calling_object),
244
     *                           },
245
     *                   },
246
     *           },
247
     *           ...,
248
     *           ...,
249
     *           $another_connection_name => array {
250
     *                   ....,
251
     *                   ....,
252
     *           }
253
     *   }
254
     *
255
     *
256
     * @param string $connection_name The name of a connection
257
     *                                (registered via \LeanOrm\DBConnector::configure($key, $value, $connection_name)
258
     *                                 or \LeanOrm\DBConnector::create($connection_name))
259
     *                                whose log entries are to be retrieved.
260
     *                                Null means retrieve log entries for all connections.
261
     *
262
     * @param null|object $object_to_match an object that triggered calling of methods of this class that executed queries in this class.
263
     *                                     Only queries associated with $object_to_match will be returned from the queries logged for
264
     *                                     $connection_name. If $object_to_match is null then all logged queries for $connection_name
265
     *                                     will be returned
266
     * @psalm-suppress MixedAssignment
267
     * @psalm-suppress MixedArrayAccess
268
     * @psalm-suppress MixedArrayOffset
269
     * @psalm-suppress RedundantConditionGivenDocblockType
270
     */
271
    public static function getQueryLog(
272
        null|string $connection_name = null,
273
        null|object $object_to_match = null
274
    ): array {
275
        
276
        $log_entries = [];
76✔
277

278
        if($connection_name === null) {
76✔
279
            
280
            $log_entries = static::$query_log;
8✔
281
            
282
        } else {
283

284
            if($object_to_match === null) {
72✔
285

286
                if(isset(static::$query_log[$connection_name])) {
56✔
287

288
                    $log_entries[$connection_name] = static::$query_log[$connection_name];
56✔
289
                }
290

291
            } else { // $object_to_match !== null
292

293
                $object_class_name = $object_to_match::class;
28✔
294

295
                if(
296
                    isset(static::$query_log[$connection_name])
28✔
297
                    && \is_array(static::$query_log[$connection_name])
28✔
298
                    && \array_key_exists($object_class_name, static::$query_log[$connection_name])
28✔
299
                ) {
300
                    $object_hash = \spl_object_hash($object_to_match);
28✔
301

302
                    foreach (static::$query_log[$connection_name][$object_class_name] as $curr_key => $curr_entry) {
28✔
303

304
                        if($object_hash === $curr_entry[static::LOG_ENTRY_CALLING_OBJECT_HASH]) {
28✔
305

306
                            if(!isset($log_entries[$connection_name])) {
28✔
307

308
                                $log_entries[$connection_name] = [$object_class_name=>[]];
28✔
309
                            }
310

311
                            $log_entries[$connection_name][$object_class_name][$curr_key] = $curr_entry;
28✔
312

313
                        } // if($object_to_match === $curr_entry[static::LOG_ENTRY_CALLING_OBJECT_KEY])
314
                    } // foreach (static::$query_log[$connection_name][$object_class_name] as $curr_key => $curr_entry)
315

316
                } // if(isset(static::$query_log[$connection_name]) &&  \is_array(static::$query_log[$connection_name]) ...
317
            } // if($object_to_match === null) {} else {}
318
        } // if($connection_name === null) { ... } else { ... }
319
        
320
        return $log_entries;
76✔
321
    }
322
    
323
    /**
324
     * Pass configuration settings to the class in the form of
325
     * key/value pairs. As a shortcut, if the second argument
326
     * is omitted and the key is a string, the setting is
327
     * assumed to be the DSN string used by PDO to connect
328
     * to the database (often, this will be the only configuration
329
     * required to use DBConnector). If you have more than one setting
330
     * you wish to configure, another shortcut is to pass an array
331
     * of settings (and omit the second argument).
332
     * 
333
     * @param array<int|string, mixed>|string $key_or_settings
334
     * @param mixed $value value we are setting
335
     * @param string $connection_name Which connection to use
336
     * 
337
     * @psalm-suppress MixedAssignment
338
     * @psalm-suppress MixedArrayAssignment
339
     * @psalm-suppress PossiblyInvalidArgument
340
     */
341
    public static function configure(
342
        array|string $key_or_settings,
343
        mixed $value = null,
344
        string $connection_name = self::DEFAULT_CONNECTION
345
    ): void {
346

347
        static::_initDbConfigWithDefaultVals($connection_name); //ensures at least default config is set
1,500✔
348

349
        if (is_array($key_or_settings)) {
1,500✔
350

351
            // Shortcut: If only one array argument is passed,
352
            // assume it's an array of configuration settings
353
            foreach ($key_or_settings as $conf_key => $conf_value) {
4✔
354

355
                static::configure($conf_key, $conf_value, $connection_name);
4✔
356
            }
357
        } else {
358

359
            if (is_null($value)) {
1,500✔
360

361
                // Shortcut: If only one string argument is passed, 
362
                // assume it's a connection string
363
                $value = $key_or_settings;
1,500✔
364
                $key_or_settings = static::CONFIG_KEY_CONNECTION_STR;
1,500✔
365
            }
366

367
            static::$config[$connection_name][$key_or_settings] = $value;
1,500✔
368
        }
369
    }
370

371
    /**
372
     * This is the factory method used to acquire instances of the class.
373
     * 
374
     * @param string $connection_name Which connection to use
375
     * @psalm-suppress UnsafeInstantiation
376
     */
377
    //rename to factory
378
    public static function create(string $connection_name = self::DEFAULT_CONNECTION): static {
379

380
        static::_setupDb($connection_name);
1,500✔
381
        return new static($connection_name);
1,500✔
382
    }
383

384
    /**
385
     * Returns an instance of this class associated with the specified connection name
386
     * If no instance exists that's associated with the specified connection name, an
387
     * instance of this class is created, associated with the connection name and returned.
388
     * Use this method if you only want one instance of this class per connection name.
389
     *
390
     * @param string $connection_name name of a connection an instance of this class is associated with
391
     * @psalm-suppress PossiblyUnusedMethod
392
     */
393
    public static function getInstance(string $connection_name = self::DEFAULT_CONNECTION): static {
394

395
        if(
396
            !isset(static::$db_connector_instances[$connection_name])
4✔
397
            || !(static::$db_connector_instances[$connection_name] instanceof DBConnector)
4✔
398
        ) {
399
            static::$db_connector_instances[$connection_name] =
4✔
400
                                            static::create($connection_name);
4✔
401
        }
402
        
403
        return static::$db_connector_instances[$connection_name];
4✔
404
    }
405

406
    /**
407
     * Set up the database connection used by the class
408
     * @param string $connection_name Which connection to use
409
     * @psalm-suppress MixedArrayAccess
410
     * @psalm-suppress MixedArgument
411
     */
412
    protected static function _setupDb(string $connection_name = self::DEFAULT_CONNECTION): void {
413

414
        if (
415
            !array_key_exists($connection_name, static::$pdo_connections) ||
1,500✔
416
            !(static::$pdo_connections[$connection_name] instanceof \PDO)
1,500✔
417
        ) {
418

419
            static::_initDbConfigWithDefaultVals($connection_name);
24✔
420

421
            $db = new \PDO(
24✔
422
                static::$config[$connection_name][static::CONFIG_KEY_CONNECTION_STR],
24✔
423
                static::$config[$connection_name][static::CONFIG_KEY_USERNAME],
24✔
424
                static::$config[$connection_name][static::CONFIG_KEY_PASSWORD],
24✔
425
                static::$config[$connection_name][static::CONFIG_KEY_DRIVER_OPTS]
24✔
426
            );
24✔
427

428
            $db->setAttribute(\PDO::ATTR_ERRMODE, static::$config[$connection_name][static::CONFIG_KEY_ERR_MODE]);
16✔
429
            static::setPdo($db, $connection_name);
16✔
430
        }
431
    }
432

433
   /**
434
    * Ensures configuration (multiple connections) is at least set to default.
435
    * @param string $connection_name Which connection to use
436
    */
437
    protected static function _initDbConfigWithDefaultVals(string $connection_name): void {
438

439
        if (!array_key_exists($connection_name, static::$config)) {
1,500✔
440

441
            static::$config[$connection_name] = static::$default_config;
20✔
442
        }
443
    }
444

445
    /**
446
     * Set the PDO object used by DBConnector to communicate with the database.
447
     * This is public in case the DBConnector should use a ready-instantiated
448
     * PDO object as its database connection. Accepts an optional string key
449
     * to identify the connection if multiple connections are used.
450
     * @param string $connection_name Which connection to use
451
     */
452
    public static function setPdo(\PDO $db, string $connection_name = self::DEFAULT_CONNECTION): void {
453

454
        static::_initDbConfigWithDefaultVals($connection_name);
24✔
455
        static::$pdo_connections[$connection_name] = $db;
24✔
456
    }
457

458
    /**
459
     * Returns the PDO instance used by the the DBConnector to communicate with
460
     * the database. This can be called if any low-level DB access is
461
     * required outside the class. If multiple connections are used,
462
     * accepts an optional key name for the connection.
463
     * @param string $connection_name Which connection to use
464
     */
465
    public static function getPdo(string $connection_name = self::DEFAULT_CONNECTION): \PDO {
466

467
        static::_setupDb($connection_name); // required in case this is called before DBConnector is instantiated
1,500✔
468
        return static::$pdo_connections[$connection_name];
1,500✔
469
    }
470
    
471
    /**
472
     * @psalm-suppress PossiblyUnusedMethod
473
     */
474
    public function getMyPdo(): \PDO {
475
        
NEW
476
        return static::getPdo($this->getConnectionName());
×
477
    }
478

479
   /**
480
    * Internal helper method for executing statements.
481
    * 
482
    * @param array $parameters An array of parameters to be bound in to the query
483
    * @param bool $return_pdo_stmt_and_exec_time true to add the \PDOStatement object used by this function & time in seconds it took the query to execute to an array of results to be returned or false to return only the Response of \PDOStatement::execute()
484
    * @param string $connection_name Which connection to use
485
    * 
486
    * @return bool|array{query_result: mixed, pdo_statement: \PDOStatement, exec_time_in_seconds: float} Response of \PDOStatement::execute() if $return_pdo_statement === false or array(bool Response of \PDOStatement::execute(), \PDOStatement the PDOStatement object)
487
    * @deprecated since 7.0, use execute() instead. Will be removed in 8.0.
488
    */
489
    protected static function _execute(string $query, array $parameters = [], bool $return_pdo_stmt_and_exec_time=false, string $connection_name = self::DEFAULT_CONNECTION): bool|array {
490

491
        $statement = static::getPdo($connection_name)->prepare($query);
8✔
492
        
493
        /** @psalm-suppress MixedAssignment */
494
        foreach ($parameters as $key => &$param) {
8✔
495

496
            if (is_null($param)) {
8✔
497

498
                $type = \PDO::PARAM_NULL;
8✔
499

500
            } else if (is_bool($param)) {
8✔
501

502
                $type = \PDO::PARAM_BOOL;
8✔
503

504
            } else {
505

506
                $type = is_int($param) ? \PDO::PARAM_INT : \PDO::PARAM_STR;
8✔
507
            }
508

509
            $statement->bindParam((is_int($key) ? ++$key : $key), $param, $type);
8✔
510
        }
511

512
        $start_time = \hrtime(true); // start timing
8✔
513
        $result = $statement->execute();
8✔
514
        $end_time = \hrtime(true); // stop timing
8✔
515
        $total_execution_time_in_seconds = (($end_time - $start_time) / static::NANO_SECOND_TO_SECOND_DIVISOR);
8✔
516

517
        if( $return_pdo_stmt_and_exec_time ) {
8✔
518

519
            $exec_result = $result;
8✔
520
            $result = ['query_result'=>$exec_result, 'pdo_statement'=>$statement, 'exec_time_in_seconds'=>$total_execution_time_in_seconds];
8✔
521
        }
522

523
        return $result;
8✔
524
    }
525

526
    // ------------------------ //
527
    // --- INSTANCE METHODS --- //
528
    // ------------------------ //
529

530
    /**
531
     * "Private" constructor; shouldn't be called directly.
532
     * Use the DBConnector::create factory method instead.
533
     */
534
    protected function __construct(
535
        // --------------------------- //
536
        // --- INSTANCE PROPERTIES --- //
537
        // --------------------------- //
538
        // Key name of the connection in static::$db used by this instance
539
        protected string $connection_name = self::DEFAULT_CONNECTION
540
    ) {
541
        static::_initDbConfigWithDefaultVals($connection_name);
1,500✔
542
    }
543

544
    /**
545
     * Get connection name for current instance of this class.
546
     */
547
    public function getConnectionName(): string {
548

549
        return $this->connection_name;
1,500✔
550
    }
551
    
552
    public function canLogQueries(): bool {
553
        
554
        return $this->can_log_queries;
1,500✔
555
    }
556

557
    public function enableQueryLogging(): static {
558

559
        $this->can_log_queries = true;
100✔
560
        return $this;
100✔
561
    }
562
    
563
    public function disableQueryLogging(): static {
564

565
        $this->can_log_queries = false;
76✔
566
        return $this;
76✔
567
    }
568

569
    public function setLogger(?LoggerInterface $logger): static {
570
        
571
        $this->logger = $logger;
184✔
572
        return $this;
184✔
573
    }
574
    
575
    public function getLogger(): ?LoggerInterface { return $this->logger; }
576

577
    /**
578
     * Executes a raw query as a wrapper for PDOStatement::execute.
579
     * 
580
     * @param string $query The raw SQL query
581
     * @param array  $parameters Optional bound parameters
582
     * @param bool $return_pdo_stmt_and_exec_time true to add the \PDOStatement object used by this function & time in seconds it took the query to execute to an array of results to be returned or false to return only the Response of \PDOStatement::execute()
583
     * 
584
     * @return bool|array{query_result: mixed, pdo_statement: \PDOStatement, exec_time_in_seconds: float} bool Response of \PDOStatement::execute() if $return_pdo_statement === false or array(bool Response of \PDOStatement::execute(), \PDOStatement the PDOStatement object)
585
     * @deprecated since 7.0, use runQuery() instead. Will be removed in 8.0.
586
     * @psalm-suppress PossiblyUnusedMethod
587
     * @psalm-suppress DeprecatedMethod
588
     */
589
    public function executeQuery(
590
        string $query,
591
        array $parameters=[],
592
        bool $return_pdo_stmt_and_exec_time=false
593
    ): bool|array {
594

595
        return static::_execute($query, $parameters, $return_pdo_stmt_and_exec_time, $this->connection_name);
4✔
596
    }
597

598
   /**
599
    * Internal helper method for executing statements.
600
    * 
601
    * @param string $query Sql query to execute
602
    * @param  array $parameters An array of parameters to be bound in to the query
603
    * @param string $connection_name Which connection to use
604
    * @param null|object $calling_object object that called a method which triggered
605
    *                                    the calling of this method. A hash of this 
606
    *                                    object is added to each query log entry. 
607
    *                                    If null, it will be set to the instance
608
    *                                    of this class this being method is being
609
    *                                    called with
610
    * @psalm-suppress MixedArrayAssignment
611
    * @psalm-suppress PossiblyNullReference
612
    */
613
    protected function execute(
614
        string $query,
615
        array $parameters = [],
616
        string $connection_name = self::DEFAULT_CONNECTION,
617
        ?object $calling_object = null
618
    ): DBExceuteQueryResult {
619

620
        if($calling_object === null) { $calling_object = $this; }
1,500✔
621
        
622
        $result = false;
1,500✔
623
        $total_execution_time_in_seconds = 0;
1,500✔
624
        $statement = static::getPdo($connection_name)->prepare($query);
1,500✔
625
        
626
        if($statement instanceof \PDOStatement) {
1,500✔
627

628
            /** @psalm-suppress MixedAssignment */
629
            foreach ($parameters as $key => &$param) {
1,500✔
630

631
                if (is_null($param)) {
268✔
632

633
                    $type = \PDO::PARAM_NULL;
44✔
634

635
                } else if (is_bool($param)) {
268✔
636

637
                    $type = \PDO::PARAM_BOOL;
36✔
638

639
                } else {
640

641
                    $type = is_int($param) ? \PDO::PARAM_INT : \PDO::PARAM_STR;
268✔
642
                }
643

644
                $statement->bindParam((is_int($key) ? ++$key : $key), $param, $type);
268✔
645
            }
646

647
            $start_time = \hrtime(true); // start timing
1,500✔
648
            $result = $statement->execute();
1,500✔
649
            $end_time = \hrtime(true); // stop timing
1,500✔
650
            $total_execution_time_in_seconds = (($end_time - $start_time) / static::NANO_SECOND_TO_SECOND_DIVISOR);
1,500✔
651

652
            if($this->canLogQueries()) {
1,500✔
653

654
                $call_trace = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT|DEBUG_BACKTRACE_IGNORE_ARGS);
84✔
655

656
                if(!array_key_exists($connection_name, static::$query_log)) {
84✔
657

658
                    static::$query_log[$connection_name] = [];
12✔
659
                }
660

661
                if(!array_key_exists($calling_object::class, static::$query_log[$connection_name])) {
84✔
662

663
                    static::$query_log[$connection_name][$calling_object::class] = [];
84✔
664
                }
665

666
                $log_entry = [
84✔
667
                    static::LOG_ENTRY_SQL_KEY => $query,
84✔
668
                    static::LOG_ENTRY_BIND_PARAMS_KEY => $parameters,
84✔
669
                    static::LOG_ENTRY_DATE_EXECUTED_KEY => \date('Y-m-d H:i:s'),
84✔
670
                    static::LOG_ENTRY_EXEC_TIME_KEY => $total_execution_time_in_seconds,
84✔
671
                    static::LOG_ENTRY_CALL_STACK_KEY => $call_trace,
84✔
672
                    static::LOG_ENTRY_CALLING_OBJECT_HASH => \spl_object_hash($calling_object),
84✔
673
                ];
84✔
674
                static::$query_log[$connection_name][$calling_object::class][] = $log_entry;
84✔
675

676
                if($this->getLogger() instanceof \Psr\Log\LoggerInterface) {
84✔
677

678
                    $this->getLogger()->info(
8✔
679
                        PHP_EOL . PHP_EOL
8✔
680
                        . '<<<=============================================>>>' . PHP_EOL
8✔
681
                        . 'SQL:' . PHP_EOL . "{$query}" . PHP_EOL . PHP_EOL
8✔
682
                        . 'BIND PARAMS:' . PHP_EOL . var_export($parameters, true)
8✔
683
                        . PHP_EOL . "Call Backtrace: "  . var_export($call_trace, true)
8✔
684
                        . '[[[=============================================]]]' . PHP_EOL
8✔
685
                        . PHP_EOL . PHP_EOL . PHP_EOL
8✔
686
                    );
8✔
687
                }
688
            }
689
        } // if($statement instanceof \PDOStatement)
690
        
691
        return new DBExceuteQueryResult(
1,500✔
692
            pdo_statement: ($statement instanceof \PDOStatement) ? $statement : null,
1,500✔
693
            pdo_statement_execute_result: $result,
1,500✔
694
            query_execution_time_in_seconds: $total_execution_time_in_seconds
1,500✔
695
        );
1,500✔
696
    }
697
    
698
    /**
699
     * Executes a raw query as a wrapper for PDOStatement::execute.
700
     * 
701
     * @param string $query  A SQL query that's valid for \PDO->prepare(..) usage
702
     * @param array  $parameters Optional bound parameters
703
     * @param null|object $calling_object object that called this method
704
     */
705
    public function runQuery(
706
        string $query, 
707
        array $parameters=[], 
708
        ?object $calling_object=null
709
    ): DBExceuteQueryResult {
710

711
        return $this->execute($query, $parameters, $this->connection_name, $calling_object);
128✔
712
    }
713
    
714
    /**
715
     * Runs a query that is supposed to return a row of data from a database table
716
     * 
717
     * @param string $select_query A SQL Select query that's valid for \PDO->prepare(..) usage
718
     * @param array  $parameters Parameters that can be bound to $select_query via \PDOStatement->bindParam(..)
719
     * @param null|object $calling_object object that called this method
720
     * 
721
     * @return mixed result of the query or false on failure or if there are no rows
722
     */
723
    public function dbFetchOne(
724
        string $select_query,
725
        array $parameters = [],
726
        ?object $calling_object=null
727
    ): mixed {
728

729
        $result = $this->execute($select_query, $parameters, $this->connection_name, $calling_object);
212✔
730

731
        return ($result->pdo_statement instanceof \PDOStatement) ? $result->pdo_statement->fetch(\PDO::FETCH_ASSOC) : null;
212✔
732
    }
733

734
    /**
735
     * Runs a query that is supposed to return rows of data from a database table
736
     * 
737
     * @param string $select_query A SQL Select query that's valid for \PDO->prepare(..) usage
738
     * @param array  $parameters Parameters that can be bound to $select_query via \PDOStatement->bindParam(..)
739
     * @param null|object $calling_object object that called this method
740
     * 
741
     * @return mixed[]
742
     */
743
    public function dbFetchAll(
744
        string $select_query,
745
        array $parameters = [],
746
        ?object $calling_object=null
747
    ): array {
748

749
        $result = $this->execute($select_query, $parameters, $this->connection_name, $calling_object);
252✔
750

751
        return ($result->pdo_statement instanceof \PDOStatement) 
252✔
752
                ? $result->pdo_statement->fetchAll(\PDO::FETCH_ASSOC) : [];
252✔
753
    }
754

755
    /**
756
     * Runs a query that is supposed to return an array of values from a column in a database table
757
     * 
758
     * @param string $select_query A SQL Select query that's valid for \PDO->prepare(..) usage
759
     * @param array  $parameters Parameters that can be bound to $select_query via \PDOStatement->bindParam(..)
760
     * @param null|object $calling_object object that called this method
761
     * 
762
     * @return mixed[]
763
     */
764
    public function dbFetchCol(
765
        string $select_query,
766
        array $parameters = [],
767
        ?object $calling_object=null
768
    ): array {
769

770
        $result = $this->execute($select_query, $parameters, $this->connection_name, $calling_object);
1,500✔
771

772
        return ($result->pdo_statement instanceof \PDOStatement) 
1,500✔
773
                ? $result->pdo_statement->fetchAll(\PDO::FETCH_COLUMN, 0) : [];
1,500✔
774
    }
775

776
    /**
777
     * Runs a query that is supposed to return an associative array of data from a database table
778
     * where the keys are the values from the first database column in the select query
779
     * and the values are values from the second database column in the select query.
780
     * 
781
     * For example:
782
     * 
783
     * $key_value_pairs = $this->dbFetchPairs(
784
     *  'Select col_1, col_2 from a_table'
785
     * );
786
     * 
787
     * $key_value_pairs2 = $this->dbFetchPairs(
788
     *   'Select col_1, col_2 from a_table where col_3 > :min_val',
789
     *   ['min_val' => 3]
790
     * );
791
     *  
792
     * 
793
     * @param string $select_query A SQL Select query that's valid for \PDO->prepare(..) usage
794
     * @param array  $parameters Parameters that can be bound to $select_query via \PDOStatement->bindParam(..)
795
     * @param null|object $calling_object object that called this method
796
     * 
797
     * @return array<int|string, mixed>
798
     * @psalm-suppress MixedAssignment
799
     * @psalm-suppress MixedArrayOffset
800
     * @psalm-suppress MixedArrayAccess
801
     */
802
    public function dbFetchPairs(
803
        string $select_query,
804
        array $parameters = [],
805
        ?object $calling_object=null
806
    ): array {
807

808
        $result = $this->execute($select_query, $parameters, $this->connection_name, $calling_object);
12✔
809
        $data = [];
12✔
810
        
811
        if($result->pdo_statement instanceof \PDOStatement) {
12✔
812

813
            while ($row = $result->pdo_statement->fetch(\PDO::FETCH_NUM)) {
12✔
814

815
                $data[$row[0]] = $row[1];
12✔
816
            }
817
        }
818

819
        return $data;
12✔
820
    }
821

822
    /**
823
     * Runs a select query and returns the result of the select query
824
     * 
825
     * @param string $select_query A SQL Select query that's valid for \PDO->prepare(..) usage
826
     * @param array  $parameters Parameters that can be bound to $select_query via \PDOStatement->bindParam(..)
827
     * @param null|object $calling_object object that called this method
828
     */
829
    public function dbFetchValue(
830
        string $select_query,
831
        array $parameters = [],
832
        ?object $calling_object=null
833
    ): mixed {
834

835
        $result = $this->execute($select_query, $parameters, $this->connection_name, $calling_object);
116✔
836

837
        return ($result->pdo_statement instanceof \PDOStatement) 
116✔
838
                ? $result->pdo_statement->fetchColumn(0) : null;
116✔
839
    }
840
}
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