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