Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.11% covered (success)
98.11%
156 / 159
84.21% covered (success)
84.21%
16 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Db
98.11% covered (success)
98.11%
156 / 159
84.21% covered (success)
84.21%
16 / 19
88
0.00% covered (danger)
0.00%
0 / 1
 connect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 mysqlConnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pdoConnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pgsqlConnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sqlsrvConnect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sqliteConnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 executeSql
98.08% covered (success)
98.08%
51 / 52
0.00% covered (danger)
0.00%
0 / 1
28
 executeSqlFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAvailableAdapters
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 isAvailable
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
9
 setDb
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getDb
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
15.03
 hasDb
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
12
 addClassToTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasClassToTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 db
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db;
15
16/**
17 * Db class
18 *
19 * @category   Pop
20 * @package    Pop\Db
21 * @author     Nick Sagona, III <dev@noladev.com>
22 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
23 * @license    https://www.popphp.org/license     New BSD License
24 * @version    6.6.5
25 */
26class Db
27{
28
29    /**
30     * Database connection(s)
31     * @var array
32     */
33    protected static array $db = ['default' => null];
34
35    /**
36     * Database connection class to table relationship
37     * @var array
38     */
39    protected static array $classToTable = [];
40
41    /**
42     * Method to connect to a database and return the database adapter object
43     *
44     * @param  string $adapter
45     * @param  array  $options
46     * @param  string $prefix
47     * @throws Exception
48     * @return Adapter\AbstractAdapter
49     */
50    public static function connect(string $adapter, array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\AbstractAdapter
51    {
52        $class = $prefix . ucfirst(strtolower($adapter));
53
54        if (!class_exists($class)) {
55            throw new Exception('Error: The database adapter ' . $class . ' does not exist.');
56        }
57
58        return new $class($options);
59    }
60
61    /**
62     * Method to connect to a MySQL database and return the MySQL database adapter object
63     *
64     * @param  array  $options
65     * @param  string $prefix
66     * @throws Exception
67     * @return Adapter\Mysql|Adapter\AbstractAdapter
68     */
69    public static function mysqlConnect(array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\Mysql|Adapter\AbstractAdapter
70    {
71        return self::connect('mysql', $options, $prefix);
72    }
73
74    /**
75     * Method to connect to a PDO database and return the PDO database adapter object
76     *
77     * @param  array  $options
78     * @param  string $prefix
79     * @throws Exception
80     * @return Adapter\Pdo|Adapter\AbstractAdapter
81     */
82    public static function pdoConnect(array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\Pdo|Adapter\AbstractAdapter
83    {
84        return self::connect('pdo', $options, $prefix);
85    }
86
87    /**
88     * Method to connect to a PostgreSQL database and return the PostgreSQL database adapter object
89     *
90     * @param  array  $options
91     * @param  string $prefix
92     * @throws Exception
93     * @return Adapter\Pgsql|Adapter\AbstractAdapter
94     */
95    public static function pgsqlConnect(array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\Pgsql|Adapter\AbstractAdapter
96    {
97        return self::connect('pgsql', $options, $prefix);
98    }
99
100    /**
101     * Method to connect to a SQL Server database and return the SQL Server database adapter object
102     *
103     * @param  array  $options
104     * @param  string $prefix
105     * @throws Exception
106     * @return Adapter\Sqlsrv|Adapter\AbstractAdapter
107     */
108    public static function sqlsrvConnect(array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\AbstractAdapter|Adapter\Sqlsrv
109    {
110        return self::connect('sqlsrv', $options, $prefix);
111    }
112
113    /**
114     * Method to connect to a SQLite database and return the SQLite database adapter object
115     *
116     * @param  array  $options
117     * @param  string $prefix
118     * @throws Exception
119     * @return Adapter\Sqlite|Adapter\AbstractAdapter
120     */
121    public static function sqliteConnect(array $options, string $prefix = '\Pop\Db\Adapter\\'): Adapter\Sqlite|Adapter\AbstractAdapter
122    {
123        return self::connect('sqlite', $options, $prefix);
124    }
125
126    /**
127     * Check the database connection
128     *
129     * @param  string $adapter
130     * @param  array  $options
131     * @param  string $prefix
132     * @return mixed
133     */
134    public static function check(string $adapter, array $options, string $prefix = '\Pop\Db\Adapter\\'): mixed
135    {
136        $result = true;
137        $class  = $prefix . ucfirst(strtolower($adapter));
138        $error  = ini_get('error_reporting');
139
140        error_reporting(E_ERROR);
141
142        try {
143            if (!class_exists($class)) {
144                $result = "Error: The database adapter '" . $class . "' does not exist.";
145            } else {
146                $db = new $class($options);
147            }
148        } catch (\Exception $e) {
149            $result = $e->getMessage();
150        }
151
152        error_reporting((int)$error);
153
154        return $result;
155    }
156
157    /**
158     * Execute SQL
159     *
160     * @param  string $sql
161     * @param  mixed  $adapter
162     * @param  array  $options
163     * @param  string $prefix
164     * @throws Exception
165     * @return int
166     */
167    public static function executeSql(
168        string $sql, mixed $adapter, array $options = [], string $prefix = '\Pop\Db\Adapter\\'
169    ): int
170    {
171        $affectedRows = 0;
172
173        if (is_string($adapter)) {
174            $adapter = ucfirst(strtolower($adapter));
175            $class   = $prefix . $adapter;
176
177            if (!class_exists($class)) {
178                throw new Exception('Error: The database adapter ' . $class . ' does not exist.');
179            }
180
181            // If Sqlite
182            if (($adapter == 'Sqlite') ||
183                (($adapter == 'Pdo') && isset($options['type'])) && (strtolower($options['type']) == 'sqlite')) {
184                if (!file_exists($options['database'])) {
185                    touch($options['database']);
186                    chmod($options['database'], 0777);
187                }
188                if (!file_exists($options['database'])) {
189                    throw new Exception('Error: Could not create the database file.');
190                }
191            }
192
193            $db = new $class($options);
194        } else {
195            $db = $adapter;
196        }
197
198        $lines      = explode("\n", $sql);
199        $statements = [];
200
201        if (count($lines) > 0) {
202            // Remove any comments, parse prefix if available
203            $insideComment = false;
204            foreach ($lines as $i => $line) {
205                if (empty($line)) {
206                    unset($lines[$i]);
207                } else {
208                    if (isset($options['prefix'])) {
209                        $lines[$i] = str_replace('[{prefix}]', $options['prefix'], trim($line));
210                    }
211                    if ($insideComment) {
212                        if (str_ends_with($line, '*/')) {
213                            $insideComment = false;
214                        }
215                        unset($lines[$i]);
216                    } else {
217                        if ((str_starts_with($line, '-')) || (str_starts_with($line, '#'))) {
218                            unset($lines[$i]);
219                        } else if (str_starts_with($line, '/*')) {
220                            $line = trim($line);
221                            if ((!str_ends_with($line, '*/')) && (!str_ends_with($line, '*/;'))) {
222                                $insideComment = true;
223                            }
224                            unset($lines[$i]);
225                        } else if (strrpos($line, '--') !== false) {
226                            $lines[$i] = substr($line, 0, strrpos($line, '--'));
227                        } else if (strrpos($line, '/*') !== false) {
228                            $lines[$i] = substr($line, 0, strrpos($line, '/*'));
229                        }
230                    }
231                }
232            }
233
234            $lines            = array_values(array_filter($lines));
235            $currentStatement = null;
236
237            // Assemble statements based on ; delimiter
238            foreach ($lines as $i => $line) {
239                $currentStatement .= ($currentStatement !== null) ? ' ' . $line : $line;
240                if (str_ends_with($line, ';')) {
241                    $statements[]     = $currentStatement;
242                    $currentStatement = null;
243                }
244            }
245
246            if (!empty($statements)) {
247                foreach ($statements as $statement) {
248                    if (!empty($statement)) {
249                        $db->query($statement);
250                        $affectedRows += $db->getNumberOfAffectedRows();
251                    }
252                }
253            }
254        }
255
256        return $affectedRows;
257    }
258
259    /**
260     * Execute SQL
261     *
262     * @param  string $sqlFile
263     * @param  mixed  $adapter
264     * @param  array  $options
265     * @param  string $prefix
266     * @throws Exception
267     * @return int
268     */
269    public static function executeSqlFile(
270        string $sqlFile, mixed $adapter, array $options = [], string $prefix = '\Pop\Db\Adapter\\'
271    ): int
272    {
273        if (!file_exists($sqlFile)) {
274            throw new Exception("Error: The SQL file '" . $sqlFile . "' does not exist.");
275        }
276
277        return self::executeSql(file_get_contents($sqlFile), $adapter, $options, $prefix);
278    }
279
280    /**
281     * Get the available database adapters
282     *
283     * @return array
284     */
285    public static function getAvailableAdapters(): array
286    {
287        $pdoDrivers = (class_exists('Pdo', false)) ? \PDO::getAvailableDrivers() : [];
288
289        return [
290            'mysqli' => (class_exists('mysqli', false)),
291            'pdo'    => [
292                'mysql'  => (in_array('mysql', $pdoDrivers)),
293                'pgsql'  => (in_array('pgsql', $pdoDrivers)),
294                'sqlite' => (in_array('sqlite', $pdoDrivers)),
295                'sqlsrv' => (in_array('sqlsrv', $pdoDrivers))
296            ],
297            'pgsql'  => (function_exists('pg_connect')),
298            'sqlite' => (class_exists('Sqlite3', false)),
299            'sqlsrv' => (function_exists('sqlsrv_connect'))
300        ];
301    }
302
303    /**
304     * Determine if a database adapter is available
305     *
306     * @param  string $adapter
307     * @return bool
308     */
309    public static function isAvailable(string $adapter): bool
310    {
311        $adapter = strtolower($adapter);
312        $result  = false;
313        $type    = null;
314
315        $pdoDrivers = (class_exists('Pdo', false)) ? \PDO::getAvailableDrivers() : [];
316        if (str_contains($adapter, 'pdo_')) {
317            $type    = substr($adapter, 4);
318            $adapter = 'pdo';
319        }
320
321        switch ($adapter) {
322            case 'mysql':
323            case 'mysqli':
324                $result = (class_exists('mysqli', false));
325                break;
326            case 'pdo':
327                $result = (in_array($type, $pdoDrivers));
328                break;
329            case 'pgsql':
330                $result = (function_exists('pg_connect'));
331                break;
332            case 'sqlite':
333                $result = (class_exists('Sqlite3', false));
334                break;
335            case 'sqlsrv':
336                $result = (function_exists('sqlsrv_connect'));
337                break;
338        }
339
340        return $result;
341    }
342
343    /**
344     * Set DB adapter
345     *
346     * @param  Adapter\AbstractAdapter $db
347     * @param  ?string                 $class
348     * @param  ?string                 $prefix
349     * @param  bool                    $isDefault
350     * @return void
351     */
352    public static function setDb(Adapter\AbstractAdapter $db, ?string $class = null, ?string $prefix = null, bool $isDefault = false): void
353    {
354        if ($prefix !== null) {
355            self::$db[$prefix] = $db;
356        }
357
358        if ($class !== null) {
359            self::$db[$class] = $db;
360            $record = new $class();
361            if ($record instanceof Record) {
362                self::$classToTable[$class] = $record->getFullTable();
363            }
364        }
365
366        if ($isDefault) {
367            self::$db['default'] = $db;
368        }
369    }
370
371    /**
372     * Get DB adapter
373     *
374     * @param  ?string $class
375     * @throws Exception
376     * @return Adapter\AbstractAdapter
377     */
378    public static function getDb(?string $class = null): Adapter\AbstractAdapter
379    {
380        $dbAdapter = null;
381
382        // Check for database adapter assigned to a full class name
383        if (($class !== null) && isset(self::$db[$class])) {
384            $dbAdapter = self::$db[$class];
385        // Check for database adapter assigned to a namespace
386        } else if ($class !== null) {
387            foreach (self::$db as $prefix => $adapter) {
388                if (str_starts_with($class, $prefix)) {
389                    $dbAdapter = $adapter;
390                }
391            }
392        }
393
394        // Check if class is actual table name
395        if (($dbAdapter === null) && ($class !== null) && in_array($class, self::$classToTable)) {
396            $class = array_search($class, self::$classToTable);
397            // Direct match
398            if (isset(self::$db[$class])) {
399                $dbAdapter = self::$db[$class];
400            // Check prefixes
401            } else {
402                foreach (self::$db as $prefix => $adapter) {
403                    if (str_starts_with($class, $prefix)) {
404                        $dbAdapter = $adapter;
405                    }
406                }
407            }
408        }
409
410        if (($dbAdapter === null) && isset(self::$db['default'])) {
411            $dbAdapter = self::$db['default'];
412        }
413
414        if ($dbAdapter === null) {
415            throw new Exception('No database adapter was found.');
416        }
417
418        return $dbAdapter;
419    }
420
421    /**
422     * Check for a DB adapter
423     *
424     * @param  ?string $class
425     * @return bool
426     */
427    public static function hasDb(?string $class = null): bool
428    {
429        $result = false;
430
431        if (($class !== null) && isset(self::$db[$class])) {
432            $result = true;
433        } else if ($class !== null) {
434            foreach (self::$db as $prefix => $adapter) {
435                if (str_starts_with($class, $prefix)) {
436                    $result = true;
437                }
438            }
439        }
440
441        if ((!$result) && ($class !== null) && in_array($class, self::$classToTable)) {
442            $table = array_search($class, self::$classToTable);
443            if (isset(self::$db[$table])) {
444                $result = true;
445            }
446        }
447
448        if ((!$result) && isset(self::$db['default'])) {
449            $result = true;
450        }
451
452        return $result;
453    }
454
455    /**
456     * Add class-to-table relationship
457     *
458     * @param  string $class
459     * @param  string $table
460     * @return void
461     */
462    public static function addClassToTable(string $class, string $table): void
463    {
464        self::$classToTable[$class] = $table;
465    }
466
467    /**
468     * Check if class-to-table relationship exists
469     *
470     * @param  string $class
471     * @return bool
472     */
473    public static function hasClassToTable(string $class): bool
474    {
475        return isset(self::$classToTable[$class]);
476    }
477
478    /**
479     * Set DB adapter
480     *
481     * @param  Adapter\AbstractAdapter $db
482     * @param  ?string                 $class
483     * @param  ?string                 $prefix
484     * @return void
485     */
486    public static function setDefaultDb(Adapter\AbstractAdapter $db, ?string $class = null, ?string $prefix = null): void
487    {
488        self::setDb($db, $class, $prefix, true);
489    }
490
491    /**
492     * Get DB adapter (alias)
493     *
494     * @param  ?string  $class
495     * @throws Exception
496     * @return Adapter\AbstractAdapter
497     */
498    public static function db(?string $class = null): Adapter\AbstractAdapter
499    {
500        return self::getDb($class);
501    }
502
503    /**
504     * Get all DB adapters
505     *
506     * @return array
507     */
508    public static function getAll(): array
509    {
510        return self::$db;
511    }
512
513}