Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.08% covered (success)
98.08%
153 / 156
84.21% covered (success)
84.21%
16 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Db
98.08% covered (success)
98.08%
153 / 156
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
97.96% covered (success)
97.96%
48 / 49
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 (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://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@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    6.5.0
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($error);
153        return $result;
154    }
155
156    /**
157     * Execute SQL
158     *
159     * @param  string $sql
160     * @param  mixed  $adapter
161     * @param  array  $options
162     * @param  string $prefix
163     * @throws Exception
164     * @return void
165     */
166    public static function executeSql(string $sql, mixed $adapter, array $options = [], string $prefix = '\Pop\Db\Adapter\\'): void
167    {
168        if (is_string($adapter)) {
169            $adapter = ucfirst(strtolower($adapter));
170            $class   = $prefix . $adapter;
171
172            if (!class_exists($class)) {
173                throw new Exception('Error: The database adapter ' . $class . ' does not exist.');
174            }
175
176            // If Sqlite
177            if (($adapter == 'Sqlite') ||
178                (($adapter == 'Pdo') && isset($options['type'])) && (strtolower($options['type']) == 'sqlite')) {
179                if (!file_exists($options['database'])) {
180                    touch($options['database']);
181                    chmod($options['database'], 0777);
182                }
183                if (!file_exists($options['database'])) {
184                    throw new Exception('Error: Could not create the database file.');
185                }
186            }
187
188            $db = new $class($options);
189        } else {
190            $db = $adapter;
191        }
192
193        $lines      = explode("\n", $sql);
194        $statements = [];
195
196        if (count($lines) > 0) {
197            // Remove any comments, parse prefix if available
198            $insideComment = false;
199            foreach ($lines as $i => $line) {
200                if (empty($line)) {
201                    unset($lines[$i]);
202                } else {
203                    if (isset($options['prefix'])) {
204                        $lines[$i] = str_replace('[{prefix}]', $options['prefix'], trim($line));
205                    }
206                    if ($insideComment) {
207                        if (str_ends_with($line, '*/')) {
208                            $insideComment = false;
209                        }
210                        unset($lines[$i]);
211                    } else {
212                        if ((str_starts_with($line, '-')) || (str_starts_with($line, '#'))) {
213                            unset($lines[$i]);
214                        } else if (str_starts_with($line, '/*')) {
215                            $line = trim($line);
216                            if ((!str_ends_with($line, '*/')) && (!str_ends_with($line, '*/;'))) {
217                                $insideComment = true;
218                            }
219                            unset($lines[$i]);
220                        } else if (strrpos($line, '--') !== false) {
221                            $lines[$i] = substr($line, 0, strrpos($line, '--'));
222                        } else if (strrpos($line, '/*') !== false) {
223                            $lines[$i] = substr($line, 0, strrpos($line, '/*'));
224                        }
225                    }
226                }
227            }
228
229            $lines            = array_values(array_filter($lines));
230            $currentStatement = null;
231
232            // Assemble statements based on ; delimiter
233            foreach ($lines as $i => $line) {
234                $currentStatement .= ($currentStatement !== null) ? ' ' . $line : $line;
235                if (str_ends_with($line, ';')) {
236                    $statements[]     = $currentStatement;
237                    $currentStatement = null;
238                }
239            }
240
241            if (!empty($statements)) {
242                foreach ($statements as $statement) {
243                    if (!empty($statement)) {
244                        $db->query($statement);
245                    }
246                }
247            }
248        }
249    }
250
251    /**
252     * Execute SQL
253     *
254     * @param  string $sqlFile
255     * @param  mixed  $adapter
256     * @param  array  $options
257     * @param  string $prefix
258     * @throws Exception
259     * @return void
260     */
261    public static function executeSqlFile(string $sqlFile, mixed $adapter, array $options = [], string $prefix = '\Pop\Db\Adapter\\'): void
262    {
263        if (!file_exists($sqlFile)) {
264            throw new Exception("Error: The SQL file '" . $sqlFile . "' does not exist.");
265        }
266
267        self::executeSql(file_get_contents($sqlFile), $adapter, $options, $prefix);
268    }
269
270    /**
271     * Get the available database adapters
272     *
273     * @return array
274     */
275    public static function getAvailableAdapters(): array
276    {
277        $pdoDrivers = (class_exists('Pdo', false)) ? \PDO::getAvailableDrivers() : [];
278
279        return [
280            'mysqli' => (class_exists('mysqli', false)),
281            'pdo'    => [
282                'mysql'  => (in_array('mysql', $pdoDrivers)),
283                'pgsql'  => (in_array('pgsql', $pdoDrivers)),
284                'sqlite' => (in_array('sqlite', $pdoDrivers)),
285                'sqlsrv' => (in_array('sqlsrv', $pdoDrivers))
286            ],
287            'pgsql'  => (function_exists('pg_connect')),
288            'sqlite' => (class_exists('Sqlite3', false)),
289            'sqlsrv' => (function_exists('sqlsrv_connect'))
290        ];
291    }
292
293    /**
294     * Determine if a database adapter is available
295     *
296     * @param  string $adapter
297     * @return bool
298     */
299    public static function isAvailable(string $adapter): bool
300    {
301        $adapter = strtolower($adapter);
302        $result  = false;
303        $type    = null;
304
305        $pdoDrivers = (class_exists('Pdo', false)) ? \PDO::getAvailableDrivers() : [];
306        if (str_contains($adapter, 'pdo_')) {
307            $type    = substr($adapter, 4);
308            $adapter = 'pdo';
309        }
310
311        switch ($adapter) {
312            case 'mysql':
313            case 'mysqli':
314                $result = (class_exists('mysqli', false));
315                break;
316            case 'pdo':
317                $result = (in_array($type, $pdoDrivers));
318                break;
319            case 'pgsql':
320                $result = (function_exists('pg_connect'));
321                break;
322            case 'sqlite':
323                $result = (class_exists('Sqlite3', false));
324                break;
325            case 'sqlsrv':
326                $result = (function_exists('sqlsrv_connect'));
327                break;
328        }
329
330        return $result;
331    }
332
333    /**
334     * Set DB adapter
335     *
336     * @param  Adapter\AbstractAdapter $db
337     * @param  ?string                 $class
338     * @param  ?string                 $prefix
339     * @param  bool                    $isDefault
340     * @return void
341     */
342    public static function setDb(Adapter\AbstractAdapter $db, ?string $class = null, ?string $prefix = null, bool $isDefault = false): void
343    {
344        if ($prefix !== null) {
345            self::$db[$prefix] = $db;
346        }
347
348        if ($class !== null) {
349            self::$db[$class] = $db;
350            $record = new $class();
351            if ($record instanceof Record) {
352                self::$classToTable[$class] = $record->getFullTable();
353            }
354        }
355
356        if ($isDefault) {
357            self::$db['default'] = $db;
358        }
359    }
360
361    /**
362     * Get DB adapter
363     *
364     * @param  ?string $class
365     * @throws Exception
366     * @return Adapter\AbstractAdapter
367     */
368    public static function getDb(?string $class = null): Adapter\AbstractAdapter
369    {
370        $dbAdapter = null;
371
372        // Check for database adapter assigned to a full class name
373        if (($class !== null) && isset(self::$db[$class])) {
374            $dbAdapter = self::$db[$class];
375        // Check for database adapter assigned to a namespace
376        } else if ($class !== null) {
377            foreach (self::$db as $prefix => $adapter) {
378                if (str_starts_with($class, $prefix)) {
379                    $dbAdapter = $adapter;
380                }
381            }
382        }
383
384        // Check if class is actual table name
385        if (($dbAdapter === null) && ($class !== null) && in_array($class, self::$classToTable)) {
386            $class = array_search($class, self::$classToTable);
387            // Direct match
388            if (isset(self::$db[$class])) {
389                $dbAdapter = self::$db[$class];
390            // Check prefixes
391            } else {
392                foreach (self::$db as $prefix => $adapter) {
393                    if (str_starts_with($class, $prefix)) {
394                        $dbAdapter = $adapter;
395                    }
396                }
397            }
398        }
399
400        if (($dbAdapter === null) && isset(self::$db['default'])) {
401            $dbAdapter = self::$db['default'];
402        }
403
404        if ($dbAdapter === null) {
405            throw new Exception('No database adapter was found.');
406        }
407
408        return $dbAdapter;
409    }
410
411    /**
412     * Check for a DB adapter
413     *
414     * @param  ?string $class
415     * @return bool
416     */
417    public static function hasDb(?string $class = null): bool
418    {
419        $result = false;
420
421        if (($class !== null) && isset(self::$db[$class])) {
422            $result = true;
423        } else if ($class !== null) {
424            foreach (self::$db as $prefix => $adapter) {
425                if (str_starts_with($class, $prefix)) {
426                    $result = true;
427                }
428            }
429        }
430
431        if ((!$result) && ($class !== null) && in_array($class, self::$classToTable)) {
432            $table = array_search($class, self::$classToTable);
433            if (isset(self::$db[$table])) {
434                $result = true;
435            }
436        }
437
438        if ((!$result) && isset(self::$db['default'])) {
439            $result = true;
440        }
441
442        return $result;
443    }
444
445    /**
446     * Add class-to-table relationship
447     *
448     * @param  string $class
449     * @param  string $table
450     * @return void
451     */
452    public static function addClassToTable(string $class, string $table): void
453    {
454        self::$classToTable[$class] = $table;
455    }
456
457    /**
458     * Check if class-to-table relationship exists
459     *
460     * @param  string $class
461     * @return bool
462     */
463    public static function hasClassToTable(string $class): bool
464    {
465        return isset(self::$classToTable[$class]);
466    }
467
468    /**
469     * Set DB adapter
470     *
471     * @param  Adapter\AbstractAdapter $db
472     * @param  ?string                 $class
473     * @param  ?string                 $prefix
474     * @return void
475     */
476    public static function setDefaultDb(Adapter\AbstractAdapter $db, ?string $class = null, ?string $prefix = null): void
477    {
478        self::setDb($db, $class, $prefix, true);
479    }
480
481    /**
482     * Get DB adapter (alias)
483     *
484     * @param  ?string  $class
485     * @throws Exception
486     * @return Adapter\AbstractAdapter
487     */
488    public static function db(?string $class = null): Adapter\AbstractAdapter
489    {
490        return self::getDb($class);
491    }
492
493    /**
494     * Get all DB adapters
495     *
496     * @return array
497     */
498    public static function getAll(): array
499    {
500        return self::$db;
501    }
502
503}